← All issues

[1] Unsafe argument capture in EXTDisjointTimerQueryWebGL2::queryCounterEXT()

Severity: High | Component: WebGL — EXTDisjointTimerQueryWebGL2 | 84075f4

Rated High because the observable effect is a use-after-free on a WebGLQuery object reachable from any web page with a WebGL 2 context, and the attacker controls the timing window between lambda creation and GC-triggered destruction — escalation to a controlled UAF primitive depends on heap grooming feasibility (unverified) but the dangling reference path is confirmed by the diff with confidence 0.95.

The lambda passed to queueMicrotask captured the WebGLQuery& query parameter by reference. Since the microtask runs asynchronously, the query object could be destroyed before execution. The fix wraps the capture in a Ref smart pointer.

Source/WebCore/html/canvas/EXTDisjointTimerQueryWebGL2.cpp

- protect(protect(context->scriptExecutionContext())->eventLoop())->queueMicrotask(protect(context->scriptExecutionContext())->vm(), [&] {
- query.makeResultAvailable();
+ protect(protect(context->scriptExecutionContext())->eventLoop())->queueMicrotask(protect(context->scriptExecutionContext())->vm(), [query = Ref { query }] {
+ query->makeResultAvailable();
});

The patch modifies a single call site in EXTDisjointTimerQueryWebGL2::queryCounterEXT(). The lambda's capture list changes from [&] (capture all by reference) to [query = Ref { query }] (capture the query as a reference-counted smart pointer). Inside the lambda body, query.makeResultAvailable() becomes query->makeResultAvailable() to match smart-pointer dereference syntax. No other logic changes — the fix is entirely in the capture semantics.

Dangling reference from by-reference lambda capture of a ref-counted object passed to an asynchronous task.

The EXT_disjoint_timer_query_webgl2 extension provides GPU timestamp queries for WebGL 2 performance measurement. When JavaScript calls queryCounterEXT(), the implementation schedules a microtask to mark the query result as available after the current GPU operation completes.

queueMicrotask schedules a lambda to run asynchronously — after the current JavaScript execution completes but before control returns to the event loop. This creates a temporal gap between when the lambda is created and when it executes.

WebGLQuery is a ref-counted WebGL object representing a GPU query; it can be destroyed when JavaScript drops all references and garbage collection runs. Ref is WebKit's intrusive reference-counting smart pointer — constructing a Ref from a reference increments the refcount and prevents destruction while the Ref is alive. The surrounding code in queryCounterEXT already uses protect() wrappers for the execution context, event loop, and GL objects — but the query parameter itself was captured by plain C++ reference.

The root cause is a by-reference capture of a ref-counted object in a lambda that outlives the synchronous scope. Before the fix, queryCounterEXT received WebGLQuery& query as a parameter and captured it via [&] in a lambda passed to queueMicrotask. Since the microtask executes asynchronously, the WebGLQuery object's reference count is not held by the lambda. If JavaScript drops all references to the query object and triggers garbage collection between the queryCounterEXT call and microtask execution, the object is destroyed. When the microtask fires, it calls query.makeResultAvailable() on freed memory — a use-after-free.

The discovery angle is straightforward code review: the [&] capture pattern in a lambda passed to an asynchronous API is a well-known C++ antipattern that can be found through mechanical inspection of capture lists at async scheduling call sites.

🔒

Explores the exploitation window and heap reclamation feasibility for this asynchronous lifetime bug

Subscribe to read more

🔒

Multiple audit patterns identified for similar async capture bugs across WebGL and WebCore scheduling APIs

Subscribe to read more