[16] [JSC] Fix data race in WaiterListManager::unregister
Rated Medium because the diff caches the realm pointer in the Waiter at construction time so the cancellation decision does not read across VM boundaries; pre-fix, WaiterListManager::unregister on one VM's sweep thread read m_dependencies mid-modification by another VM's GC End phase, yielding a torn/freed-pointer dereference under specific scheduling.
Waiter caches the realm pointer at construction (before publication to any list). WaiterListManager::unregister consults the cached pointer instead of ticket->target()->realm().
Source/JavaScriptCore/runtime/WaiterListManager.cpp
Cross-VM read-from-mutating storage: one VM's sweep thread iterated waiters belonging to a foreign VM and dereferenced m_dependencies while the foreign VM's GC End phase mutated it.
Patch Details
Waiter gains a JSGlobalObject* member set at construction. unregister reads only this cached pointer when deciding whether to cancel. The list lock continues to serialize structural changes to the WaiterList.
Background
WaiterListManager is the per-process registry that backs Atomics.wait / Atomics.waitAsync on SharedArrayBuffers. Because SABs span VMs (main window, iframes, dedicated workers), a single WaiterList (keyed on the SAB pointer) can hold Waiters from multiple VMs. DeferredWorkTimer::TicketData::m_dependencies (a FixedVector<JSCell*>) is the GC-managed pin for the promise/target across the wait.
Analysis
Pre-fix, WaiterListManager::unregister ran on the unregistering VM's sweep thread and iterated every waiter on the shared list. For each it called waiter->ticket(listLocker)->target() — implemented as bit_cast<JSObject*>(m_dependencies.last()) — to compare the target's realm to the unregistering global object.
The owning VM could simultaneously be in its GC End phase running TicketData::cancelAndClear(), which mutates / frees m_dependencies. The list lock and m_taskLock belong to different VMs, so neither serializes the two threads. The read could observe a torn or freed backing store, returning a stale JSObject*, on which the code then called ->realm() — a second UAF dereference — before comparing to globalObject.
VM A (sweep) VM B (GC End)
───────────── ──────────────
list lock m_taskLock
read m_dependencies.last() ←────── cancelAndClear() mutates m_dependencies
deref → realm() ←────── freed memory
The fix sidesteps the cross-VM read entirely: the realm is cached in the Waiter at construction, so readers acquiring the list lock always see the completed write.
This weakens the GC/scheduling invariant across VMs in the SAB waiter registry. The primitive is a race-driven UAF read across VM boundaries.
Audit directions
- Any cross-VM data accessed under one VM's lock alone. Grep
WaiterListManager,DeferredWorkTimer, andJSAtomicsObjectfor fields read on one VM that another VM can mutate. TicketData::m_dependenciesconsumers for similar foreign-VM read patterns.- GC End phase mutators for shared registries — confirm they all hold every relevant lock or use copy-on-write.