CVE-2026-10927
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/dawn/native/Commands.cpp |
modified | |
ifsrc/dawn/native/d3d11/CommandBufferD3D11.cpp |
modified | |
ifsrc/dawn/native/d3d12/CommandBufferD3D12.cpp |
modified | |
ifsrc/dawn/native/metal/CommandBufferMTL.mm |
modified | |
ifsrc/dawn/native/opengl/CommandBufferGL.cpp |
modified | |
ifsrc/dawn/native/vulkan/CommandBufferVk.cpp |
modified | |
TEST_Psrc/dawn/tests/end2end/CommandEncoderTests.cpp |
modified |
Files Changed
src/dawn/native/Commands.cppsrc/dawn/native/d3d11/CommandBufferD3D11.cppsrc/dawn/native/d3d12/CommandBufferD3D12.cppsrc/dawn/native/metal/CommandBufferMTL.mmsrc/dawn/native/opengl/CommandBufferGL.cppsrc/dawn/native/vulkan/CommandBufferVk.cppsrc/dawn/tests/end2end/CommandEncoderTests.cpp
Patch
From 175606a9f9d8e0a1ffd1c08e05d7a02fdc9d9fbe Mon Sep 17 00:00:00 2001
From: Corentin Wallez <cwallez@chromium.org>
Date: Thu, 23 Apr 2026 11:15:32 -0700
Subject: [PATCH] [dawn] Fix backends not consuming data of empty WriteBuffer commands
Fixed: 500090141
Change-Id: I3ceb070c59cc9c865c1d5bcc0451c80d25b50d4a
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/304675
Commit-Queue: Corentin Wallez <cwallez@chromium.org>
Auto-Submit: Corentin Wallez <cwallez@chromium.org>
Reviewed-by: Loko Kung <lokokung@google.com>
---
diff --git a/src/dawn/native/Commands.cpp b/src/dawn/native/Commands.cpp
index 05d9afa..f131c97 100644
--- a/src/dawn/native/Commands.cpp
+++ b/src/dawn/native/Commands.cpp
@@ -422,9 +422,7 @@
case Command::WriteBuffer: {
auto cmd = commands->NextCommand<WriteBufferCmd>();
- if (cmd->size > 0) {
- commands->NextData<uint8_t>(cmd->size);
- }
+ commands->NextData<uint8_t>(cmd->size);
break;
}
diff --git a/src/dawn/native/d3d11/CommandBufferD3D11.cpp b/src/dawn/native/d3d11/CommandBufferD3D11.cpp
index ee935cd..c74bb8e 100644
--- a/src/dawn/native/d3d11/CommandBufferD3D11.cpp
+++ b/src/dawn/native/d3d11/CommandBufferD3D11.cpp
@@ -502,6 +502,8 @@
case Command::WriteBuffer: {
WriteBufferCmd* cmd = mCommands.NextCommand<WriteBufferCmd>();
+ uint8_t* data = mCommands.NextData<uint8_t>(cmd->size);
+
if (cmd->size == 0) {
// Skip no-op writes.
continue;
@@ -509,7 +511,6 @@
Buffer* dstBuffer = ToBackend(cmd->buffer.Get());
DAWN_TRY(dstBuffer->TrackUsage(commandContext, pendingSerial));
- uint8_t* data = mCommands.NextData<uint8_t>(cmd->size);
DAWN_TRY(dstBuffer->Write(commandContext, cmd->offset, data, cmd->size));
break;
diff --git a/src/dawn/native/d3d12/CommandBufferD3D12.cpp b/src/dawn/native/d3d12/CommandBufferD3D12.cpp
index 2d6c6b4..28f9822 100644
--- a/src/dawn/native/d3d12/CommandBufferD3D12.cpp
+++ b/src/dawn/native/d3d12/CommandBufferD3D12.cpp
@@ -1389,12 +1389,13 @@
WriteBufferCmd* write = mCommands.NextCommand<WriteBufferCmd>();
const uint64_t offset = write->offset;
const uint64_t size = write->size;
+ uint8_t* data = mCommands.NextData<uint8_t>(size);
+
if (size == 0) {
continue;
}
Buffer* dstBuffer = ToBackend(write->buffer.Get());
- uint8_t* data = mCommands.NextData<uint8_t>(size);
DAWN_TRY(device->GetDynamicUploader()->WithUploadReservation(
size, kCopyBufferToBufferOffsetAlignment,
diff --git a/src/dawn/native/metal/CommandBufferMTL.mm b/src/dawn/native/metal/CommandBufferMTL.mm
index a8db888..ed8b4a4 100644
--- a/src/dawn/native/metal/CommandBufferMTL.mm
+++ b/src/dawn/native/metal/CommandBufferMTL.mm
@@ -1513,12 +1513,13 @@
WriteBufferCmd* write = mCommands.NextCommand<WriteBufferCmd>();
const uint64_t offset = write->offset;
const uint64_t size = write->size;
+ uint8_t* data = mCommands.NextData<uint8_t>(size);
+
if (size == 0) {
continue;
}
Buffer* dstBuffer = ToBackend(write->buffer.Get());
- uint8_t* data = mCommands.NextData<uint8_t>(size);
Device* device = ToBackend(GetDevice());
DAWN_TRY(device->GetDynamicUploader()->WithUploadReservation(
diff --git a/src/dawn/native/opengl/CommandBufferGL.cpp b/src/dawn/native/opengl/CommandBufferGL.cpp
index 06d2d6b..6d14648 100644
--- a/src/dawn/native/opengl/CommandBufferGL.cpp
+++ b/src/dawn/native/opengl/CommandBufferGL.cpp
@@ -1139,12 +1139,13 @@
WriteBufferCmd* write = mCommands.NextCommand<WriteBufferCmd>();
uint64_t offset = write->offset;
uint64_t size = write->size;
+ uint8_t* data = mCommands.NextData<uint8_t>(size);
+
if (size == 0) {
continue;
}
Buffer* dstBuffer = ToBackend(write->buffer.Get());
- uint8_t* data = mCommands.NextData<uint8_t>(size);
DAWN_TRY(dstBuffer->EnsureDataInitializedAsDestination(offset, size));
DAWN_GL_TRY(gl, BindBuffer(GL_ARRAY_BUFFER, dstBuffer->GetHandle()));
diff --git a/src/dawn/native/vulkan/CommandBufferVk.cpp b/src/dawn/native/vulkan/CommandBufferVk.cpp
index f726da9..60438f4 100644
--- a/src/dawn/native/vulkan/CommandBufferVk.cpp
+++ b/src/dawn/native/vulkan/CommandBufferVk.cpp
@@ -1455,12 +1455,13 @@
WriteBufferCmd* write = mCommands.NextCommand<WriteBufferCmd>();
const uint64_t offset = write->offset;
const uint64_t size = write->size;
+ uint8_t* data = mCommands.NextData<uint8_t>(size);
+
if (size == 0) {
continue;
}
Buffer* dstBuffer = ToBackend(write->buffer.Get());
- uint8_t* data = mCommands.NextData<uint8_t>(size);
DAWN_TRY(device->GetDynamicUploader()->WithUploadReservation(
size, kCopyBufferToBufferOffsetAlignment,
diff --git a/src/dawn/tests/end2end/CommandEncoderTests.cpp b/src/dawn/tests/end2end/CommandEncoderTests.cpp
index 94be114..258f5ae 100644
--- a/src/dawn/tests/end2end/CommandEncoderTests.cpp
+++ b/src/dawn/tests/end2end/CommandEncoderTests.cpp
@@ -61,6 +61,21 @@
EXPECT_BUFFER_U32_EQ(0, bufferC, 3 * sizeof(uint32_t));
}
+// Tests an empty WriteBuffer commands.
+TEST_P(CommandEncoderTests, EmptyWriteBuffer) {
+ wgpu::Buffer buffer =
+ utils::CreateBufferFromData(device, wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::CopySrc,
+ {
+ 42,
+ });
+
+ wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+ encoder.WriteBuffer(buffer, 0, nullptr, 0);
+ wgpu::CommandBuffer commands = encoder.Finish();
+ queue.Submit(1, &commands);
+
+ EXPECT_BUFFER_U32_EQ(42, buffer, 0);
+}
DAWN_INSTANTIATE_TEST(CommandEncoderTests,
D3D11Backend(),
D3D12Backend(),
Regression Test / PoC
diff --git a/src/dawn/tests/end2end/CommandEncoderTests.cpp b/src/dawn/tests/end2end/CommandEncoderTests.cpp
index 94be114..258f5ae 100644
--- a/src/dawn/tests/end2end/CommandEncoderTests.cpp
+++ b/src/dawn/tests/end2end/CommandEncoderTests.cpp
@@ -61,6 +61,21 @@
EXPECT_BUFFER_U32_EQ(0, bufferC, 3 * sizeof(uint32_t));
}
+// Tests an empty WriteBuffer commands.
+TEST_P(CommandEncoderTests, EmptyWriteBuffer) {
+ wgpu::Buffer buffer =
+ utils::CreateBufferFromData(device, wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::CopySrc,
+ {
+ 42,
+ });
+
+ wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+ encoder.WriteBuffer(buffer, 0, nullptr, 0);
+ wgpu::CommandBuffer commands = encoder.Finish();
+ queue.Submit(1, &commands);
+
+ EXPECT_BUFFER_U32_EQ(42, buffer, 0);
+}
DAWN_INSTANTIATE_TEST(CommandEncoderTests,
D3D11Backend(),
D3D12Backend(),
Original Bug Report
GPU Process RCE via Dawn CommandIterator Desync in WriteBuffer(size=0)
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 security team.
Overview: A logic error in Dawn’s command stream handling allows a compromised renderer to cause a command iterator desync in the GPU process. By sending a WriteBuffer command with a size of zero, an unconsumed marker is left in the stream, leading to undefined behavior and potential Remote Code Execution (RCE) via type confusion in the D3D12, Metal, and OpenGL backends.
Affected files:
third_party/dawn/src/dawn/native/d3d12/CommandBufferD3D12.cppthird_party/dawn/src/dawn/native/metal/CommandBufferMTL.mmthird_party/dawn/src/dawn/native/opengl/CommandBufferGL.cppthird_party/dawn/src/dawn/native/CommandEncoder.cppthird_party/dawn/src/dawn/native/CommandAllocator.h
Estimated timestamp from git blame: 2025-12-30
Summary
A vulnerability in Dawn’s command stream handling allows a compromised renderer to cause a command iterator desync in the GPU process. When CommandEncoder.WriteBuffer is called with a size of 0, an internal kAdditionalData marker is written to the command stream but is not properly consumed by the D3D12, Metal, and OpenGL backends during execution. This causes the next iteration of the command loop to interpret the marker as a command ID, triggering a DAWN_UNREACHABLE() fallback. Due to compiler optimizations on undefined behavior, this can deterministically redirect execution to a valid command handler, leading to type confusion, arbitrary pointer dereference, and potential Remote Code Execution (RCE).
Technical Details
When a renderer sends a CommandEncoderWriteBuffer command with size == 0 over the WebGPU wire protocol, the following sequence occurs:
- Encoding: In
CommandEncoder::APIWriteBuffer(CommandEncoder.cpp), the encoder callsallocator->AllocateData<uint8_t>(0). This helper unconditionally writes a 4-byte marker (detail::kAdditionalData/0xFFFFFFFE) into the native command stream, even when the size is 0. - Validation:
ValidateWriteBufferallows a size of 0 as it is a valid WebGPU operation. - Backend Execution Flaw: During command recording in the D3D12 (
CommandBufferD3D12.cpp), Metal (CommandBufferMTL.mm), and OpenGL (CommandBufferGL.cpp) backends, the loop encounters theWriteBuffercommand. The code checksif (size == 0)and executescontinue;. This critically skips the call tomCommands.NextData<uint8_t>(size), failing to consume thekAdditionalDatamarker from the stream. - Desynchronization: On the next iteration of the
while (mCommands.NextCommandId(&type))loop, the iterator reads the unconsumed0xFFFFFFFEmarker as the next command ID. In the C++Commandenum, this value is interpreted as-2. - Undefined Behavior: The
switch (type)statement does not have a case for-2and falls to thedefaultcase, which callsDAWN_UNREACHABLE(). In release builds, this macro expands to__builtin_unreachable()or__assume(false). - Exploitation via Optimization: Because
__builtin_unreachable()tells the compiler the path is impossible, the compiler optimizes the sparseswitchstatement (e.g., jump tables) without bounds checks. Executing-2through this optimized switch deterministically forces execution into a valid, attacker-selected command case (e.g.,Command::BeginRenderPass). - Type Confusion & RCE: The incorrectly reached handler calls
NextCommand<BeginRenderPassCmd>(). BecauseBeginRenderPassCmdis 8-byte aligned, the internalAlignPtrskips the 4-byte ID of the actual next command in the stream. The iterator lands perfectly on the attacker-controlled data payload of the next command (e.g.,SetImmediatesdata). This raw data is interpreted as theBeginRenderPassCmdstruct, which contains unprotected raw pointers insideRef<T>members. Dereferencing these forged pointers during execution leads to vtable hijacking and immediate RCE in the GPU process.
Note: The Vulkan backend is partially protected because its default case uses break instead of DAWN_UNREACHABLE(), and D3D11 is protected because it explicitly returns an error.
Potential Reproduction Steps
Please note these are suggested steps, as we do not have a working Proof of Concept.
- From a compromised renderer, serialize a
CommandEncoderWriteBufferwire command withsize=0. - Immediately follow this command in the wire stream with a command that accepts arbitrary data (e.g.,
RenderPassEncoderSetImmediates). - Structure the arbitrary data payload to perfectly mimic a Dawn command struct (e.g.,
BeginRenderPassCmd), containing a forgedRef<AttachmentState>pointer targeting a fake vtable in renderer-sprayed memory. - Submit the command buffer. The GPU process will hit
DAWN_UNREACHABLE(), desync, parse the fake struct, dereference the forged pointer, and execute the ROP chain.
Suggested Fix
Remove the early continue in the backend execution loops for WriteBuffer. The marker must be consumed regardless of the data size.
Change the backend implementations (D3D12, Metal, OpenGL) from:
if (size == 0) {
continue;
}
Buffer* dstBuffer = ToBackend(write->buffer.Get());
uint8_t* data = mCommands.NextData<uint8_t>(size);
To:
uint8_t* data = mCommands.NextData<uint8_t>(size);
if (size == 0) {
continue;
}
Buffer* dstBuffer = ToBackend(write->buffer.Get());
Additionally, the helper SkipCommand in Commands.cpp:423 shares this bug and should be similarly fixed.
Evaluated with Chrome root at commit: f200f57a19490707ff8bc7aa5de3cbc443a3afad
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.