Conversation
|
Updated 3:06 PM PT - Jul 22nd, 2026
❌ @robobun, your commit aab4381 has 2 failures in
🧪 To try this PR locally: bunx bun-pr 34270That installs a local version of the PR into your bun-34270 --bun |
|
Found 1 issue this PR may fix:
🤖 Generated with Claude Code |
|
Re the bot's suggestion: #34095 is the |
WalkthroughChangesThe scheduler now marks VM teardown atomically, rejects deferred work during termination, and skips execution when script execution is stopped or a termination exception is pending. A worker lifetime regression test verifies clean process termination. Worker termination handling
Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/js/web/workers/worker-terminate-lifetime.test.ts`:
- Around line 166-176: Update the expectation in the worker termination test to
assert the raw stderr value unconditionally, without gating it on exitCode.
Reorder the expected fields to the established canonical sequence: stderr,
stdout, exitCode, then signalCode, while preserving their existing expected
values.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: fde047fb-1537-41c8-bc81-bf916f0d730c
📒 Files selected for processing (2)
src/jsc/bindings/JSCTaskScheduler.cpptest/js/web/workers/worker-terminate-lifetime.test.ts
There was a problem hiding this comment.
I didn't find any bugs, but this touches the core JSC DeferredWorkTimer integration path (shared by FinalizationRegistry, Atomics.waitAsync, and Wasm streaming) during worker termination — worth a maintainer's eyes, especially given the noted overlap with #30857 in the same block.
What was reviewed:
- The new guard mirrors upstream
DeferredWorkTimer::doWork(scriptExecutionStatus == Running && !hasPendingTerminationException());scriptExecutionStatusis populated in both the ZigGlobalObject and NodeVM method tables. ticket->target()/scriptExecutionOwner()are dereferenced only after!isCancelled(), matching the existingjob->vm()accessor's assumption.- Main-thread path: status is
Runningand no termination exception is pending, so the guard is a no-op there. - Test follows the file's conventions; the CodeRabbit stderr-assertion feedback was applied in 2204cec.
Extended reasoning...
Overview
The PR adds a two-condition guard in JSCTaskScheduler::runPendingWork (src/jsc/bindings/JSCTaskScheduler.cpp) that skips a queued DeferredWorkTimer task when the ticket's global object reports scriptExecutionStatus != Running or the VM has a pending TerminationException. It threads the JSC::VM& through from Bun__runDeferredWork and adds DeferredWorkTimerInlines.h / GlobalObjectMethodTable.h includes. A new debug/ASAN-gated regression test in worker-terminate-lifetime.test.ts reproduces the executeCallImpl assertNoException() abort via a FinalizationRegistry cleanup racing worker.terminate().
Security risks
None identified. The change is purely a drop-this-task guard on an internal scheduler path; no user input parsing, no auth/crypto, no new external surface.
Level of scrutiny
Medium-high. The diff is tiny (~10 C++ lines) and directly mirrors upstream JSC's DeferredWorkTimer::doWork, which is a strong correctness signal. But the code path is load-bearing for every DeferredWorkTimer client — not just FinalizationRegistry but also Atomics.waitAsync and Wasm streaming compilation — and worker-termination races are historically subtle in this repo (the test file itself is a catalogue of prior UAFs and thread-affinity asserts). A maintainer who owns Bun's JSC integration should confirm that silently dropping the task (rather than, e.g., cancelling the ticket or clearing the exception) is the intended semantics for all three clients, and coordinate the merge order with #30857 which the description says touches the same block.
Other factors
- I checked that
scriptExecutionStatusis a non-null method-table entry for bothZig::GlobalObjectand the NodeVM global (ZigGlobalObject.h:208,NodeVM.cpp:906), so the indirect call is safe. job->ticket->target()and->scriptExecutionOwner()are only reached afterpendingTicket && !pendingTicket->isCancelled(); the pre-existingJSCDeferredWorkTask::vm()already dereferencesscriptExecutionOwner()unconditionally, so no new null-deref surface is introduced.- On the main thread,
scriptExecutionStatusreturnsRunningand there's no termination exception, so the guard is a no-op — no behaviour change for the common case. - The test is well-structured (subprocess spawn, concurrent pipe drain, unconditional
{stderr, stdout, exitCode, signalCode}assertion) and the one CodeRabbit review comment was addressed and resolved. - No prior claude[bot] review on this PR.
3d542e2 to
3ba6d4a
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/jsc/bindings/JSCTaskScheduler.cpp`:
- Around line 55-64: Update the scheduling path around the termination check and
Bun__queueJSCDeferredWorkTaskConcurrently so termination cannot begin between
the check and enqueue. Serialize the m_isTerminating transition with enqueue, or
implement an equivalent cancellation-safe handoff that removes or cancels any
task raced by teardown while preserving onCancelPendingWork balancing.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: a15d5590-6e21-469b-b5d6-1d9698b125ea
📒 Files selected for processing (5)
src/jsc/bindings/JSCTaskScheduler.cppsrc/jsc/bindings/JSCTaskScheduler.hsrc/jsc/bindings/ZigGlobalObject.cppsrc/jsc/bindings/webcore/Worker.cpptest/js/web/workers/worker-terminate-lifetime.test.ts
There was a problem hiding this comment.
91d991a applied the destructOnExit sibling store — all three of my earlier findings are now addressed and I have nothing further. Deferring to a human given this is worker-VM teardown / JSC DeferredWorkTimer plumbing with cross-thread atomics.
What was reviewed:
runPendingWorkgate matches JSC'sDeferredWorkTimer::doWork(scriptExecutionStatus == Running && !hasPendingTerminationException).onScheduleWorkSoongates on the new dedicatedm_isTerminatingatomic (not the transientVM::hasTerminationRequest), set from both teardown mirrors.onCancelPendingWorkon the drop path balances theonAddPendingWorkevent-loop ref;jobis deleted on the skipped-run path.- Test follows the file's existing subprocess pattern; asserts stderr unconditionally per earlier feedback.
Extended reasoning...
Overview
The PR adds two guards to Bun's JSCTaskScheduler (the DeferredWorkTimer replacement) so that FinalizationRegistry / Atomics.waitAsync / Wasm-streaming tasks are dropped once a worker is terminating: a run-time gate in runPendingWork mirroring JSC's own DeferredWorkTimer::doWork, and a schedule-time gate in onScheduleWorkSoon keyed on a new std::atomic<bool> m_isTerminating. The atomic is set from both WebWorker__teardownJSCVM and Zig__GlobalObject__destructOnExit. A debug/ASAN-gated regression test spawns a subprocess that terminates workers with pending FinalizationRegistry cleanup.
Files touched: JSCTaskScheduler.{cpp,h}, one-line stores in Worker.cpp and ZigGlobalObject.cpp, and worker-terminate-lifetime.test.ts.
Review history on this PR
I've left three prior inline findings, each addressed by a subsequent commit:
- Botched force-push that stripped the src change → fixed in 3ba6d4a.
onScheduleWorkSoongating on transientVM::hasTerminationRequest()(which node:vm{timeout}/{breakOnSigint}sets and clears) → replaced with a dedicated non-transient atomic in 69525d3.- Missing sibling store in
Zig__GlobalObject__destructOnExit→ added in 91d991a.
The bug hunting system found nothing new at HEAD.
Security risks
None identified. This is teardown-path lifecycle management; no user input parsing, auth, or crypto.
Level of scrutiny
High. This is JSC bindings code on the worker VM teardown path, involving cross-thread atomics with explicit acquire/release ordering, GC finalization callbacks that can run off the JS thread (Atomics.notify), and TerminationException sequencing. CLAUDE.md's memory-safety and thread-affinity sections flag exactly this class of change as the most-blocked category. The PR has already been through three correctness iterations during review, which itself signals the subtlety.
Other factors
- The
runPendingWorkchange is a direct port of upstream JSC'sDeferredWorkTimer::doWorkbehavior, which lowers risk for that hunk. - The
m_isTerminatingatomic is set-once/never-cleared, read with acquire against a release store — the ordering choice is defensible and commented. - The PR description notes an adjacent PR (#30857) touching the same block; whichever lands second needs a rebase, worth a human confirming the interaction.
- The test is debug/ASAN-only (the assert compiles out in release), which is appropriate but means release CI won't exercise it.
Given all prior feedback is addressed and no new issues surfaced, this looks correct to me — but it is not the kind of mechanical change I'd approve without a human maintainer's eyes on worker teardown sequencing.
|
CI on 91d991a (build 73453): the new
Diff is ready for review. |
|
This also fixes |
…loop's last tick (#34293) `test/js/web/timers/timer-heap-race.test.ts` went red on the x64-asan lane after #33131 landed (build 73570): ``` SUMMARY: AddressSanitizer: 1280 byte(s) leaked in 40 allocation(s). ConcurrentTask::create src/event_loop/ConcurrentTask.rs:319 Bun__queueJSCDeferredWorkTaskConcurrently src/jsc/JSCScheduler.rs:64 Bun::JSCTaskScheduler::onScheduleWorkSoon JSCTaskScheduler.cpp:54 JSC::DeferredWorkTimer::scheduleWorkSoon DeferredWorkTimer.cpp:235 JSC::Waiter::cancelAndClear WaiterListManager.cpp:298 JSC::WaiterListManager::unregister(JSC::VM*) WaiterListManager.cpp:310 JSC::VM::~VM() VM.cpp:591 WebWorker__teardownJSCVM Worker.cpp:676 ``` The leak is pre-existing; #33131's new cross-thread `Atomics.waitAsync` fixture is the first test that terminates a worker with pending async waiters on a SharedArrayBuffer the parent keeps alive. ### Cause Worker `shutdown()` drains the concurrent task queue once (`release_queued_tasks_for_shutdown`), then calls `WebWorker__teardownJSCVM`, which ends in `~VM()`. `~VM()` runs `WaiterListManager::unregister(this)`, and for every pending `Atomics.waitAsync` ticket that reaches `Waiter::cancelAndClear` → `DeferredWorkTimer::scheduleWorkSoon` → our `onScheduleWorkSoon` hook. The hook allocates a `JSCDeferredWorkTask` and a `ConcurrentTask` and enqueues them into the worker's concurrent queue, which was just drained for the last time. When the worker's `VirtualMachine` box is raw-`dealloc`'d, both become unreachable. The same path is reachable from the final `collectNow` via `JSFinalizationRegistry::finalizeUnconditionally`. A second, narrower leak: a cross-thread `Atomics.notify` that lands between the worker's last tick and `teardownJSCVM` enqueues a `JSCDeferredWorkTask` that `release_queued_tasks_for_shutdown` forwards into `self.tasks`. `__bun_release_task_at_shutdown` had no arm for that tag, so it was re-queued, and `EventLoop::deinit` re-queued it once more into a freshly allocated `LinearFifo` buffer that leaked on worker dealloc. ### Fix - `JSCTaskScheduler` gets an `std::atomic<bool> m_isShuttingDown`, set at the start of `WebWorker__teardownJSCVM` and `Zig__GlobalObject__destructOnExit` (mirroring the existing `ctx->markTerminating()`). `onScheduleWorkSoon` and `onAddPendingWork` drop the work once it's set; `onScheduleWorkSoon` also balances the `onAddPendingWork` ref via `onCancelPendingWork`. - `__bun_release_task_at_shutdown` gains a `JSCDeferredWorkTask` arm that deletes the job via a new `Bun__deleteDeferredWorkTask` FFI. This runs before JSC teardown, so `~Ref<TicketData>` and the captured `Task` lambda release against a live VM. ### Test `timer-heap-atomics-teardown-fixture.ts` terminates a worker with 32 pending `Atomics.waitAsync` tickets on a parent-owned SAB, a few of them notified cross-thread first, under `detect_leaks=1`. Without the fix LSan reports ~29 `ConcurrentTask` allocations from `WaiterListManager::unregister` and SIGABRTs; with it the fixture exits clean. The original race fixture is also 0/10 failures under the CI env (was ~1/5 on a debug build and 1/1 on release-asan). ### Overlap with #34270 \#34270 adds the same `m_isShuttingDown`/`onScheduleWorkSoon` gate (there named `m_isTerminating`) while fixing a separate `FinalizationRegistry` assert, but without the `__bun_release_task_at_shutdown` arm the race fixture still fails ~1/10 on that branch. Whichever lands first, the other is a small rebase over `JSCTaskScheduler.{h,cpp}`. <!-- robobun:evidence:begin --> --- **[review]** gate passed · iteration 3 · 9 files touched <details><summary>fails on main (without fix)</summary> ```console ASAN without fix: 1 FAILED $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/web/timers/timer-heap-race.test.ts info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05) info: component rust-src is up to date info: checking for self-update (current version: 1.29.0) bun test v1.4.0 (4d9b926) test/js/web/timers/timer-heap-race.test.ts: (pass) timer heap survives cross-thread Atomics.waitAsync timeout cancellation [3806.16ms] (pass) timer heap stays consistent while GC re-arms the RunLoop timer [2408.11ms] 51 | ASAN_OPTIONS: "allow_user_segv_handler=1:disable_coredump=0:detect_leaks=1:abort_on_error=1", 52 | LSAN_OPTIONS: `malloc_context_size=30:print_suppressions=0:suppressions=${path.join(import.meta.dir, "..", "..", "..", "leaksan.supp")}`, 53 | }); 54 | // LSan writes its leak report to stderr and SIGABRTs; stdout holds the 55 | // fixture's own OK line either way, so assert exitCode/signal explicitly. 56 | expect({ stdout, stderr, signal, exitCode }).toEqual({ ... (truncated) release without fix: 2 skipped bun test v1.4.0-canary.1 (1498d7b) test/js/web/timers/timer-heap-race.test.ts: (pass) timer heap survives cross-thread Atomics.waitAsync timeout cancellation [3042.35ms] (skip) timer heap stays consistent while GC re-arms the RunLoop timer (skip) terminating a worker with pending Atomics.waitAsync tickets does not leak deferred-work tasks 1 pass 2 skip 0 fail 1 expect() calls Ran 3 tests across 1 file. [3.20s] __F:0:S:2 ``` </details> <details><summary>passes on PR (with fix)</summary> ```console ASAN with fix: all passed $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/web/timers/timer-heap-race.test.ts info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05) info: component rust-src is up to date info: checking for self-update (current version: 1.29.0) bun test v1.4.0 (4d9b926) test/js/web/timers/timer-heap-race.test.ts: (pass) timer heap survives cross-thread Atomics.waitAsync timeout cancellation [3805.27ms] (pass) timer heap stays consistent while GC re-arms the RunLoop timer [2481.63ms] (pass) terminating a worker with pending Atomics.waitAsync tickets does not leak deferred-work tasks [7403.00ms] 3 pass 0 fail 3 expect() calls Ran 3 tests across 1 file. [15.73s] __F:0:S:0 release with fix: 2 skipped $ bun scripts/build.ts --profile=release info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05) info: component rust-src is up to date info: checking for self-update (current version: 1.29.0) [configured] bun-profile → bun (stripped) target linux-x64-gnu build type Release build dir ./build/release revision 4d9b926 features (none) 22 deps, 106 codegen, 1168 objects in 891ms ninja: Entering directory `/workspace/bun/build/release' [1/1231] install /workspace/bun bun install v1.4.0-canary.1 (1498d7b) Checked 124 installs across 170 packages (no changes) [13.00ms] [2/1231] install /workspace/bun/packages/bun-error bun install v1.4.0-canary.1 (1498d7b) Checked 1 install across 2 packages (no changes) [3.00ms] [3/1231] install /workspace/bun/src/node-fallbacks bun install v1.4.0-canary.1 (1498d7b) Checked 129 installs across 147 packages (no changes) [11.00ms] [4/1231] gen ErrorCode+*.h [5/1231] fetch zlib [zlib] up to date [6/1231] gen bindgenv2 [7/1231] fetch picohttpparser [picohttpparser] up to date [8/1231] gen .bind ... (truncated) ``` </details> <details><summary>diff hotspot</summary> ``` src/jsc/VirtualMachine.rs | 8 +++ src/jsc/bindings/JSCTaskScheduler.cpp | 77 ++++++++++++++++++---- src/jsc/bindings/JSCTaskScheduler.h | 13 ++++ src/jsc/bindings/ZigGlobalObject.cpp | 2 + src/jsc/bindings/webcore/Worker.cpp | 5 ++ src/jsc/web_worker.rs | 8 +++ src/runtime/dispatch.rs | 15 +++++ .../timers/timer-heap-atomics-teardown-fixture.ts | 32 +++++++++ test/js/web/timers/timer-heap-race.test.ts | 25 ++++++- 9 files changed, 171 insertions(+), 14 deletions(-) ``` </details> **gate history** · 2 passed · 0 rejected · iteration 3 <details><summary>evidence per changed file</summary> ``` file reads edits tests src/jsc/VirtualMachine.rs 5 2 0 src/jsc/bindings/JSCTaskScheduler.cpp 7 10 0 src/jsc/bindings/JSCTaskScheduler.h 5 4 0 src/jsc/bindings/ZigGlobalObject.cpp 1 1 0 src/jsc/bindings/webcore/Worker.cpp 1 1 0 src/jsc/web_worker.rs 5 4 0 src/runtime/dispatch.rs 2 1 0 …st/js/web/timers/timer-heap-atomics-teardown-fixture.ts 0 2 0 test/js/web/timers/timer-heap-race.test.ts 2 3 0 ``` </details> <!-- robobun:evidence:end -->
…nating
Bun replaces JSC's DeferredWorkTimer::doWork with JSCTaskScheduler but omitted
the scriptExecutionStatus / hasPendingTerminationException guards that doWork
has before running each task. A worker.terminate() that lands after spin()'s
initial GC has scheduled a FinalizationRegistry cleanup task left the task in
the queue; an earlier task on the same tick handled the VM trap and threw the
TerminationException; the next tick ran the FR task with the exception pending
and Interpreter::executeCallImpl's entry scope.assertNoException() aborted the
whole process.
Gate runPendingWork on scriptExecutionStatus == Running and no pending
TerminationException, matching DeferredWorkTimer::doWork. The task is dropped
instead of entering JS. A dropped FinalizationRegistry cleanup is fine: node
runs no user JS after terminate(). Main-thread cleanups still fire (including
after a transient node:vm {timeout} termination, since scriptExecutionStatus
only reports Stopped for worker terminate or VM shutdown).
The onScheduleWorkSoon half of this (dropping tasks scheduled during teardown)
is already covered by m_isShuttingDown.
91d991a to
aab4381
Compare
|
Rebased onto current main. The CI (build 78046): the new
Ready for review. |
There was a problem hiding this comment.
No issues found in the rebased diff — the remaining change is the runPendingWork guard mirroring JSC's DeferredWorkTimer::doWork, plus the regression test. Deferring to a human because this sits in the worker-termination / JSC VM lifecycle path.
What was reviewed:
- The
scriptExecutionStatus+hasPendingTerminationExceptiongate matches JSC's own dispatch check; all Bun global-object method tables (Zig, NodeVM, Bake) populatescriptExecutionStatus, so the vtable call is never null. - Confirmed the
m_isShuttingDownmechanism now on main covers what the earlieronScheduleWorkSoon/m_isTerminatinghunk did, so dropping it in the rebase leaves no gap. - Test follows the file's conventions (unconditional stderr assert,
signalCode: null, debug/ASAN-gated) and the earlier CodeRabbit feedback is applied.
Extended reasoning...
Overview
After the rebase onto current main, the PR is now two files: a ~12-line addition to runPendingWork in src/jsc/bindings/JSCTaskScheduler.cpp that gates job->task(...) on scriptExecutionStatus == Running && !vm.hasPendingTerminationException(), and a new debug/ASAN-gated case in test/js/web/workers/worker-terminate-lifetime.test.ts. The earlier onScheduleWorkSoon / m_isTerminating half of the diff was dropped because the m_isShuttingDown mechanism that landed on main since (visible in the current file: onAddPendingWork, onScheduleWorkSoon, markShuttingDown, Bun__deleteDeferredWorkTask) already covers scheduling during teardown. I checked that the dropped hunk's intent is fully subsumed — onScheduleWorkSoon now holds m_lock across the m_isShuttingDown check and the enqueue, and both teardown paths flip it via markShuttingDown.
Security risks
None. This is a debug-assertion fix in the worker-termination path; the guard only ever drops work that would otherwise run against a terminating VM. No user-controlled input reaches the new code, and the failure mode being fixed is a debug/ASAN abort rather than a release-build behaviour change.
Level of scrutiny
Medium-high. The logic change is tiny and directly mirrors JSC's upstream DeferredWorkTimer::doWork (the code comment says so, and the PR description traces the exact assertion path), so the correctness argument is straightforward. But it lives in the JSC VM lifecycle / worker-termination path, which the review history of this PR itself shows is subtle (transient hasTerminationRequest vs. permanent teardown, cross-thread Atomics.notify, mirror-path divergence with Zig__GlobalObject__destructOnExit). Those earlier concerns are all resolved — either by the rebase making them moot or by the m_isShuttingDown machinery now on main — but the area warrants a maintainer's eyes.
Other factors
- I verified every method table in Bun (
ZigGlobalObject.cpp:957/985,NodeVM.cpp:906,BakeGlobalObject.cpp:220) setsscriptExecutionStatusnon-null, so the newglobalObjectMethodTable()->scriptExecutionStatus(...)call cannot be a null deref regardless of which global the ticket's owner belongs to. The PR description also notes main-threadvm.runInNewContext(..., {timeout})was tested and cleanups still fire, sincescriptExecutionStatusreportsRunningthere. - The test matches the file's established pattern: subprocess spawn with
bunEnv, concurrent stdout/stderr/exited drain, unconditionalstderrassertion first,signalCode: nullto catch SIGABRT, andtest.skipIf(!isASAN && !isDebug)since the assert is compiled out in release. - All three of my prior inline findings (empty src diff, transient
hasTerminationRequest,destructOnExitmirror) and CodeRabbit's stderr-gating comment are resolved; the rebase makes two of them structurally moot. No outstanding reviewer comments remain. - The bug hunting system found nothing on the current revision.
Debug/ASAN abort when
worker.terminate()lands while aFinalizationRegistryhas a pending cleanup in the worker:Repro
Aborts reliably on a debug build. Also reachable via
fs.promises.openin a worker (which registers aFileHandlewith aFinalizationRegistry), which is how #34260 tripped over it, and vianode:diagnostics_channelsubscribe/unsubscribe (any dc-based APM in a worker pool).Cause
JSC schedules
FinalizationRegistrycleanup (andAtomics.waitAsync, Wasm streaming compilation) throughDeferredWorkTimer. JSC's ownDeferredWorkTimer::doWorkchecksscriptExecutionStatusbefore each task and drops it onStopped; it also bails whenvm.hasPendingTerminationException(). Bun's replacement,JSCTaskScheduler::runPendingWork, does neither.Sequence:
setIntervalregistered,postMessagesent).terminate();notifyNeedTerminationsets the termination trap.spin()'s initialrun_gc()collects the registered target;JSFinalizationRegistry::finalizeUnconditionallyschedules aJSCDeferredWorkTask.CppTaskwhose post-runreturn_if_exceptionhandles the trap and throws theTerminationException;tick()returns with it pending. TheJSCDeferredWorkTaskis still in the queue.spin()'s loop callstick()again before itsrequested_terminatecheck. TheJSCDeferredWorkTaskruns,runFinalizationCleanupcallsJSC::call, andInterpreter::executeCallImpl's entryscope.assertNoException()fires.Stack:
Fix
runPendingWork: gate onscriptExecutionStatus == Running && !vm.hasPendingTerminationException(), matchingDeferredWorkTimer::doWork. The task is dropped instead of entering JS.A dropped
FinalizationRegistrycleanup is the expected behaviour: node runs no user JS afterterminate(). Main-thread cleanups still fire, including after a transientvm.runInNewContext(..., {timeout})termination, sincescriptExecutionStatusonly reportsStoppedfor worker terminate or VM shutdown.The
onScheduleWorkSoonhalf of this (dropping tasks scheduled duringteardownJSCVM'scollectNow) is already covered bym_isShuttingDown.Verification
New case in
test/js/web/workers/worker-terminate-lifetime.test.ts(debug/ASAN-gated; the assert compiles to a no-op in release):git stash push -- src/ && bun bd test ... -t FinalizationRegistry→SIGABRT,ASSERTION FAILED: !exception()git stash pop && bun bd test ... -t FinalizationRegistry→ passFinalizationRegistrystill fires on the main thread, including aftervm.runInNewContext(..., {timeout})times outtest/js/web/atomics.test.ts,test/regression/issue/atomics-waitasync-wtftimer-uaf.test.tspassAdjacent: #30857 adds exception reporting after the task runs in the same block; this change gates before it runs. Both are needed; whichever lands second rebases.