Critical watch CVSS 9.8 chrome Type Confusion 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
9.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType Confusion in Dawn
DescriptionType Confusion in Dawn
ComponentDawn
Bug ClassType Confusion
Action tierwatch — ub-class only — no demonstrated corruption path
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker513012139
Fix commita9365ae383d3 (dawn) +20/-15
CWECWE-843 (Type confusion)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-14 (per vendor advisory)
Fix landed2026-05-14 (public git)
Disclosed2026-06-30
Patch gap47 days — fix public in source before the advisory

Background

`Dawn`
Chromium’s WebGPU implementation, running in the GPU process, that translates renderer-issued GPU API calls into native backend commands.
`RenderBundle`
a pre-recorded, reusable sequence of render commands replayed inside a render pass via executeBundles.
`CommandAllocator`
Dawn’s arena allocator that packs encoded command headers and their trailing variable-length data into contiguous command-buffer memory.
`ExecuteBundlesCmd`
the encoded command header whose count field declares how many Ref<RenderBundleBase> entries follow it in the allocator stream.

Root Cause Analysis

The vulnerable path is RenderPassEncoder::APIExecuteBundles, which unconditionally called allocator->Allocate<ExecuteBundlesCmd>(Command::ExecuteBundles) to emit a command header and then allocator->AllocateData<Ref<RenderBundleBase>>(count) to reserve the trailing bundle array. When the caller passed an empty bundle list (count == 0), the code still emitted the header and requested a zero-length trailing allocation via AllocateData<Ref<RenderBundleBase>>(0). The defect is confined to the CommandAllocator allocation level: emitting a command with a zero-length trailing data blob triggers the “allocation issues” the commit references, per the patch subject “Don’t allocate command space for 0 render bundles.” Critically, the per-bundle loop and every downstream consumer are bounded by for (uint32_t i = 0; i < count; ++i), so with count == 0 the loop body never executes and no Ref<RenderBundleBase> entry is ever written or read — there is no reinterpretation of adjacent bytes as bundle pointers.

The fix wraps both the header allocation and the trailing data allocation (and the loop) in an if (count) guard, making a zero-count executeBundles a no-op at encode time. The reset of mCommandBufferState is deliberately moved outside the guard so state tracking is always cleared regardless of count.

Key insight
The single mistake was requesting a zero-length trailing allocation (AllocateData<Ref<RenderBundleBase>>(0)) alongside an ExecuteBundlesCmd header when count == 0; guarding both with if (count) prevents the degenerate empty-payload command from ever entering the CommandAllocator stream while still always resetting mCommandBufferState.

Attack Path

  1. Open a render pass Renderer-controlled script begins a GPURenderPassEncoder on a valid device through the WebGPU API surfaced by Dawn.
  2. Submit empty bundle list Script calls executeBundles([]), driving RenderPassEncoder::APIExecuteBundles with count == 0.
  3. Degenerate encode Dawn emits an ExecuteBundlesCmd header and requests AllocateData<Ref<RenderBundleBase>>(0), a zero-length trailing allocation, exercising the CommandAllocator allocation issue the commit fixes. The count-bounded loop does not run, so no bundle pointers are read or dereferenced.
  4. Trigger allocator issue The zero-length trailing allocation is the observable defect; the precise runtime effect at the allocator level is not detailed in the diff. (inferred)

Impact Assessment

The advisory classifies this as a critical Type Confusion in Dawn; it is not marked in-the-wild or KEV. The diff shows only a degenerate zero-length command allocation, and because the count-bounded loop never executes when count == 0, no out-of-bounds read, pointer dereference, or memory corruption is demonstrated by the patch. Any escalation to exploitable out-of-bounds access or control-flow corruption in the GPU process is beyond what the diff establishes. (inferred)

Changed Functions

FunctionChangeNotes
if
src/dawn/native/RenderPassEncoder.cpp
modified
for
src/dawn/native/RenderPassEncoder.cpp
modified

Files Changed

  • src/dawn/native/RenderPassEncoder.cpp

Audit Directions

  • Zero-length trailing allocation
    Audit other Dawn encoder methods that pair Allocate<Cmd>() with AllocateData<T>(count) for count-driven payloads, checking whether a count == 0 input still emits a header plus a zero-length data blob.
  • State reset vs. early return
    Where an encoder both mutates tracker state (e.g. mCommandBufferState) and conditionally emits a command, confirm the state update is not skipped by a count/validity guard, as the fix here deliberately keeps the reset unconditional.
  • Empty caller-supplied lists
    Inspect WebGPU-facing entry points that accept caller-supplied arrays (bundles, buffers, bind groups) to verify empty inputs are handled as no-ops before any allocation rather than producing degenerate command records.
From a9365ae383d3fb461aa36a41d47e5d252abce122 Mon Sep 17 00:00:00 2001
From: Brandon Jones <bajones@chromium.org>
Date: Thu, 14 May 2026 13:20:05 -0700
Subject: [PATCH] Don't allocate command space for 0 render bundles

Early terminates APIExecuteBundles on the Render Pass if count is
0, which was otherwise causing allocation issues.

Bug: 513012139
Fixes: 513012139
Change-Id: Ia58e6da36c8049573e5a60a558c65fdfa72bedc8
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/308855
Auto-Submit: Brandon Jones <bajones@chromium.org>
Reviewed-by: Loko Kung <lokokung@google.com>
Commit-Queue: Brandon Jones <bajones@chromium.org>
---

diff --git a/src/dawn/native/RenderPassEncoder.cpp b/src/dawn/native/RenderPassEncoder.cpp
index 17bb569..f9898aa 100644
--- a/src/dawn/native/RenderPassEncoder.cpp
+++ b/src/dawn/native/RenderPassEncoder.cpp
@@ -362,26 +362,31 @@
                 }
             }
 
+            // Always reset state
             mCommandBufferState = CommandBufferStateTracker{};
 
-            ExecuteBundlesCmd* cmd =
-                allocator->Allocate<ExecuteBundlesCmd>(Command::ExecuteBundles);
-            cmd->count = count;
+            // Only encode an ExecuteBundles command if count > 0
+            if (count) {
+                ExecuteBundlesCmd* cmd =
+                    allocator->Allocate<ExecuteBundlesCmd>(Command::ExecuteBundles);
+                cmd->count = count;
 
-            Ref<RenderBundleBase>* bundles = allocator->AllocateData<Ref<RenderBundleBase>>(count);
-            for (uint32_t i = 0; i < count; ++i) {
-                bundles[i] = renderBundles[i];
+                Ref<RenderBundleBase>* bundles =
+                    allocator->AllocateData<Ref<RenderBundleBase>>(count);
+                for (uint32_t i = 0; i < count; ++i) {
+                    bundles[i] = renderBundles[i];
 
-                mUsageTracker.MergeResourceUsages(bundles[i]->GetResourceUsage());
-                if (bundles[i]->GetResourceUsage().usesFramebufferFetch) {
-                    mUsageTracker.MarkFramebufferFetchUsed();
+                    mUsageTracker.MergeResourceUsages(bundles[i]->GetResourceUsage());
+                    if (bundles[i]->GetResourceUsage().usesFramebufferFetch) {
+                        mUsageTracker.MarkFramebufferFetchUsed();
+                    }
+
+                    if (IsValidationEnabled()) {
+                        mIndirectDrawMetadata.AddBundle(renderBundles[i]);
+                    }
+
+                    mDrawCount += bundles[i]->GetDrawCount();
                 }
-
-                if (IsValidationEnabled()) {
-                    mIndirectDrawMetadata.AddBundle(renderBundles[i]);
-                }
-
-                mDrawCount += bundles[i]->GetDrawCount();
             }
 
             return {};
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential command stream desynchronization in Dawn via transient OOM in ExecuteBundles

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A logic error in Dawn’s RenderPassEncoder::APIExecuteBundles allows for command stream desynchronization when a transient out-of-memory (OOM) condition occurs during the execution of an empty bundle list. This leads to type confusion in the GPU process, where attacker-controlled command data is interpreted as internal object pointers.

Affected files:

  • third_party/dawn/src/dawn/native/RenderPassEncoder.cpp
  • third_party/dawn/src/dawn/native/CommandAllocator.cpp
  • third_party/dawn/src/dawn/native/CommandAllocator.h
  • third_party/dawn/src/dawn/native/vulkan/CommandBufferVk.cpp
  • third_party/dawn/src/dawn/native/Commands.cpp

Estimated timestamp from git blame: Unknown (Google3 checkout)

Summary

A potential vulnerability in Dawn (Chromium’s WebGPU implementation) could allow a compromised renderer to desynchronize the GPU process’s command stream. This is achieved by triggering a transient out-of-memory (OOM) condition during an executeBundles call with a count of zero. The resulting desynchronization leads to type confusion, allowing renderer-controlled data to be interpreted as internal pointers (Ref<T>), which could enable arbitrary code execution in the GPU process.

Root Cause Analysis

In third_party/dawn/src/dawn/native/RenderPassEncoder.cpp, the APIExecuteBundles function records an ExecuteBundlesCmd and then unconditionally attempts to allocate space for a bundle array using allocator->AllocateData<Ref<RenderBundleBase>>(count):

ExecuteBundlesCmd* cmd = allocator->Allocate<ExecuteBundlesCmd>(Command::ExecuteBundles);
cmd->count = count;
Ref<RenderBundleBase>* bundles = allocator->AllocateData<Ref<RenderBundleBase>>(count);

When count is 0, AllocateData still attempts to write a kAdditionalData marker to the command stream. If the current command block lacks sufficient space, Allocate invokes AllocateInNewBlock. In third_party/dawn/src/dawn/native/CommandAllocator.cpp, if the allocation of a new block fails due to transient memory pressure, it returns nullptr, and the kAdditionalData marker is never written.

Crucially, because count is zero, the encoder’s loop never dereferences the returned nullptr, and the function returns success without checking if the allocation (and the marker write) actually succeeded. This results in a command stream that is missing a required marker.

Potential Exploitation Path

  1. Induce Memory Pressure: An attacker induces transient memory pressure in the GPU process via WebGPU resource exhaustion.
  2. Trigger Vulnerability: The attacker calls executeBundles(0, []). If the timing is precise, the ExecuteBundlesCmd is written to a full block, but the subsequent kAdditionalData marker allocation fails.
  3. Command Injection: The attacker encodes a payload command with controlled data, such as setBlendConstant({R, G, B, A}).
  4. Backend Desynchronization: During command execution in the GPU process, the iterator processes ExecuteBundlesCmd and calls NextData<T>(0). Because the kAdditionalData marker is missing, the iterator incorrectly consumes the first uint32_t of the next block (the next command’s ID) as the marker.
  5. Type Confusion: The iterator is now misaligned and interprets the payload of the subsequent command (e.g., the floats in SetBlendConstant) as a new command ID. By controlling these bytes, an attacker can inject a command like SetRenderPipeline, which contains a Ref<T> field. The GPU process will then interpret controlled bytes as a pointer and call Release() on it during cleanup, leading to RCE.

Impact

This allows for arbitrary code execution in the GPU process. On Android, the GPU process is typically unsandboxed, making this a critical privilege escalation. On other platforms, it represents a significant step toward a full sandbox escape.

Suggested Fix

All calls to Allocate and AllocateData in RenderPassEncoder.cpp (and other encoders) should be checked for nullptr. If an allocation fails, the encoder should return a MaybeError to the EncodingContext, ensuring the command buffer is marked as invalid and not submitted for execution. Alternatively, the CommandAllocator could be modified to track a persistent error state upon any allocation failure.

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker