[3] Use-after-free of StreamingCompiler::m_ticket across iframe teardown
Severity: High | Component: JavaScriptCore Wasm streaming compiler | acee67e
Rated High because the diff converts a raw deferred-work ticket and a raw JSGlobalObject capture into liveness-checked weak references, fixing a reliable dereference of freed realm-scoped state after iframe teardown; escalation to a stronger memory-corruption primitive would require an attacker to reliably reclaim the freed allocations, which the diff does not establish.
This patch fixes a UAF when the Wasm streaming compiler is invoked in an iframe which is disposed of before compilation finishes. The result of streaming compilation is expected by a lambda created when compilation starts. The lambda captures a raw pointer to the globalObject and (transitively via the streaming compiler) a raw pointer to the TicketData. It is possible for the lambda to outlive those two objects: if the iframe is removed before compilation finishes, the iframe's globalObject is collected and the ticket is cancelled and destroyed. When compilation finishes, the lambda dereferences dangling pointers.
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;
Patch Details
m_ticket changes from a raw DeferredWorkTimer::Ticket to a ThreadSafeWeakPtr<TicketData>. A new takeTicketIfActive() promotes the weak pointer to a RefPtr and returns null if the ticket was destroyed or cancelled, clearing m_ticket. The use sites didComplete(), fail(), and cancel() now early-return on a null result instead of unconditionally using std::exchange(m_ticket, nullptr). A second helper globalObjectIfActive() fetches the live JSGlobalObject* from the ticket's dependency list. In JSDOMGlobalObject.cpp, the consumeBodyReceivedByChunk lambda stops capturing the raw globalObject, capturing only VM* and the compiler, and re-derives the global object via compiler->globalObjectIfActive().
Raw pointer to an asynchronously-owned object outliving its owner across an iframe-teardown boundary, dereferenced without a liveness re-check.
Background
WebAssembly.compileStreaming/instantiateStreaming accept a fetch Response and compile the module incrementally as the body streams in. DeferredWorkTimer is JSC's mechanism for posting a deferred result back onto the JS thread; addPendingWork returns a TicketData (typedef'd Ticket) that keeps the target promise and its dependency objects (such as the requesting JSGlobalObject) alive. ThreadSafeWeakPtr<T>::get() atomically promotes to a RefPtr<T> if the object is still alive and returns null otherwise. An iframe forms its own realm with its own JSGlobalObject; removing the iframe from the DOM makes that global object collectible. Streaming compilation runs on a background worklist, so completion callbacks can fire after the originating realm has gone away.
Analysis
This is a use-after-free from a dangling raw pointer surviving owner destruction across an asynchronous boundary. The TicketData and JSGlobalObject both have lifetimes tied to the requesting iframe/realm. When streaming compilation is started in an iframe that is then removed before completion, the iframe's JSGlobalObject is garbage-collected and the deferred-work ticket is cancelled and destroyed.
Aaa Aaaaaaaaaaa Aaaaaaaa Aaaaa Aa Aaa Aaaaaaaaa Aa Aaaaa Aaaaa Aaaaaaaaaaaaaaaaaaaa Aaaa Aaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaa Aaa Aaaaaaaaa Aaaa Aa a Aaaaaaaaaaaaa Aaaa Aaa Aaaaaaa Aaaa Aaaaaa Aaaaaaaaaaaaaa Aaa Aaaaaaaaaa Aaaaaa Aaaaaaaaaaaa Aaa Aaaaaaaa Aaa Aaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaa Aaaaa Aaaa Aaaaaa Aaaaaa Aaa Aaaa Aaaaaaaaaa Aaa Aaaaaaaa Aaaaaaaaaaaaaaa Aaaaaaa Aaa Aaa Aaaaaaaaaaaaaa Aaaaaa Aaa Aaaaaaaaaa Aaaa Aaa Aaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaa Aaaa Aaa Aaa Aaa Aaaaaaaa Aaaaa Aa Aaa Aaaaa Aaaaaaaaaaaaaa Aaaaaa Aaa Aaaaaaaa Aaaaaaaaaaa Aa Aaaaaaaaaaaa Aa Aaaaaaaa Aaa Aaaaa Aaaaaaaa Aaaaaaa Aaa Aaaaa Aaaaaaaaaaa Aaaaa Aaaaaaa Aaa Aaaaaaaaaaaaaaaa Aaaaaa Aaaaaa a Aaaaaaaa Aaaaaaaaaaaaaaaaa Aaaaaaaaa Aa Aaa Aaaaaaaaa Aaa Aaaa Aaaaaaaaaaa Aa Aaa Aaaaaaaaaaa Aaaaa Aaa a Aaaaaaaa Aaaaaaa Aaaaaa Aaaaa Aaaaa Aa Aaaaaaaa Aa Aaaaa Aaaaaaaaaaa
Aaaa Aaaaaaaaaaaaa Aaaaaaa Aaaaaa Aaaaaa Aaaaaa Aaa Aaaaaaaaaa Aaaaaaaa Aaa Aaaaaaaa Aaaaa Aaaaaaa Aaaaaaa Aaaaa Aaaaaaaa Aa Aaaaa Aa a Aaaaaaaaaaaa Aaa Aaa Aaaaaaaaaaaa Aaaaa Aaaa Aaaaa Aa Aaaa Aaaaa Aaaaaa Aaa Aaaa Aaaaaaaaaaaa Aaaaaaaaaaaaaa Aaaaaaaaaaa Aaaaaaaaa Aaa Aaaaaa Aaaaaaaa Aaaa Aaa Aaaa a Aaaaa Aaaaaaaaaaaa Aaa a Aaaaaaaaa Aaaaaaaaaaaaaaaaa Aaa Aaaaaaa Aaaaaaa Aaaaa Aaaaaaaaaa Aaaaa a Aaaaaaaaaaaaaaaa Aaaaaaaaaaa Aa Aaaaaa Aaa Aaaa a Aaa Aaaaa Aaa Aaaaaaaaaaaa Aaaaaaaaa Aa Aaa Aaa Aa Aaaaaaa Aaaa Aaa Aaaaaaaaaa Aaaaa Aaaaaa
Aaaaaaaaa Aaa Aaaaaaaaaaaaaaa Aaaaaaa Aaa Aaa Aaaaaaaaaaaaaa Aaaaaaaa Aaaaaa Aaa Aaaaaaaa Aaaa Aaa Aaaaaaaaaaa Aaaa Aaaaaa Aaaa Aaaaaaaa Aaaaaaa Aa Aaa Aaaaaaa Aaa Aaaa Aaaaaaaa Aaaaaa Aaa Aaa Aaa a Aaaaaaaaaaaa Aaa Aaaaaa Aaa Aaaaaaaaaaa Aaa Aaaaaa Aaaaaa a Aaa Aaaaa Aaaaaaaaa Aa Aaa Aaaaa
🔒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.
Subscribe to read more
Audit directions
a Aaaaaaa Aaaaaaaaa Aaaaaaaaa Aaa Aaaaa Aaaaaaaaaaa Aaaaaa Aaaaa Aaaaaaaaaa Aaaa Aaaaaaa a Aaa Aaaaaaaaaaaaaaaaaaaaaaa Aaaaaaa Aaa a Aaa Aaaaaaaaaaaaa Aaaaaaa Aa a Aaaaaaaa Aaaa Aaa Aaa Aaaaa Aaa Aaaaaaaaaaa Aaaaaaaaaaaa Aa Aaaa Aaaaa Aaaaa Aaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaa Aaaa Aaaaaaaaaaaaaa Aaa Aaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaa Aaa Aaaaaaaaaaaaaaaaaaa Aaa Aaaaa Aaa Aaaaa Aaaaaaa Aaaa Aaaaaaaaaaaaaaaaaa Aaa Aaaaaa Aaaaa Aa Aaaaaaaaaaaaa Aaaaaa Aaaaaa Aaaaaaaaaaaaaa
a Aaaaaaaaa Aaaaaaa Aaaa Aaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaa Aaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaa Aaaaaa Aa Aaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaa Aaaaaaaaaaaaaaaaaa Aaaaa Aaaaaa Aa Aaaaaaa Aaa Aaaaaa Aaaaaa Aa Aaaaaaaaaa Aaa Aaaaaaaaaaaaaaaa Aa Aaaaaaaa Aaaa Aaaaaa Aaaa Aaaaaaaa Aaaa
a Aaaaaaaaaaa Aaaa Aaaaaaaaa Aaaaaaaaa Aaaaaaaa Aaaaaaaaaaa Aaaaaaa Aaaaaaaaaaa Aaaaaaaaaaa Aaa Aaaaaaaa Aaaaaaaaaaaaaaaaaaaa Aaa Aaa Aaaaa Aaaaa Aaaaaaaa Aaa Aaaaaaa Aaaa Aaa Aaaaaaaaaaaa Aaaaa Aaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaa Aa Aaaaaaaaa Aaaa Aaaaa Aaaaaaaaaa Aaaaaaa Aaa Aaaaaaaaaaaaaaaaa Aaaaaaaaa
🔒Multiple reusable audit patterns identified for finding sibling async-lifetime bugs, with concrete deferred-work and binding-callback starting points.
Subscribe to read more