CVE-2026-13776
Overview
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
countfield declares how manyRef<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.
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
- Open a render pass
Renderer-controlled script begins a
GPURenderPassEncoderon a valid device through the WebGPU API surfaced by Dawn. - Submit empty bundle list
Script calls
executeBundles([]), drivingRenderPassEncoder::APIExecuteBundleswithcount == 0. - Degenerate encode
Dawn emits an
ExecuteBundlesCmdheader and requestsAllocateData<Ref<RenderBundleBase>>(0), a zero-length trailing allocation, exercising theCommandAllocatorallocation issue the commit fixes. Thecount-bounded loop does not run, so no bundle pointers are read or dereferenced. - 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
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
| Function | Change | Notes |
|---|---|---|
ifsrc/dawn/native/RenderPassEncoder.cpp |
modified | |
forsrc/dawn/native/RenderPassEncoder.cpp |
modified |
Files Changed
src/dawn/native/RenderPassEncoder.cpp
Audit Directions
- Zero-length trailing allocationAudit other Dawn encoder methods that pair
Allocate<Cmd>()withAllocateData<T>(count)for count-driven payloads, checking whether acount == 0input still emits a header plus a zero-length data blob. - State reset vs. early returnWhere an encoder both mutates tracker state (e.g.
mCommandBufferState) and conditionally emits a command, confirm the state update is not skipped by acount/validity guard, as the fix here deliberately keeps the reset unconditional. - Empty caller-supplied listsInspect 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.
Patch
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 {};
Original Bug Report
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.cppthird_party/dawn/src/dawn/native/CommandAllocator.cppthird_party/dawn/src/dawn/native/CommandAllocator.hthird_party/dawn/src/dawn/native/vulkan/CommandBufferVk.cppthird_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
- Induce Memory Pressure: An attacker induces transient memory pressure in the GPU process via WebGPU resource exhaustion.
- Trigger Vulnerability: The attacker calls
executeBundles(0, []). If the timing is precise, theExecuteBundlesCmdis written to a full block, but the subsequentkAdditionalDatamarker allocation fails. - Command Injection: The attacker encodes a payload command with controlled data, such as
setBlendConstant({R, G, B, A}). - Backend Desynchronization: During command execution in the GPU process, the iterator processes
ExecuteBundlesCmdand callsNextData<T>(0). Because thekAdditionalDatamarker is missing, the iterator incorrectly consumes the firstuint32_tof the next block (the next command’s ID) as the marker. - 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 likeSetRenderPipeline, which contains aRef<T>field. The GPU process will then interpret controlled bytes as a pointer and callRelease()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.