Repository navigation
Conversation
|
Updated 2:22 AM PT - Sep 8th, 2026
@autofix-ci[bot], your commit 82af3a8 is building: |
| /// Diagnostic: the loop `Bun.spawnSync` runs on (set by `SpawnSyncEventLoop`). | ||
| /// With `BUN_DEBUG_SPAWNSYNC_DRIFT` set, every counter adjustment that lands on | ||
| /// it is printed with a backtrace, so an inc/dec that belongs to the main loop | ||
| /// but hits this one can be attributed. |
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
| // Raw return addresses as offsets into the executable, for a symbolizer, | ||
| // in case the names above come out as `<unknown>`. |
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
| // bun links as ET_EXEC (no PIE), so the address itself is what a | ||
| // symbolizer wants; tag the ones inside the executable. |
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 three comment-cop findings are on |
| // Return addresses via the frame-pointer walk the crash handler uses. bun | ||
| // links as ET_EXEC, so an address is what a symbolizer wants as-is. |
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
|
Same as above for the new comment-cop finding on |
| // PROBE: the loop can iterate without a wake source when the isolated | ||
| // loop has no polls registered (`us_loop_run_bun_tick` returns at | ||
| // once on `num_polls == 0`) while some stdio still reports pending | ||
| // activity. Name what is pending the first time that happens. |
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-cop finding on |
…over While Bun.spawnSync() waits for its child it points vm.event_loop_handle at a private uSockets loop. It restored the handle only when the call returned, after it had built the result object. Building the result allocates on the JS heap (the stdout and stderr Buffers, the resourceUsage object), and an allocation can run finalizers of dead cells. FilePoll::deinit resolves its loop through vm.event_loop_handle, so a finalizer that released a main-loop poll in that window debited the private loop instead: its num_polls went negative and the main loop kept a poll count nothing would ever release. A private loop left at -2 makes the next spawnSync spin: with its three polls registered num_polls is 1, the first EOF takes it to 0, and us_loop_run_bun_tick returns before epoll_wait whenever num_polls is 0. The wait loop then turns at full speed and never collects the child's exit. That is the hang behind linker-order.test.ts timing out in the aarch64 parallel test batch: every file of an --isolate worker leaves the previous global's process.stdout/stderr FileSinks behind, each holding a poll on a dup() of the worker's stdio socket, and the test's spawnSync(llvm-nm) calls finalized them. Restore the handle right after the wait loop, before any JS value is created. cleanup() becomes idempotent so the existing scope guard still covers the early-return paths.
f1a16f0 to
4f7e569
Compare
| //! - Temporarily overrides vm.event_loop_handle to point to the isolated loop. | ||
| //! Poll and keep-alive accounting resolves its loop through that handle, so | ||
| //! the override must end before anything that can run GC finalizers of | ||
| //! main-loop objects (any JS allocation); `spawnSync` calls `cleanup` as soon | ||
| //! as its wait is over. |
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
| /// Restore the original event loop handle. Idempotent: `spawnSync` calls it | ||
| /// as soon as its wait is over, and again on every exit path. |
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
| // Early returns before the wait; after the wait `cleanup` already ran | ||
| // and this is a no-op. |
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
| // Restore the VM's loop before the JS allocations below: they can run | ||
| // finalizers, and a finalizer that releases a main-loop poll resolves | ||
| // `vm.event_loop_handle`. It must not debit this private 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
|
Closing this in favor of #40078. The root cause is the one that PR fixes: a GC finalizer that runs while The branch now holds only a smaller alternative fix (restore the handle right after the wait loop, +25/-3 under |
Closed in favor of #40078, which fixes the same root cause. This branch keeps a smaller alternative and a deterministic fixture, described in #40078 (comment).
Problem
test/js/bun/perf/linker-order.test.ts(function tracer > records exact entries, and keeps them across an exec'd child) timed out after 90 s in thebun test --parallelbatch on the aarch64 glibc lanes (builds 110996, 111195, 111496, 111536) and passed alone.spawnSync(llvm-nm)spun:Bun.spawnSyncpointsvm.event_loop_handleat a private loop until the call returns, and it built its result (JS allocations) before restoring it. A GC finalizer that ran there and released a main-loop poll (FilePoll::deinitresolvesvm.event_loop_handle) debited the private loop. With the private loop at -2, the next spawnSync readnum_polls == 0with its pidfd and stdout still armed, andus_loop_run_bun_tickreturned beforeepoll_waiton every turn.process.stdout/process.stderrsinks that every file of an--isolateworker leaves behind (FileSinks polling a dup of the worker's stdio socket).Fix
SpawnSyncEventLoop::cleanupis idempotent so the scope guard still covers early returns.test/js/bun/spawn/spawnsync-isolated-event-loop.test.ts(two new cases, both fail on an unfixed build and pass with the change), plus the spawn, spawnSync and child_process suites.