[3] Use-after-free of StreamingCompiler::m_ticket across iframe teardown
compileStreaming hands a raw deferred-work ticket and a raw globalObject to a callback that fires later — remove the iframe first, and both are already gone.
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
Source/JavaScriptCore/wasm/WasmStreamingCompiler.cpp
Source/WebCore/bindings/js/JSDOMGlobalObject.cpp
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.
The compilation finishes later on the worklist, at which point didComplete/fail used std::exchange(m_ticket, nullptr) and scheduled work on a TicketData* that had already been freed. Symmetrically, the body-chunk lambda dereferenced its captured raw globalObject (globalObject->vm()) after that global object had been collected. The specific iframe-teardown trigger and the after-teardown timing are consistent with the surrounding StreamingPlan/Worklist code but are not directly shown in the diff. Exploitability beyond the reliable dereference is conditional: an attacker who could reliably reclaim the freed allocations could develop the dangling-pointer access toward a stronger memory-corruption primitive in the renderer, but that reclamation is not established here, and a separate sandbox escape would still be required to leave WebContent.
This vulnerability weakens memory safety within the WebContent process. The security model assumes objects whose lifetime is bound to a realm/iframe are not dereferenced after that realm is torn down; before the fix, asynchronous streaming-Wasm completions outliving the iframe violated that for both a freed TicketData and a collected JSGlobalObject. The removed comment about m_ticket being a non-GC-scannable PackedPtr is itself the tell — the field was deliberately invisible to the GC, so nothing kept the referenced state alive.
Note: The iframe-teardown trigger and the after-teardown worklist timing are inferred from the surrounding code rather than directly visible in the commit. The core lifetime hazard and its fix — weak-holding the ticket and re-deriving the global object — are fully supported by the diff.
Audit directions
- Async callbacks capturing raw realm pointers. Native async operations that capture a raw
JSGlobalObject*/realm pointer (or a raw deferred-work ticket) in a callback that can run after the originating iframe/realm is torn down. Audit otherDeferredWorkTimer::addPendingWorkconsumers; grep JavaScriptCore foraddPendingWork,scheduleWorkSoon, andcancelPendingWorkuse sites and check whether each promotes/validates the ticket (e.g. anisCancelledcheck) before dereferencing. - Binding lambdas with bare globalObject/document/frame captures. Review
Source/WebCore/bindings/jscallbacks driven byconsumeBodyReceivedByChunkand similar Response-streaming entry points to confirm the global object is re-fetched and liveness-checked at callback time rather than captured raw. - Remaining Wasm streaming lifecycle paths. Investigate whether
finalize,addBytes, and worklistdidCompileFunctioncan run after realm teardown and whether they now consistently route throughtakeTicketIfActive()/globalObjectIfActive(); verify no remaining path readsm_ticketwithout the promote-and-check sequence.