Skip to content

Atomics.wait: wake for a termination requested from another thread - #392

Open
dylan-conway wants to merge 1 commit into
mainfrom
dylan/atomics-wait-termination
Open

Atomics.wait: wake for a termination requested from another thread#392
dylan-conway wants to merge 1 commit into
mainfrom
dylan/atomics-wait-termination

Conversation

@dylan-conway

@dylan-conway dylan-conway commented Aug 7, 2026

Copy link
Copy Markdown
Member

VM::notifyNeedTermination() called from another thread (a dedicated worker's terminate()) fires the NeedTermination trap, and VMTraps::requestThreadStopIfNeeded() / the SignalSender notify the target VM's syncWaiter() condition so a thread blocked in Atomics.wait wakes up. But WaiterListManager::waitSync() only re-checks vm.hasTerminationRequest() — and m_hasTerminationRequest is set by VMTraps::handleTraps() on the blocked thread itself, which cannot run while that thread is inside waitUntil(). So the waiter wakes, sees no request, and goes back to sleep: a worker parked in Atomics.wait(…) with no timeout can never be terminated (its thread never exits; whoever joins it hangs). Upstream main has the same code: https://github.com/WebKit/WebKit/blob/9ef04dabf52d7432a3e7ab13f44f9d8b4bd933a0/Source/JavaScriptCore/runtime/WaiterListManager.cpp#L95-L105 (loop) and https://github.com/WebKit/WebKit/blob/9ef04dabf52d7432a3e7ab13f44f9d8b4bd933a0/Source/JavaScriptCore/runtime/VMTraps.cpp#L419-L420 (the notify).

Change:

  • waitSync() treats a pending NeedTermination trap bit (atomic, safe to read here) as a termination as well as hasTerminationRequest().
  • atomicsWaitImpl's Terminated case handles that trap when the request has not been recorded yet — handleTraps(NeedTermination) sets the request and throws the TerminationException — instead of calling throwTerminationException() directly, which asserts hasTerminationRequest().

waitAsync is unaffected. Observed in Bun as oven-sh/bun#32802 (worker.terminate() never completes for a worker in Atomics.wait); Node/V8 interrupt the wait in the same situation.

VM::notifyNeedTermination() from another thread fires the NeedTermination
trap and VMTraps notifies the blocked VM's syncWaiter condition, but
waitSync()'s loop only re-checked vm.hasTerminationRequest(), which is set
by trap *handling* on the blocked thread itself — impossible while it sits
in waitUntil(). The waiter woke, saw no request, and slept again, so a
worker parked in Atomics.wait could never be terminated.

waitSync() now also treats a pending NeedTermination trap bit as a
termination, and the Atomics.wait host function handles that trap (which
records the request and throws the TerminationException) when the request
has not been recorded yet.
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8ac8080c-7155-4b5b-8379-a20483630b9d

📥 Commits

Reviewing files that changed from the base of the PR and between 171babe and 083c6f8.

📒 Files selected for processing (2)
  • Source/JavaScriptCore/runtime/AtomicsObject.cpp
  • Source/JavaScriptCore/runtime/WaiterListManager.cpp

Walkthrough

Synchronous atomic waits now detect both recorded termination requests and pending NeedTermination traps. The wait result reports termination for either condition, and atomicsWaitImpl processes the applicable termination path.

Changes

Termination handling

Layer / File(s) Summary
Synchronous wait termination flow
Source/JavaScriptCore/runtime/WaiterListManager.cpp, Source/JavaScriptCore/runtime/AtomicsObject.cpp
waitSyncImpl stops waiting when a termination request or NeedTermination trap is present. The result reports Terminated for either condition. atomicsWaitImpl throws an existing termination exception or handles the pending trap.
🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the bug and fix, but it omits the required Bugzilla link, review line, and changed-file/function list. Add the bug title and Bugzilla URL, the review line, and the required formatted explanation and changed paths/functions.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the Atomics.wait termination fix caused by a request from another thread.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Comment @coderabbitai help to get the list of available commands.

didGetDequeued = list->findAndRemove(listLocker, syncWaiter);
ASSERT(didGetDequeued);
return vm.hasTerminationRequest() ? WaitSyncResult::Terminated : WaitSyncResult::TimedOut;
return terminationRequested() ? WaitSyncResult::Terminated : WaitSyncResult::TimedOut;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 This widens WaitSyncResult::Terminated to cover the "only a NeedTermination trap bit is set" state, but the second caller of waitSync() — Wasm's waitImpl() in WasmOperationsInlines.h:750 — was not updated and still calls vm.throwTerminationException() directly, which ASSERT(hasTerminationRequest())s. Terminating a worker blocked in memory.atomic.wait32/64 will now hit that debug assertion (and in release, throw with m_hasTerminationRequest unset and the trap bit still pending). The Wasm caller needs the same hasTerminationRequest() / handleTraps(NeedTermination) branch you added in AtomicsObject.cpp, or waitSyncImpl could handle the trap itself before returning Terminated.

Extended reasoning...

What changed and why it breaks the Wasm path

Before this PR, waitSyncImpl() only returned WaitSyncResult::Terminated when vm.hasTerminationRequest() was already true. Both callers therefore safely responded with a bare vm.throwTerminationException().

This PR broadens the condition: terminationRequested() now also fires when vm.traps().needHandling(VMTraps::NeedTermination) is true — i.e. when another thread has set the trap bit via VM::notifyNeedTermination() but this thread has not yet run VMTraps::handleTraps() to record m_hasTerminationRequest. The PR description explicitly calls out that throwTerminationException() "asserts hasTerminationRequest()", and correctly updates the JS Atomics.wait caller in AtomicsObject.cpp:472-478 to branch: if hasTerminationRequest() is already set, throw directly; otherwise call vm.traps().handleTraps(VMTraps::NeedTermination), which sets the request and throws.

However, WaiterListManager::waitSync() has exactly one other caller: Wasm's waitImpl() in Source/JavaScriptCore/wasm/WasmOperationsInlines.h:738-755, reached from memory.atomic.wait32/wait64 in every Wasm tier (memoryAtomicWait32/64, operationMemoryAtomicWait32/64, ipint_extern_memory_atomic_wait32/64). That caller was not touched:

case WaiterListManager::WaitSyncResult::Terminated:
    vm.throwTerminationException();
    return -1;

And VM::throwTerminationException() (VM.cpp:1102) begins with ASSERT(hasTerminationRequest()); VM::setException() (VM.cpp:1093) asserts the same invariant.

Step-by-step proof

  1. A worker executes Wasm memory.atomic.wait32 on shared memory with no timeout. Control reaches waitImpl()WaiterListManager::waitSync()waitSyncImpl(), which parks on syncWaiter->condition().waitUntil(...).
  2. Another thread calls worker.terminate()VM::notifyNeedTermination(). This fires the NeedTermination trap bit and (via VMTraps::requestThreadStopIfNeeded / SignalSender) notifies the sync waiter's condition. Crucially it does not set m_hasTerminationRequest — that only happens in VMTraps::handleTraps() on the target thread.
  3. waitSyncImpl wakes and re-evaluates the loop guard. terminationRequested() returns true because vm.traps().needHandling(VMTraps::NeedTermination) is true, even though vm.hasTerminationRequest() is still false. The loop exits, the waiter is dequeued, and the function returns WaitSyncResult::Terminated.
  4. Back in Wasm waitImpl(), the Terminated case calls vm.throwTerminationException().
  5. VM::throwTerminationException() executes ASSERT(hasTerminationRequest())debug-build assertion failure.

In release builds the asserts compile out and the TerminationException is thrown, but m_hasTerminationRequest is never set and the NeedTermination trap bit is never cleared — violating the VM invariant that hasTerminationRequest() is set whenever a TerminationException is pending, and leaving a stale trap bit to be re-handled at the next trap check.

Why nothing else prevents it

The only guard is at the call sites. The PR added the guard to one of the two call sites and left the other unchanged. There is no fallback in throwTerminationException() that promotes the trap bit to a request; it simply asserts.

Fix

Apply the same treatment to WasmOperationsInlines.h:749-751:

case WaiterListManager::WaitSyncResult::Terminated:
    if (vm.hasTerminationRequest())
        vm.throwTerminationException();
    else
        vm.traps().handleTraps(VMTraps::NeedTermination);
    return -1;

Alternatively, centralize this: have waitSyncImpl call vm.traps().handleTraps(VMTraps::NeedTermination) (or otherwise record the request) before returning Terminated, so both callers can keep the bare throwTerminationException() and future callers can't make the same mistake.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Preview Builds

Commit Release Date
083c6f88 autobuild-preview-pr-392-083c6f88 2026-08-07 20:51:07 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant