CVE-2026-13776
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/dawn/native/RenderPassEncoder.cpp |
modified | |
forsrc/dawn/native/RenderPassEncoder.cpp |
modified |
Files Changed
src/dawn/native/RenderPassEncoder.cpp
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.