[13] DeferredWorkTimer leaves stale ticket pointer in m_tasks after cancellation
Severity: Medium | Component: JavaScriptCore DeferredWorkTimer | e7c6375
Medium으로 평가된 이유는 다음과 같습니다. 이 diff는 m_tasks에서의 stale pointer dequeue를 수정합니다. 첫 번째 pop이 TicketData를 해제하면 중복 항목이 해제된(또는 TZone에서 재사용된) pointer를 역참조합니다. 이 window에 도달하려면 충분한 수의 in-flight ticket이 있는 상태에서 cross-realm teardown이 필요합니다. primitive의 강도는 TZone 재사용이 해제된 slot에서 발생하는지 여부에 달려 있습니다.
cancelPendingWorkSafe()는 소멸 중인 global의 모든 weak ticket에 대해 무조건 (ticket, noop) 항목을 m_tasks에 추가하고 있었습니다. 그러나 이는 불필요한 동작이었습니다. doWork()에는 이미 removeIf(isCancelled) 처리 단계가 있어 취소된 ticket을 m_pendingTickets에서 제거하고, setTimeUntilFire(0_s) 호출이 doWork() 실행을 이미 보장하기 때문입니다.
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
제거된 것은 단 한 줄입니다. 소멸 중인 JSGlobalObject에 속한 모든 weak ticket에 대해 (ticket, noop) tuple을 m_tasks에 무조건 추가하던 코드입니다. 나머지 로직은 그대로 유지됩니다. global의 m_weakTickets를 순회하면서 취소되지 않은 각 ticket에 대해 cancelPendingWork를 호출하고, setTimeUntilFire(0_s)를 통해 doWork() 실행을 예약합니다. 취소된 ticket의 정리는 doWork() 마지막 단계에 이미 존재하는 m_pendingTickets.removeIf(isCancelled) 처리에 맡겨집니다. regression test는 child global 안에서 FinalizationRegistry 인스턴스를 다수 생성한 뒤, global을 해제하고 GC를 수행한 다음, timer 기반 deferred work를 대량으로 예약하는 방식으로 구성되었습니다.
Work queue 내 stale raw pointer 재사용: 소유자 측 cleanup이 중복 raw pointer 항목을 추가했고, 이 항목들이 consumer 측의 소유 Ref 해제 이후에도 남아있었습니다.
Background
DeferredWorkTimer는 두 개의 자료구조를 관리합니다. 첫 번째인 m_pendingTickets는 각 ticket의 keep-alive reference를 보유하는 HashSet<Ref<TicketData>>이고, 두 번째인 m_tasks는 Ticket = TicketData*가 raw pointer인 Deque<tuple<Ticket, Task>>입니다. JS가 deferred work를 요청할 때(Atomics.waitAsync, FinalizationRegistry cleanup, WebAssembly promise 연동) ticket이 추가되고, doWork()는 VM API lock 하에서 m_tasks를 FIFO 방식으로 pop합니다. TicketData::cancel()은 m_isCancelled 플래그를 설정하지만, 어느 컨테이너에서도 ticket을 제거하지 않습니다. 정리는 doWork() 내부에서 지연 처리됩니다. cancelPendingWorkSafe(JSGlobalObject*)는 JSGlobalObject::~JSGlobalObject에서 호출되어 realm이 소멸 중인 ticket들을 정리합니다. TicketData는 WTF_MAKE_TZONE_ALLOCATED_IMPL로 할당됩니다. 해제된 인스턴스는 type별로 분리된 freelist로 반환되며, 다음 TicketData::create 호출이 동일한 주소를 재사용할 수 있습니다. HashSet<Ref<T>>::find(rawPtr)는 pointer 값 기반의 해싱/동등 비교를 사용하며, 멤버십 결정 시 입력 pointer를 역참조하지 않습니다.
Analysis
cancelPendingWorkSafe는 소멸 중인 global의 모든 weak ticket에 대해, 이미 대기 중인 항목과 별개로 (ticket, noop) 항목을 m_tasks에 추가했습니다. m_tasks에서 Ticket은 raw TicketData*로 저장되고, keep-alive Ref는 m_pendingTickets에 있습니다. doWork()의 메인 루프는 첫 번째 m_tasks 항목에서 취소된 ticket을 발견하면 m_pendingTickets.remove(pendingTicket)을 호출합니다. 이 호출이 마지막 Ref를 해제하고 TicketData를 소멸시킵니다. cancelPendingWorkSafe가 동일한 ticket pointer를 두 번째로 큐에 추가했기 때문에, 다음 루프 반복에서 pop되는 (ticket, noop)의 ticket은 이미 dangling TicketData* 상태가 됩니다.
Aaaa Aaaa Aaaaa Aaa Aaa Aaaa Aaaa Aaaa Aaaaaaaaaaaaa Aaaa Aaaa Aaaaaaaaaaaaa Aaaaa Aa Aaaa Aaaaa Aa Aa Aaaaaaaaaaaaaaaaaaaa Aaaaaaa Aaaa Aaaa a Aaaa Aa Aaaaaa a Aa Aaaaaaaaa Aaaa Aaa Aaa Aaaaaaaaaaaaaaaa Aaaaa Aa Aaa Aaa Aa Aaaa Aaaaaaa Aa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaa Aaaaa Aa Aa Aa Aaaaaaa Aaaaaaaaaa Aaaa Aaaaa Aaaa Aaaaaaaa a Aaaaaaa Aa Aaa Aaa Aaaaaa
Aaaaaaa Aaa Aaa Aaaaa Aa Aaaaaaaaa Aaaaaaaaaaaaa Aaaaaaa Aa Aaa Aaaaa Aaaaaa Aaaaaa Aaa Aa Aaa Aaaaaaa Aa Aa Aaaaaaaaa Aaaaaaaaaaa Aaa Aaaaaaaaaa Aa Aaaa Aaa Aaaaa a Aa Aaaaa Aaaaaaa Aaaa Aaaaaaaaaaaaaaaaaaaaaaaa Aaaa a Aa Aaaaaaaaa Aaaaaa Aaa Aaaaaa Aaaaa Aaaaaaaaaaaaaaaaaaaaaaa Aa Aaa Aaaaaaaaaaaaaaaaaaa a Aaa Aaaaaaaaaaaaa Aaaa Aaaaaa Aaaa Aaa a Aaaaaaaa Aaa Aaaaaa
a Aaaaaaaaaaaaaa Aaa Aaa Aaa Aaa Aaaa Aaaaaaaa Aa Aa Aaa Aaaaaaaaaaa Aaaa Aaaaaaaaaaaaaa Aa Aaaaaaaaaaaaaaaaaaa Aaaa Aaaa Aaaa Aa Aaaaa Aa Aaa Aaaaaa Aaa Aaaaaa Aaaaaaaa a a Aa Aaa Aaaa a Aa Aaaa Aaa Aaaaaa a Aa Aaaa Aaaaaaaa Aaaaaaa Aaaa Aaaa Aaaaa Aaa Aaaaa Aaa Aaa Aaa Aa Aaaa Aaaa Aaaaaa Aa Aaaa Aaaaaaa Aaaaaaaaaa Aaaa Aaa a Aaaaaaa Aa Aa Aa Aaaaa Aaaaa Aaaaaa Aaaaa Aaaaaaaaaaaaaaaaa Aa Aaaaaaaaaaaaaaaaaaaaaaaa Aaaa Aaaaaaaa Aaaa Aaaa Aa Aaa Aaaaaaa
🔒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.
더 확인하려면 구독해 주세요
Audit directions
a Aaaaa Aaaaaaaa Aa Aaaaa Aa Aaa Aaa Aaaaaaa Aaaa Aaaaaaaa Aaaaaaaaaaaaaaaaaaaa Aaaaaaa Aaa Aa Aa Aaaaaaaa Aaaa Aaaaaaaaaaaaaaaaaa Aaaaaaaaaa Aaa Aa Aa Aaa Aaaaaa Aaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaa Aaaaaaaa Aa Aaaaaaaaaaaaaaaaaaaaaaa Aa Aaaa Aaaaa Aa Aaaaaaaa Aaa Aaa Aa Aa Aaa Aaaaaaaa a a Aa Aa Aaa a Aaa Aaaaaa
a Aaaa Aa Aaaaaa Aaaa Aa Aa Aaaaaaaaaa Aaa Aaa Aa Aaaa Aaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaa Aaaa Aaaaaa a Aa Aa Aa Aaa Aaaaaa Aaaa Aaaa Aaaaaaaaaaaaaa Aaa Aaaa Aaa Aaaaaa Aaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaa Aaaaaaaaaaaa Aa Aaaaaaaaaaaaaaaaaaaaaaa Aa Aaaaaaaa Aaa Aaaaaa
a Aaaaa Aaaaaaa Aaaaaaaaa Aa Aaaa Aaaaaaaaaaaaaaaaa Aaaaaaaaa Aaaaaa Aaaaaaaaaaaaaa Aaaaaaaaaaaa Aa Aa Aaa Aaaaa Aa Aaa Aaaaaaaa Aaaa Aaaa Aaaaa Aaaaaa Aaaaa Aaa Aaaaa Aaa Aaaaaaaa a Aa Aaa Aa Aaaa Aaaaa Aaa Aaa a Aaaaa Aaa Aaa Aaaaaaaaaa Aaaaaaaa Aaaaaaaaaa Aaaa Aaa Aaaaaa
a Aaaa Aaa Aa Aaaa Aaaaaaaaaaa Aaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaa Aa Aa Aa Aaaa Aa Aa Aaa Aaaaaa Aaa Aa Aaa Aa Aaaa Aa Aaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaa Aaaaa a Aaaaaaaa Aaaa Aaaaa Aaaaaa
🔒Multiple reusable audit patterns identified across JSC runtime queues, with concrete starting points for variant discovery in deferred-work, microtask, and waiter-list bookkeeping.
더 확인하려면 구독해 주세요