[16] [JSC] Fix data race in WaiterListManager::unregister
Severity: Medium | Component: JSC WaiterListManager | 21ab50e
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.