← Chrome — 2026-W02

6b088b99daa0116d45bd72e1fba82eff0dda6a29  WebNN: Schedule `OnLost` task immediately to avoid race conditions

severity medium class InfoDisclosure CVSS 5.3 confidence 0.7 signal score 6.1 services/webnn/dml AI review CONFIRMED
Primitive: Context-lost handling was scheduled after tensor reads (race allowing reads of invalid results) and broadcast the lost reason to all contexts, including those from other origins, leaking cross-origin info.
Triage note: Decouples OnLost from DestroyAllContextsAndKillGpuProcess so it fires before tensor reads and no longer broadcasts the reason across origins. Commit explicitly calls out the cross-origin info-leak; a race/info-disclosure, not memory corruption.

The bug at a glance

Two distinct defects are folded into one CL. The primary author motive is a functional race (flaky conformance results), but the commit message itself calls out a genuine cross-origin information-disclosure weakness: the GPU-process context provider broadcast the raw context-lost reason string to every live WebNNContextImpl, including contexts owned by unrelated origins. The leaked datum is a driver/backend-supplied error message, not attacker-chosen memory, and delivery was already non-deterministic because the GPU process is torn down concurrently. That bounds impact well below a true cross-origin read primitive, so medium/5.3 is honest: real cross-origin channel, low-entropy and unreliable payload, no memory-safety consequence.

Silent security content hidden behind a ‘fix flaky test / race condition’ subject. The interesting half is not the scheduling race the title advertises but the removal of the OnLost broadcast loop in WebNNContextProviderImpl, which the message explicitly flags as a cross-origin leak (crbug 474141334 tracks re-adding same-origin-only broadcast). Watch-tier because the security payload is a low-value error string and the leak was already racy.

Root cause

Before the patch, when a WebNN device was removed (DirectML or ORT backend), the backend called WebNNContextImpl::DestroyAllContextsAndKillGpuProcess(reason). That hopped to the GPU main thread and, inside WebNNContextProviderImpl::DestroyAllContextsAndKillGpuProcess(reason), iterated every context in context_impls_ calling impl->OnLost(reason) before running lose_all_contexts_callback_ to kill the GPU process. Two consequences follow directly from the diff. First, OnLost for the context that actually experienced the loss was serialized behind a main-thread PostTask, so it could land after an already-queued output-tensor read on that context’s owning thread, letting the renderer read an invalid result instead of receiving MLTensor::OnConnectionError. Second, the loop delivered the same reason string to contexts belonging to other origins/pages. The fix decouples the two responsibilities: the losing backend now calls OnLost(reason) directly (immediate, on the affected context) and then DestroyAllContextsAndKillGpuProcess() with no reason argument; the provider’s broadcast loop is deleted entirely, so no per-context OnLost is fanned out and the reason parameter is removed from the whole call chain.

Key code

Provider no longer fans the lost reason out to every (possibly cross-origin) context

-void WebNNContextProviderImpl::DestroyAllContextsAndKillGpuProcess(
-    const std::string& reason) {
+void WebNNContextProviderImpl::DestroyAllContextsAndKillGpuProcess() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_checker_);
-  // Send the contexts lost reason to the renderer process.
-  for (const auto& impl : context_impls_) {
-    impl->OnLost(reason);
-  }
 
   std::move(lose_all_contexts_callback_).Run();
 }

Patch walkthrough

  • services/webnn/dml/context_impl_dml.cc — In the DirectML device-removal path, DestroyAllContextsAndKillGpuProcess(“device removed.”) is split into OnLost(“Device removed.”) followed by DestroyAllContextsAndKillGpuProcess(). OnLost now runs immediately on this context’s owning sequence, so MLTensor::OnConnectionError fires before any pending tensor read completes.
  • services/webnn/ort/context_impl_ort.cc — Same restructuring for the ONNX Runtime backend, plus explanatory comments: the author states broadcasting the lost reason across contexts (especially cross-origin ones) may leak information from one origin to another, and files crbug 474141334 to later restore same-origin-only broadcast. This is the clearest statement of the security intent.
  • services/webnn/webnn_context_impl.cc / .h — DestroyAllContextsAndKillGpuProcess loses its reason parameter across both the cross-thread PostTask wrapper and the direct call; the comment is trimmed from ‘inform the renderer and kill’ to just ‘Kill the GPU process’. Responsibility for informing the renderer moved to the explicit OnLost call at the backend.
  • services/webnn/webnn_context_provider_impl.cc / .h — The core change: the loop that called impl->OnLost(reason) for every context in context_impls_ is deleted, and the reason parameter removed. The provider now only runs lose_all_contexts_callback_ to tear down the GPU process. This is what eliminates the cross-origin broadcast.

Background

WebNN (Web Neural Network API) — Renderer-exposed API for on-device ML inference. Its service side runs in the GPU process (services/webnn) with per-backend context implementations (DirectML on Windows, ONNX Runtime, TFLite).

Context lost — Analogous to WebGL context loss. When the underlying device is removed or errors, the context must be invalidated and the renderer notified via Mojo (MLTensor::OnConnectionError) so pending operations reject rather than return garbage.

GPU main thread vs context owning thread — Each WebNNContextImpl has an owning sequence where dispatch/read tensor tasks run in order. DestroyAllContextsAndKillGpuProcess executes on the GPU main sequence, creating a cross-sequence ordering hazard against queued tensor reads.

Broadcast leak — context_impls_ holds every live context in the GPU process regardless of originating page/origin. Iterating it to deliver a reason string crosses the origin boundary the renderer normally enforces.

Vulnerability window

  1. Introduced — Present since the WebNN Windows context-loss plumbing routed OnLost through the provider’s broadcast loop. Both the ordering race and the cross-origin fan-out are properties of that original design.
  2. Observed — Surfaced as flaky WebNN conformance results (renderer sometimes read an invalid tensor before OnConnectionError). Bug 472359963 filed.
  3. Fixed — Commit landed 2026-01-10 (r1567460), refs/heads/main@{#1567460}. Follow-up crbug 474141334 tracks restoring same-origin broadcast.

Proof of concept

No exploit or test code ships in this diff. A behavioral repro recipe: (1) open two documents from different origins that each create a WebNN context on Windows using the DirectML or ORT backend; (2) from one origin, issue a dispatch followed by a tensor read; (3) trigger a device-removal condition (e.g. a GPU reset / TDR, or driver error) so the backend enters the loss path. Pre-patch, the second origin’s context would receive the same OnLost reason string, and a renderer with a read queued before OnLost could observe an invalid tensor result. No memory-corruption exploit exists; the disclosed value is a fixed backend error message, so this is a confidentiality-only observation, not a demonstrable attacker-controlled read.

Exploitation

  1. Precondition — Windows, WebNN enabled, a device-removal/error event. The attacker cannot reliably force device removal; it is an ambient/rare condition, which greatly limits practical exploitation.
  2. Cross-origin signal — A co-resident context from another origin receives the lost reason string. Because the string is backend-fixed (‘device removed.’, ORT error text), the cross-origin bits conveyed are essentially the fact and category of loss, not secret data.
  3. Race variant — For the same origin, mis-ordering let a renderer read an invalid tensor result. That is a correctness/data-integrity issue on the origin’s own data, not a cross-origin read.
  4. Ceiling — No path to memory disclosure or code execution. Realistic impact is a weak cross-origin side channel plus flaky results.

Detection & hunting

For defenders and detection engineers:

  • OnLost fan-out — Flag any code that iterates a process-global container of per-origin service objects (context_impls_, similar registries) to deliver renderer-visible state. That pattern is where cross-origin leaks hide.
  • Reason strings crossing Mojo — Audit error/reason strings passed to renderer-facing callbacks for whether they can carry backend- or origin-specific detail; prefer generic messages.
  • Cross-sequence teardown vs queued work — Look for teardown/invalidation posted to a different sequence than the one processing user requests; those are ordering-race candidates producing stale reads.

Audit directions

  • services/webnn other backends — Confirm TFLite and any future backend do not still route through a broadcast; verify each calls OnLost on the affected context only, matching the DML/ORT fix.
  • GPU-process service registries — Enumerate other GPU-process services holding process-global per-origin state (WebGPU, WebGL context sets) and check none broadcast origin-derived state on device loss.
  • Follow-up 474141334 — When same-origin broadcast is reintroduced, verify the origin/isolation-key filter is correct and cannot be widened to cross-origin again.

Before / after

Loading diff…