← All issues

[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

- JSGlobalObject* targetRealm = waiter->ticket(listLocker)->target()->realm();
+ JSGlobalObject* targetRealm = waiter->cachedRealm();

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.

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.

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.

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.

🔒

What can really happen when two VMs share a single SAB waiter list and one of them tears down its realm while the other GC-frees its ticket — the cross-VM lifetime and exploitation reasoning is unpacked in depth.

Subscribe to read more

🔒

Four reusable audit patterns identified, covering shared per-process registries, ticket-target dereferences, cached-identity-at-construction, and racy interior-pointer reads of vector backing stores.

Subscribe to read more