0cc1de910ca52728046a68e18aaa3f0f49535dde [blink] Use member functions for device callbacks.
Triage note: Lifetime bug in WebGPU logging/uncaptured-error callbacks, reachable in default config where WebGPU is enabled. Result is a null-deref crash (DoS), not corruption; fixed by using member-function binding with built-in weak handling.
The bug at a glance
The described failure is a null-pointer dereference, not a use-after-free: when the GPUDevice is garbage-collected, WrapWeakPersistent(this) is cleared to nullptr, and the hand-rolled lambda dereferences self without a null check, crashing the renderer. That is a denial-of-service (renderer crash) reachable from a web page using WebGPU, which is why it is medium rather than low. It is not patch-now because a WeakPersistent that has been nulled points at nullptr (a defined, non-dangling value under Oilpan), so the diff shows a crash primitive, not a controllable UAF or memory corruption. The commit is titled and framed as a maintainability/hardening change, and the crash is a sandboxed-renderer DoS with no demonstrated path to code execution.
GPUDevice registered WebGPU logging and uncaptured-error callbacks with Dawn using lambdas that captured WrapWeakPersistent(this) and then unconditionally dereferenced the captured pointer (self->OnLoggingImpl(...), self->OnUncapturedErrorImpl(...)). Oilpan’s WeakPersistent is automatically nulled once the GPUDevice is collected, but these lambdas never checked for that, so a callback that fires after the device is gone dereferences nullptr and crashes the renderer. The fix replaces the raw lambdas with bound member functions (GPUDevice::OnLogging, GPUDevice::OnUncapturedError) so Blink’s BindRepeating + WrapWeakPersistent machinery applies its built-in weak-pointer guard, silently dropping the call when the receiver is dead.
Root cause
WebGPU device callbacks are registered with Dawn via MakeWGPURepeatingCallback(blink::BindRepeating(…)). In the vulnerable form, the logging and uncaptured-error callbacks were built from C++ lambdas that took GPUDevice* self bound through WrapWeakPersistent(this) and immediately called self->OnLoggingImpl(...) / self->OnUncapturedErrorImpl(...). WrapWeakPersistent produces a WeakPersistent handle: under Oilpan garbage collection, once the GPUDevice becomes unreachable the handle is reset to nullptr rather than kept alive. Dawn, however, can invoke these repeating callbacks asynchronously (logging messages, uncaptured errors) at a time not synchronized with the device’s GC lifetime. If a callback fires after the device has been collected, the bound weak pointer is nullptr and the lambda dereferences it directly, with no if (!self) return; guard, producing a null-pointer read/crash in the renderer.
The fix, per the commit message, reverts to using member functions for these callbacks so the WeakPersistent handling built into Blink’s callback binding code is used instead of a manual dereference. Concretely, the diff introduces GPUDevice::OnLogging and GPUDevice::OnUncapturedError thin member wrappers (declared in gpu_device.h, defined in gpu_device.cc) that forward to the existing *Impl methods, and rebinds the callbacks with blink::BindRepeating(&GPUDevice::OnLogging, WrapWeakPersistent(this)) and &GPUDevice::OnUncapturedError. When a Blink callback is bound to a member function on a WrapWeakPersistent receiver, the binding layer checks the weak receiver before invoking and skips the call if the receiver has been collected. This eliminates the unchecked nullptr dereference.
Grounded strictly in the diff and message: the corrected fault is a missing null check on an Oilpan weak pointer, i.e. a null dereference / renderer crash, not a use-after-free of live-but-freed memory. The bugs referenced (477254529, 476509609) and the framing (“cause a crash”) are consistent with a stability/DoS defect. This is why the bugclass is reclassified here as NullDeref despite the input JSON’s UAF tag.
Key code
Unchecked WeakPersistent lambda replaced by a bound member function that gets Blink’s built-in weak guard
- error_callback.reset(MakeWGPURepeatingCallback(blink::BindRepeating(
- [](GPUDevice* self, const wgpu::Device&, wgpu::ErrorType errorType,
- wgpu::StringView message) {
- self->OnUncapturedErrorImpl(errorType,
- StringFromASCIIAndUTF8(message));
- },
- WrapWeakPersistent(this))));
+ error_callback.reset(MakeWGPURepeatingCallback(blink::BindRepeating(
+ &GPUDevice::OnUncapturedError, WrapWeakPersistent(this))));
Patch walkthrough
third_party/blink/renderer/modules/webgpu/gpu_device.cc— Rewrites both the logging-callback and uncaptured-error-callback registrations to bind member functions (&GPUDevice::OnLogging, &GPUDevice::OnUncapturedError) via blink::BindRepeating with WrapWeakPersistent(this), instead of lambdas that unconditionally dereferenced the weakself. Adds the two thin wrapper definitions GPUDevice::OnUncapturedError and GPUDevice::OnLogging that convert the wgpu::StringView and forward to OnUncapturedErrorImpl / OnLoggingImpl. The behavioral effect is that when the GPUDevice has been collected, Blink’s binding layer drops the callback rather than dereferencing nullptr.third_party/blink/renderer/modules/webgpu/gpu_device.h— Declares the new OnUncapturedError(const wgpu::Device&, wgpu::ErrorType, wgpu::StringView) and OnLogging(wgpu::LoggingType, wgpu::StringView) member functions alongside the existing *Impl methods, so the callbacks can be bound to real member functions and inherit the WeakPersistent receiver check.
Background
Oilpan WeakPersistent / WrapWeakPersistent — Blink’s garbage-collected heap (Oilpan) provides WeakPersistent handles that do not keep an object alive and are automatically reset to nullptr when the object is collected. Dereferencing one after collection yields nullptr, not freed memory.
blink::BindRepeating on a member function — When a repeating callback is bound to a member function with a WrapWeakPersistent receiver, Blink’s binding machinery checks whether the weak receiver is still alive before each invocation and skips the call if it has been collected. Binding to a lambda that captures the weak pointer bypasses this guard, leaving the null check to the programmer.
Dawn callbacks (logging / uncaptured error) — Dawn, the WebGPU implementation, invokes registered callbacks for device log messages and uncaptured errors. These fire asynchronously relative to the JavaScript GPUDevice object’s lifetime, so a callback can arrive after the wrapper GPUDevice has been garbage-collected.
Renderer DoS vs. memory corruption — A null-pointer dereference in the sandboxed renderer typically aborts the process (a crash / tab kill), which is a denial of service. It is distinct from a use-after-free on attacker-groomed memory, which can enable corruption; the diff here addresses the former.
Vulnerability window
- Introduced — The logging and uncaptured-error callbacks were implemented as lambdas capturing WrapWeakPersistent(this) and dereferencing it without a null check, so a callback after device collection would hit nullptr.
- Reachable — A web page that creates a WebGPU device, drops its references (allowing GC), and has Dawn deliver a pending logging message or uncaptured error can drive the callback to fire against a nulled weak pointer, crashing the renderer. Bugs 477254529 / 476509609 track the observed crashes.
- Fixed — Commit 0cc1de910c (Jan 21 2026, main@{#1572755}) rebinds the callbacks to member functions so Blink’s built-in weak-receiver check drops the call safely.
Proof of concept
No test shipped in the diff. Honest trigger recipe (no working exploit demonstrated): from JavaScript, obtain a GPUDevice via navigator.gpu.requestAdapter()/requestDevice(), arrange for a device log message or uncaptured error to be queued, then release all references to the device and force garbage collection so its WrapWeakPersistent handle is nulled before Dawn delivers the pending callback. On the pre-patch code the callback dereferences the null weak pointer and crashes the renderer. Timing GC against Dawn’s asynchronous delivery is the hard part and is not reliably controllable from script; the outcome is a renderer crash (DoS), not memory corruption. No exploit code is provided.
Exploitation
- Set up the race — Attacker JS must get the GPUDevice collected (drop refs, trigger GC) while a Dawn logging/uncaptured-error callback is still pending. GC timing is not directly controllable from script, so triggering is opportunistic rather than deterministic.
- Fault type — The bound weak pointer is nullptr under Oilpan, so the fault is a null-pointer read, not access to freed, attacker-controlled memory. There is no shown way to place controlled data at the dereferenced address.
- Impact ceiling — Best realistic outcome is a renderer-process crash (tab kill / DoS) inside the sandbox. No path to information disclosure or code execution is demonstrated in the diff.
Detection & hunting
For defenders and detection engineers:
- Renderer null-deref crashes in WebGPU callbacks — Look for renderer crash reports with faulting addresses near null in stacks through GPUDevice::OnLoggingImpl / OnUncapturedErrorImpl or the WebGPU callback lambda thunks in gpu_device.cc.
- WebGPU usage correlated with GC pressure — Pages that create and rapidly discard GPUDevice objects while generating device errors/log messages are the behavioral profile that would exercise the pre-patch path.
Audit directions
- Other Dawn/WebGPU callbacks in modules/webgpu — Grep for MakeWGPURepeatingCallback / blink::BindRepeating with lambda captures of WrapWeakPersistent(this) that dereference the captured pointer without an
if (!self) return;guard; each is the same anti-pattern this commit fixes. - Blink-wide manual WeakPersistent dereferences in callbacks — Search for lambdas capturing WrapWeakPersistent/WrapWeakPersistent(this) that call
self->...directly instead of binding a member function; these bypass Blink’s built-in weak-receiver check and can null-deref after collection.