CVE-2026-10921
Overview
Files Changed
src/dawn/native/Commands.cppsrc/dawn/native/Commands.h
Patch
From a9185ea4ddcc586e2e0ed10fda490f5eb0c79561 Mon Sep 17 00:00:00 2001 From: Corentin Wallez <[email protected]> Date: Thu, 23 Apr 2026 10:46:32 -0700 Subject: [PATCH] [dawn][native] Use size_t to store string command lengths. Fixed: 499159695 Change-Id: Ia811965d2d9813eaf73341f9238e816663e5dd21 Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/304695 Commit-Queue: Corentin Wallez <[email protected]> Reviewed-by: Loko Kung <[email protected]> --- diff --git a/src/dawn/native/Commands.cpp b/src/dawn/native/Commands.cpp index 2e538b1..05d9afa 100644 --- a/src/dawn/native/Commands.cpp +++ b/src/dawn/native/Commands.cpp @@ -435,9 +435,9 @@ } } -const char* AddNullTerminatedString(CommandAllocator* allocator, StringView s, uint32_t* length) { +const char* AddNullTerminatedString(CommandAllocator* allocator, StringView s, size_t* length) { std::string_view view = s; - *length = static_cast<uint32_t>(view.length()); + *length = view.length(); // Include extra null-terminator character. The string_view may not be null-terminated. It also // may already have a null-terminator inside of it, in which case adding the null-terminator is diff --git a/src/dawn/native/Commands.h b/src/dawn/native/Commands.h index c975fba..a0f9af0 100644 --- a/src/dawn/native/Commands.h +++ b/src/dawn/native/Commands.h @@ -342,7 +342,7 @@ }; struct InsertDebugMarkerCmd { - uint32_t length; + size_t length; }; struct PixelLocalStorageBarrierCmd {}; @@ -350,7 +350,7 @@ struct PopDebugGroupCmd {}; struct PushDebugGroupCmd { - uint32_t length; + size_t length; }; struct ResolveQuerySetCmd { @@ -465,7 +465,7 @@ void SkipCommand(CommandIterator* commands, Command type); // Helper function to copy a wgpu::StringView into a safely null-terminated C-string in commands. -const char* AddNullTerminatedString(CommandAllocator* allocator, StringView s, uint32_t* length); +const char* AddNullTerminatedString(CommandAllocator* allocator, StringView s, size_t* length); } // namespace dawn::native
Original Bug Report
GPU Process RCE via Integer Overflow in Dawn Command Iterator
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: An integer overflow exists in Dawn’s command processing when handling large debug marker strings. By supplying a string of length 0xFFFFFFFF, an attacker can desynchronize the command iterator. This causes the execution loop to interpret attacker-controlled string data as command IDs, triggering an unreachable code path that can be leveraged for Remote Code Execution.
Affected files:
third_party/dawn/src/dawn/native/Commands.cppthird_party/dawn/src/dawn/native/d3d12/CommandBufferD3D12.cppthird_party/dawn/src/dawn/native/vulkan/CommandBufferVk.cppthird_party/dawn/src/dawn/native/CommandAllocator.hthird_party/dawn/src/dawn/native/Commands.h
Estimated timestamp from git blame: 2025-11-17
Description
A potential integer overflow vulnerability exists in Dawn’s handling of debug marker and group wire messages (e.g., CommandEncoderInsertDebugMarker). This vulnerability can be triggered to corrupt the command stream iterator and execute attacker-controlled data as a function pointer jump, leading to potential Remote Code Execution (RCE) in the GPU process.
When a large string is provided to AddNullTerminatedString (in third_party/dawn/src/dawn/native/Commands.cpp), the string’s length is explicitly cast and stored in a 32-bit integer: cmd->length = static_cast<uint32_t>(view.length());. However, the memory allocation uses 64-bit arithmetic: allocator->AllocateData<char>(view.length() + 1);.
If an attacker supplies a string of exactly 0xFFFFFFFF (4GB) non-NUL bytes, the 4GB block is successfully allocated and populated. Later, when the backend command execution loops (e.g., in CommandBufferD3D12::RecordComputePass or FreeCommands) attempt to skip this data, they call:
mCommands.NextData<char>(cmd->length + 1);
Since cmd->length is a uint32_t (0xFFFFFFFF), the addition of 1 causes a 32-bit integer overflow, resulting in 0. Consequently, NextData<char>(0) is called. The command iterator consumes the internal kAdditionalData marker but advances its internal pointer by 0 bytes, failing to skip the 4GB string data.
On the next iteration, the command loop reads the next 4 bytes to determine the next Command ID. Because the pointer was not advanced, it reads directly from the attacker-controlled string data. The resulting invalid command ID falls through to the default: case in the execution loop’s switch statement, which calls DAWN_UNREACHABLE().
In standard Chromium release builds, DAWN_UNREACHABLE() expands to __builtin_unreachable(). Compilers (like Clang) use this as a hint to elide bounds checks on switch jump tables. As a result, the attacker-controlled command ID is used as an out-of-bounds index for the jump table, enabling control-flow hijacking.
Potential Attacker Steps
Note: Our tooling does not yet have the ability to run code, so these are theoretical steps an attacker might follow based on code analysis.
- Bypass Client Constraints: From a compromised renderer, manually construct Dawn chunked wire commands to bypass client-side buffer serializers, sending an
InsertDebugMarkercommand with a total size of0xFFFFFFFFbytes of non-NUL data. - Server Assembly: The GPU process’s
ChunkedCommandHandlerallocates a 4GB buffer (which succeeds on 64-bit systems with sufficient memory) and reconstructs the chunked payload. - Command Deserialization: The Dawn wire server deserializes the
WGPUStringView, preserving the0xFFFFFFFFlength (as it is bounded byWGPU_STRLEN, which isSIZE_MAX). - Trigger Overflow: The command is recorded into the native command buffer. The string is stored, but the
lengthfield in the command struct truncates. - Execution Desync: Upon submission, the GPU process attempts to execute the command buffer. The
cmd->length + 1overflow causes the iterator to stall exactly at the attacker’s payload. - Control Flow Hijack: The switch statement reads the attacker’s payload as a command ID, falling through to
DAWN_UNREACHABLE(). The compiler-optimized jump table reads an out-of-bounds function pointer dictated by the attacker’s fake ID, granting RCE.
Suggested Fix
- Validate String Lengths: Reject any
StringViewwith a length exceeding a reasonable upper bound (e.g., a few megabytes) early inNormalizeMessageStringor during Wire deserialization. - Use Checked Arithmetic: Modify
CommandAllocator::NextDataand iterator offset calculations to usebase::CheckedNumericto prevent overflow wrap-around. - Fix Struct Types: Change the
lengthfield in structures likeInsertDebugMarkerCmdandPushDebugGroupCmdtosize_trather thanuint32_t.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
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.