[13] DeferredWorkTimer leaves stale ticket pointer in m_tasks after cancellation
Severity: Medium | Component: JavaScriptCore DeferredWorkTimer | e7c6375
Rated Medium because the diff fixes a stale-pointer dequeue from m_tasks where the first pop frees the TicketData and the duplicate entry dereferences the freed (or TZone-reused) pointer; reaching the window requires cross-realm teardown with sufficient in-flight tickets, and the strength of the primitive is contingent on TZone reuse landing in the freed slot.
cancelPendingWorkSafe() was unconditionally appending a (ticket, noop) entry to m_tasks for every weak ticket of a dying global. This is unnecessary because doWork() already has a removeIf(isCancelled) pass that purges cancelled tickets from m_pendingTickets, and setTimeUntilFire(0_s) already ensures doWork() fires to run that cleanup.
Source/JavaScriptCore/runtime/DeferredWorkTimer.cpp
void DeferredWorkTimer::cancelPendingWorkSafe(JSGlobalObject* globalObject)
{
for (Ref<TicketData> ticket : *globalObject->m_weakTickets) {
if (!ticket->isCancelled())
cancelPendingWork(ticket.ptr());
- m_tasks.append(std::make_tuple(ticket.ptr(), [](DeferredWorkTimer::Ticket) { }));
}
if (!isScheduled() && !m_currentlyRunningTask)
setTimeUntilFire(0_s);
Patch Details
A single line is removed: the unconditional append of a (ticket, noop) tuple to m_tasks for every weak ticket belonging to a dying JSGlobalObject. The remaining logic still iterates the global's m_weakTickets, calls cancelPendingWork on each non-cancelled ticket, and schedules doWork() via setTimeUntilFire(0_s). Cleanup of cancelled tickets is left to the trailing m_pendingTickets.removeIf(isCancelled) pass already at the end of doWork(). A regression test creates many FinalizationRegistry instances in a child global, drops the global, GCs, and then schedules large bursts of timer-driven deferred work.
Stale raw-pointer reuse in a work queue: an owner-side cleanup pushed duplicate raw-pointer entries that outlive the owning Ref drop done by the consumer.
Background
DeferredWorkTimer queues two structures: m_pendingTickets (a HashSet<Ref<TicketData>> holding the keep-alive reference for each ticket) and m_tasks (a Deque<tuple<Ticket, Task>> where Ticket = TicketData* is a raw pointer). Tickets are added when JS asks for deferred work (Atomics.waitAsync, FinalizationRegistry cleanup, WebAssembly promise integration); doWork() pops m_tasks FIFO under the VM API lock. TicketData::cancel() flips m_isCancelled but does not remove the ticket from either container; cleanup happens lazily inside doWork(). cancelPendingWorkSafe(JSGlobalObject*) runs from JSGlobalObject::~JSGlobalObject to clean up any tickets whose realm is dying. TicketData is allocated under WTF_MAKE_TZONE_ALLOCATED_IMPL, which means freed instances are returned to a type-segregated freelist and the next TicketData::create may reuse the exact same address. HashSet<Ref<T>>::find(rawPtr) uses pointer-value hashing/equality and does not dereference the input pointer to decide membership.
Analysis
cancelPendingWorkSafe appended a (ticket, noop) entry to m_tasks for every weak ticket of the dying global, in addition to whatever entries were already queued for those tickets. m_tasks stores Ticket as a raw TicketData*; the keep-alive Ref lives in m_pendingTickets. The mainline doWork() loop, when it sees a cancelled ticket on the first matching m_tasks entry, calls m_pendingTickets.remove(pendingTicket), which drops the last Ref and destroys the TicketData. Because the same ticket pointer was queued a second time by cancelPendingWorkSafe, the next loop iteration pops (ticket, noop) whose ticket is now a dangling TicketData*.
Aaaaa Aaaa Aaaaa a Aaaa Aa Aaaaaaa Aaaaaaa Aaaaaaaaaaaa Aa Aaaaaaaaaaaaaaa Aaa Aaa Aaaa Aaaaaaa Aaaaaaaa Aa Aaaaaaaaaaaaaaaaaaaaa Aaaaaaa Aa Aaaaaaa Aaaaa a Aaa Aaaaaa Aaaa a Aaaaaaaaaaa Aaaa Aaaaaaaaaaaa Aaaaaaaaaaa Aa Aaa Aaaa Aaaaaaaa Aaaaaaaaa Aaaaaaaaa Aaaaa Aaaaaaaaaaaaaaa Aaaa Aaa Aaaaa Aaaaaaa Aaa Aaaaa Aaaaaaaaa Aaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aa a Aaaaaaaaaa Aaaaaaa Aaaaaaaa Aaa Aaaaaaaaaa Aaa Aaa Aaaaaaaa Aaa Aaaa Aaaaaaa Aa Aa Aa Aaaa Aaaa Aaaaaaaa Aaaa Aaaaa Aaa Aaaaaaa Aaaaaa Aaaaaa a Aaaaa Aaaaa Aaaaaaa Aaaa Aaaaaaaaa Aaaaaaaaaaaaa Aaaaaaaa Aaaaa Aa Aaaaa Aaa Aaaaaa Aa Aaaaaaa Aaaa a Aaaa Aaaaaaaaa Aaaaaaaaaaa Aaaaa Aa Aaaaaaaaaa Aaaa Aaaa Aaaa Aaa Aaaaa Aaaaaa Aa Aaaaaaaaaaaaaaaaaaaaaaa Aaaa Aaa Aaaaaaa a Aaaaaa Aaaaaaaaa Aaaaaa Aaaaaa Aaa Aaaaaa Aaaaaaa Aaaaaaaaaaaaaaaaaaaaa Aaaaaa Aaaaaaaaaaaaaaaaaaa a Aaaa Aaaaaaaaaaaaaa Aaaaaaaa Aaaaaaa Aa Aaaaaaa Aaaa Aaaaaaaa Aaaaaa
Aaaa Aaaaaaaaaaaaa Aaaaaaaa Aaaaaa Aaaaaa Aaaaaa Aaa Aaa Aaaaaaaa Aaa Aaaaaaa Aaaa Aaaaaaaa Aaa Aaaaaaaaa Aaaa Aaa Aaaaaaaaaaaaa Aaaaaa Aaaa Aaaaaaaaa Aa Aaaaaa Aaaaa Aaaaaaaaaa Aa Aaaaaaaaaaaaaaaaaa Aa Aaaaaaaaaa Aaaaaaa Aaaa Aaa Aaaa Aaaaaa Aaaaaaa Aa Aaaaaa Aaaaa Aaa Aaa Aaaaa Aaa Aaaaa Aaa Aaa Aaaaaa Aaa Aaaaaaa a Aaaaaaaaaaaaaaaa Aaaaaaaaaaaa Aaaaa Aaaaa Aaaaa Aaaa Aaaaaaaaa Aa a Aaaaaaaaaaaa Aaaaaaaaaa Aaaaaaaaa Aa Aaaaa a Aaaaaaaaaa Aaaaaa Aaaaa Aaa Aaaaaaaaaa Aaa Aaa a Aaaaa Aaaa Aa Aaaaaaaa Aa Aaaaa Aa Aaaa Aaaaaaaa Aaaa Aaaaa Aaa Aaaaaaaaaaaaa Aaaaaaaaaaaaaaaa Aaa a Aaaaaaaaa Aaaaaaa Aaaa Aaaaaaaaaa a Aaaaaaa Aaa Aaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaa Aaaaaaa Aaaaaaaaa
🔒The lifetime and reuse implications of a single removed `m_tasks.append` are explored in depth, including what the queue actually holds and what happens when the same pointer is enqueued twice.
Subscribe to read more
Audit directions
a Aaaaaaaaaaaaa Aaaa Aaaaaa Aaaaaa Aaaa a Aaaaaaaa Aaaaaaaaaaaaaaaa Aaaaaaaaaaaa Aaaaa Aaaaa Aaa Aaaaaaaaaa Aaaaa a Aaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaa Aaaaa Aa Aaa Aaaa Aaaaaaa Aaa a Aaaaaaaaaaaaaaaaa Aa Aaa Aaaaaaaaaaa Aaaaa Aaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaa Aaaaaaaa Aaaaaaa Aaa Aaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaa Aaaaaa Aa Aaaaaaaa Aaaa Aaa Aaaaaaa Aaa Aaa Aaaaaaa Aaaa Aaaa Aaaa Aaa Aaa Aaaaaaaaa
a Aaaaaaaaaaaaaaaaaaa Aaaaaaa Aaaaaaa Aa a Aaaaaa Aaaaaaaa Aaaaaaaa Aaaa Aaa Aaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaaaaaaaaaa Aaaaa Aaaaaa Aaa Aaaaaaa Aaaaa Aaaa Aaaaaa Aa Aaaaaa Aaaaaaa Aaaaaaaaaa Aaaaaaaaaaaaaa Aaaaaaa Aa Aaaaaaaa Aaaa Aaa Aaaaaa Aaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaa Aaaaaaa Aa Aaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaaaaaa Aaaaaaaaaaaaa
a Aaaaaaaaaaaaaaaaaaa Aaaaaaaaa Aaaa Aaaa Aaaaaaa Aaa Aaa Aaaaaaa Aaaaaaaaaaa Aaaaa Aaa Aaaaaaaaa Aaaaaaa Aaaaaaaaaaaaaa Aaaaaaaaaaaaa Aa Aaaaaaa Aaaaaaaaaaaaa Aaa Aaaaaaaa Aa Aaaaa Aaaaa Aaaaa Aaaaaa Aaa Aaaaaa Aaa Aa a Aaaaa Aaaaaaa Aaa Aaaaaaaa Aaa a Aaaaaaaaa Aaaa Aaaaaaaaa Aaaaaaaa Aaaaaaa Aaaa Aaaaaaa Aaaaaa Aa Aaaaa Aa a Aaaaaaaaaa Aaaaaaa Aa a Aaaaaaaaaa
a Aaaaaaaaaaaaa Aaaaaaaa Aaaaaaaa Aaaaaa Aaaaaaa Aaaaaaaaa Aaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaa Aaaa Aaaa Aaaaaaa Aaaaaaa Aaaaaaa Aaaa Aaaaaaa Aa Aaaaaaaaaa Aaaaa Aaaaaaaaaaaa Aaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaa Aaaaa Aaaaaa Aaaaa Aaaaa Aa Aaa Aaaa Aaa
🔒Multiple reusable audit patterns identified across JSC runtime queues, with concrete starting points for variant discovery in deferred-work, microtask, and waiter-list bookkeeping.
Subscribe to read more