Repository navigation
Diagnostics: identify the double close behind the async Closer EBADF assertion on the asan lane - #38630
Diagnostics: identify the double close behind the async Closer EBADF assertion on the asan lane#38630robobun wants to merge 8 commits into
Conversation
| //! Debug-only (`debug_assertions`) diagnostics for double closes. | ||
| //! | ||
| //! Every successful `close(2)` issued through `bun_sys` records the calling | ||
| //! thread and a backtrace, keyed by fd number. When a later close of the same | ||
| //! number fails with `EBADF` (a use-after-close, or a second owner closing a | ||
| //! descriptor it does not own), the reporter can name the code path that | ||
| //! closed it first. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
|
Status: diagnostics-only PR, not for merging. Waiting on the x64-asan lane of build 96382 to hit the |
…d of std::backtrace
|
Updated 12:41 PM PT - Aug 14th, 2026
⏳ @autofix-ci[bot], your commit b334a7d is still building in |
| //! Debug-only (`debug_assertions`) diagnostics for double closes. | ||
| //! | ||
| //! Every successful `close(2)` issued through `bun_sys` records the calling | ||
| //! thread and a frame-pointer stack trace, keyed by fd number. When a later | ||
| //! close of the same number fails with `EBADF` (a use-after-close, or a second | ||
| //! owner closing a descriptor it does not own), the reporter prints the code | ||
| //! path that closed it first. Traces are printed as raw return addresses | ||
| //! (symbolize them against the binary with llvm-symbolizer). |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// Print an EBADF report for a close attempt of `fd` made by `what`: the | ||
| /// current stack, then the stack that closed `fd` last. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
|
The comment-style notes above are on the throwaway instrumentation and will go away with it; nothing in this PR is meant to land. Current state: build 96420 has the instrumented asan binary built and its 20 test shards queued. Once a shard reproduces the |
…port epoll registration failures
| /// What the fd pointed at when the close was scheduled (empty if it | ||
| /// was already closed by then). |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| //! Debug-only (`debug_assertions`) diagnostics for double closes. | ||
| //! | ||
| //! Every successful `close(2)` issued through `bun_sys` records the calling | ||
| //! thread, what the descriptor pointed at (`/proc/self/fd/N`), and a | ||
| //! frame-pointer stack trace, keyed by fd number (the last few closes of each | ||
| //! number are kept). When a later close of the same number fails with `EBADF` | ||
| //! (a use-after-close, or a second owner closing a descriptor it does not | ||
| //! own), the reporter prints that history. Traces are printed as raw return | ||
| //! addresses (symbolize them against the binary with llvm-symbolizer). |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// Record that `fd` (which pointed at `description` before the call) was just | ||
| /// closed successfully by the current thread. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// Print an EBADF report for a close attempt of `fd` made by `what`: the | ||
| /// current stack, then the close history of `fd`. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// Print the current stack plus what `fd`, stdout and stderr point at. Used to | ||
| /// report failures to register `fd` with the event loop. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
|
Round 2 results (build 96420, instrumented asan binary). The bot notes above are on the throwaway instrumentation and are not being addressed. The crash reproduced twice with the ledger active (the
That layout matches Still open: why |
| /// Event-loop registration events (epoll add/del) per fd, same shape as the | ||
| /// close ledger. `kind` is a free-form label supplied by the event loop. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// Print the current stack plus what `fd`, stdout and stderr point at, the | ||
| /// registration events recorded for `fd`, and its close history. Used to | ||
| /// report failures to register `fd` with the event loop. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
|
Round 3 results (build 96482). The bot notes on the instrumentation are still not being addressed; it is all going away. Confirmed end to end, twice (the
The fix for the crash is in |
Diagnostics only, do not merge
The x64-asan test lane intermittently aborts a
bun test --test-worker --isolateprocess with:i.e. by the time the async
Closerran, the fd it was handed had already been closed by something else. The crash report does not say which fd or which object scheduled the close, and it has not reproduced locally with a debug build (the same 52-file batch passes under--isolateand--parallel=N). It reproduces on the asan lane in most builds (seen in builds 95992, 95993, 95999, 96000 x2, 96094 x2, usually attributed to the file that just finished running, e.g.test/cli/test/concurrent-test-glob.test.ts,test/js/node/events/event-emitter.test.ts,test/js/node/readline/stdin-pause-pty.test.ts).This PR adds
debug_assertions-only instrumentation (Linux) so the next occurrence names both sides of the double close:bun_sys::close_ledger: every successful close issued throughbun_sysrecords a backtrace keyed by fd number; anEBADFclose prints the attempt's backtrace plus the recorded first closer.Closercaptures a backtrace when it is scheduled and prints it before panicking if its close comes backEBADF.The instrumentation will be removed; the real fix goes in a separate PR once the CI output identifies the owner.