← All issues

[3] Use-after-free of StreamingCompiler::m_ticket across iframe teardown

Severity: High | Component: JavaScriptCore Wasm streaming compiler | acee67e

diff는 raw deferred-work ticket과 raw JSGlobalObject capture를 각각 liveness 검사가 포함된 weak reference로 전환했습니다. 이를 통해 iframe teardown 이후 해제된 realm 범위 상태에 대한 안정적인 역참조 취약점이 수정된 점에서 High로 평가됩니다. 더 강력한 memory-corruption primitive로의 확장은 공격자가 해제된 allocation을 안정적으로 재사용할 수 있어야 하는데, diff에서는 이를 확인할 수 없습니다.

이 패치는 Wasm streaming compiler가 컴파일 완료 전에 종료되는 iframe 내에서 호출될 때 발생하는 UAF를 수정합니다. streaming 컴파일의 완료 결과는 컴파일 시작 시 생성된 lambda에서 처리됩니다. 이 lambda는 globalObject에 대한 raw pointer를 직접 capture하며, streaming compiler를 통해 TicketData에 대한 raw pointer도 간접적으로 보유합니다. lambda가 이 두 객체보다 오래 살아남는 상황이 가능합니다. 컴파일 완료 전에 iframe이 제거되면, iframe의 globalObject가 수집되고 ticket은 취소 및 소멸됩니다. 컴파일이 완료되는 시점에 lambda는 dangling pointer를 역참조하게 됩니다.

Source/JavaScriptCore/wasm/WasmStreamingCompiler.h

- DeferredWorkTimer::Ticket m_ticket;
+ ThreadSafeWeakPtr<DeferredWorkTimer::TicketData> m_ticket;

Source/JavaScriptCore/wasm/WasmStreamingCompiler.cpp

void StreamingCompiler::didComplete()
{
auto result = makeValidationResult(*m_plan);
- auto ticket = std::exchange(m_ticket, nullptr);
+ auto ticket = takeTicketIfActive();
+ if (!ticket)
+ return;
...
+RefPtr<DeferredWorkTimer::TicketData> StreamingCompiler::takeTicketIfActive()
+{
+ auto ticket = m_ticket.get();
+ m_ticket = nullptr;
+ if (!ticket || ticket->isCancelled())
+ return nullptr;
+ return ticket;
+}
+
+JSGlobalObject* StreamingCompiler::globalObjectIfActive()
+{
+ auto ticket = m_ticket.get();
+ if (!ticket || ticket->isCancelled())
+ return nullptr;
+ return uncheckedDowncast<JSGlobalObject>(ticket->dependencies()[0]);
+}

Source/WebCore/bindings/js/JSDOMGlobalObject.cpp

- inputResponse->consumeBodyReceivedByChunk([globalObject, compiler = WTF::move(compiler)](auto&& result) mutable {
- VM& vm = globalObject->vm();
+ inputResponse->consumeBodyReceivedByChunk([vmPtr = &vm, compiler = WTF::move(compiler)](auto&& result) mutable {
+ auto& vm = *vmPtr;
JSLockHolder lock(vm);
+ auto* globalObject = compiler->globalObjectIfActive();
+ if (!globalObject)
+ return;

m_ticket은 raw DeferredWorkTimer::Ticket에서 ThreadSafeWeakPtr<TicketData>로 변경되었습니다. 새로 추가된 takeTicketIfActive()는 weak pointer를 RefPtr로 승격하며, ticket이 소멸되거나 취소된 경우 null을 반환하고 m_ticket을 비웁니다. didComplete(), fail(), cancel()의 호출 지점은 이제 std::exchange(m_ticket, nullptr)를 무조건 실행하는 대신, null 결과를 받으면 조기 반환합니다. 추가된 helper인 globalObjectIfActive()는 ticket의 dependency 목록에서 살아있는 JSGlobalObject*를 조회하도록 설계되었습니다. JSDOMGlobalObject.cpp에서는 consumeBodyReceivedByChunk lambda가 raw globalObject를 직접 capture하는 방식을 중단합니다. 대신 VM*과 compiler만 capture하고, compiler->globalObjectIfActive()를 통해 global object를 다시 조회하도록 변경되었습니다.

iframe teardown 경계를 넘어 소유자보다 오래 살아남은 비동기 소유 객체의 raw pointer를 liveness 재검증 없이 역참조하는 패턴.

WebAssembly.compileStreaming / instantiateStreamingfetch Response를 받아 body가 스트리밍되는 동안 모듈을 점진적으로 컴파일합니다. DeferredWorkTimer는 지연된 결과를 JS 스레드로 다시 전달하는 JSC의 메커니즘입니다. addPendingWork는 대상 promise와 그 dependency 객체(요청한 JSGlobalObject 포함)를 살아있게 유지하는 TicketData(typedef명 Ticket)를 반환합니다. ThreadSafeWeakPtr<T>::get()은 객체가 아직 살아있으면 원자적으로 RefPtr<T>로 승격하고, 그렇지 않으면 null을 반환합니다. iframe은 자체 JSGlobalObject를 가진 독립 realm을 구성하며, DOM에서 iframe을 제거하면 해당 global object가 수집 대상이 됩니다. streaming 컴파일은 background worklist에서 실행되므로, 완료 callback은 원래 realm이 소멸된 이후에도 실행될 수 있습니다.

비동기 경계를 넘어 소유자가 소멸된 뒤에도 살아남은 dangling raw pointer로 인한 use-after-free입니다. TicketDataJSGlobalObject의 수명은 모두 요청한 iframe/realm에 묶여 있습니다. 이 상태에서 컴파일 완료 전에 iframe이 제거되면, iframe의 JSGlobalObject는 GC 대상이 되고 deferred-work ticket은 취소 및 소멸됩니다.

🔒

The lifetime and ownership story behind this dangling-pointer bug is traced across the iframe-teardown boundary, with an assessment of how far the crash could be pushed.

더 확인하려면 구독해 주세요

🔒

Multiple reusable audit patterns identified for finding sibling async-lifetime bugs, with concrete deferred-work and binding-callback starting points.

더 확인하려면 구독해 주세요

🔒

The lifetime and ownership story behind this dangling-pointer bug is traced across the iframe-teardown boundary, with an assessment of how far the crash could be pushed.

더 확인하려면 구독해 주세요

🔒

Multiple reusable audit patterns identified for finding sibling async-lifetime bugs, with concrete deferred-work and binding-callback starting points.

더 확인하려면 구독해 주세요