← Chrome — 2026-W04

3a7666894a5e6e2ca3248ec2f790504dd3dd0cb7  [M145] [blink] Use member functions for device callbacks.

severity low class UAF CVSS 4.3 confidence 0.65 signal score 3.8 third_party/blink/renderer AI review CONFIRMED
Primitive: Identical to origin CL: member-function binding replaces raw-lambda weak-ptr callbacks that could deref null after GPUDevice teardown.
Triage note: M145 cherry-pick of 0cc1de910c (Bug 478249693). Same null-deref DoS in WebGPU callbacks; kept only for tracking, deduped to the primary.

The bug at a glance

This is the [M145] stable-branch backport (cherry-pick of 0cc1de910c, refs/branch-heads/7632@{#881}) of the mainline CL ‘Use member functions for device callbacks.’ It converts hand-rolled raw-lambda + WrapWeakPersistent callbacks into member-function bindings so Blink’s callback machinery applies its built-in WeakPersistent null-guarding. The original commit message states the concrete defect: the manual lambdas dereferenced self (a WeakPersistent) without checking for null, so once the GPUDevice was garbage-collected the WeakPersistent resolved to nullptr and the callback crashed. That is a null-pointer dereference at a Dawn/WebGPU callback site, not a demonstrated attacker-controlled UAF write. It is renderer-side, requires the GPUDevice to be torn down while a device-level error or logging callback is still in flight, and the observable outcome is a crash (DoS-grade), so it is watch-tier low, not patch-now. CVSS 4.3 reflects a network-reachable-but-low-impact availability defect with no memory-corruption primitive shown.

If you run WebGPU in your fleet, this is a reliability-and-latent-safety fix worth noting, not an emergency. A page that creates a GPUDevice, provokes an uncaptured error or a logging message, and then drops all references so the device is collected before Dawn delivers the queued callback could hit a nullptr deref and crash the renderer. There is no evidence of a controllable use-after-free or of an attacker steering freed memory; the crash is the realistic worst case. Track it because WebGPU callback lifetime bugs are a rich area and because the backport signals Chrome considered it security-relevant enough to ship to stable, but do not treat it as exploitable-grade.

Root cause

GPUDevice registers two device-level callbacks with Dawn: an uncaptured-error callback and a logging callback. Before this change, on the non-observer path the callbacks were built from stateless lambdas that captured this via WrapWeakPersistent(this) and, inside the lambda body, immediately called self->OnUncapturedErrorImpl(...) / self->OnLoggingImpl(...). WrapWeakPersistent yields a WeakPersistent that is automatically nulled when the GPUDevice is garbage-collected, but the lambda body performed the self-> dereference manually with no null check. Dawn invokes these callbacks asynchronously; if the GPUDevice had already been collected by the time an error or log arrived, self was nullptr and the self->... call dereferenced null, crashing the renderer.

The fix replaces the lambdas with blink::BindRepeating(&GPUDevice::OnUncapturedError, WrapWeakPersistent(this)) and &GPUDevice::OnLogging, and adds those two thin member functions that forward to the existing *Impl methods. When a BindRepeating callback is bound to a member function with a WeakPersistent receiver, Blink’s callback binding layer checks the weak receiver for null before invoking the method and silently drops the call if the object is gone. So the null-guard moves from error-prone hand-written code into the framework path that is already correct and tested. This is fundamentally a hardening/refactor with a real latent-safety motive; the class is UAF/null-deref but the shipped impact is a crash.

Key code

Before: raw lambda dereferences a WeakPersistent self with no null check (uncaptured-error path). After: bound member function lets Blink’s weak-receiver handling drop the call.

    error_callback.reset(MakeWGPURepeatingCallback(blink::BindRepeating(
-        [](GPUDevice* self, const wgpu::Device&, wgpu::ErrorType errorType,
-           wgpu::StringView message) {
-          self->OnUncapturedErrorImpl(errorType,
-                                      StringFromASCIIAndUTF8(message));
-        },
-        WrapWeakPersistent(this))));
+        &GPUDevice::OnUncapturedError, WrapWeakPersistent(this))));

Patch walkthrough

  • third_party/blink/renderer/modules/webgpu/gpu_device.cc — Two callback construction sites (logging near line 176, uncaptured-error near line 852) drop their inline lambdas and instead BindRepeating to new member functions with WrapWeakPersistent(this). Two new forwarders, GPUDevice::OnUncapturedError and GPUDevice::OnLogging, are added; each just converts the wgpu::StringView and calls the existing *Impl. The null-safety now comes from Blink’s WeakPersistent-aware binding rather than an unchecked self-> deref.
  • third_party/blink/renderer/modules/webgpu/gpu_device.h — Declares the two new forwarder methods, OnUncapturedError(const wgpu::Device&, wgpu::ErrorType, wgpu::StringView) and OnLogging(wgpu::LoggingType, wgpu::StringView), alongside the pre-existing *Impl methods.

Background

WeakPersistent / WrapWeakPersistent — A Blink smart pointer to an Oilpan-GC object that does not keep it alive and becomes null when the object is collected. When bound as the receiver of a BindRepeating(&Class::Method, WrapWeakPersistent(this)) callback, Blink checks it for null and skips the call if the object is gone; when used inside a raw lambda you must null-check it yourself.

Dawn device callbacks — WebGPU error and logging notifications are delivered asynchronously by Dawn to callbacks GPUDevice registers. Their timing is not tied to the JS reference graph, so they can fire after the JS-visible GPUDevice is unreferenced and collected.

[M145] backport — The subject’s [M145] tag and the Cr-Branched-From / refs/branch-heads/7632 lines mark this as a stable-branch cherry-pick of mainline 0cc1de910c (Cr-Commit-Position 1572755). Backporting a refactor to stable indicates Chrome weighted the crash/latent-safety risk as worth shipping out-of-band.

Vulnerability window

  1. Origin — Mainline CL merged 2026-01-23 (Loko Kung), refs/heads/main@{#1572755}, bugs 477254529 / 476509609.
  2. Backport — Cherry-picked to M145 stable (refs/branch-heads/7632@{#881}) via rubber-stamper, adding bug 478249693.
  3. Analyst review — 2026-09-27: assessed as watch-tier low; crash-grade null-deref, no memory-corruption primitive demonstrated.

Proof of concept

No test ships in this diff and no exploit is demonstrated. Recipe to reproduce the crash class (not an exploit): request a GPUDevice via navigator.gpu, trigger an uncaptured error (e.g. an invalid operation) or a logging event so Dawn has a pending device-level callback, then drop every JS reference to the device and force garbage collection so the WeakPersistent is nulled before Dawn delivers. On the pre-patch build the lambda would deref a null self. This yields at most a renderer crash; nothing here shows attacker-controlled memory reuse.

Exploitation

  1. Timing window — The bug requires the GPUDevice to be GC’d between callback registration and Dawn’s async delivery. An attacker can nudge this with reference-dropping plus GC pressure but cannot deterministically guarantee the ordering.
  2. Primitive — The faulting operation is self->Method() on a null WeakPersistent, i.e. a read/call through nullptr. This is a crash, not a write primitive; there is no freed-then-reused object under attacker control shown in the diff.
  3. Ceiling — Realistic impact is renderer denial of service. Escalation to RCE would need an unrelated primitive; nothing in this change supports that, which is why it stays watch-tier.

Detection & hunting

For defenders and detection engineers:

  • Renderer crash reports —
  • WebGPU telemetry —

Audit directions

  • Other Dawn/WebGPU callbacks —
  • WrapWeakPersistent in lambdas fleet-wide —

Before / after

Loading diff…