CVE-2026-10968
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Psrc/dawn/tests/white_box/SharedBufferMemoryTests.cpp |
modified |
Files Changed
src/dawn/native/Buffer.cppsrc/dawn/tests/white_box/SharedBufferMemoryTests.cpp
Patch
From 63321f623f725e52bfc92675581cba683a4b98bd Mon Sep 17 00:00:00 2001 From: Antonio Maiorano <[email protected]> Date: Tue, 12 May 2026 15:11:27 -0700 Subject: [PATCH] [native][d3d12] SharedBufferMemory: Make Unmap after EndAccess fail Calling Unamp on a SBM after EndAccess should fail. Otherwise, a compromised renderer could take advantage of this to gain concurrent, unbarriered access to the SBM. Bug: 511758373 Change-Id: I3de036c0b71f05b2483fa5e0ae5ab7f543742197 Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/307916 Reviewed-by: Corentin Wallez <[email protected]> Commit-Queue: Antonio Maiorano <[email protected]> Reviewed-by: Loko Kung <[email protected]> --- diff --git a/src/dawn/native/Buffer.cpp b/src/dawn/native/Buffer.cpp index 74d4848..0206566 100644 --- a/src/dawn/native/Buffer.cpp +++ b/src/dawn/native/Buffer.cpp @@ -885,7 +885,7 @@ case BufferState::Unmapped: return {}; case BufferState::SharedMemoryNoAccess: - break; + return DAWN_VALIDATION_ERROR("%s unmapped without shared memory access.", this); case BufferState::PendingMap: case BufferState::Destroyed: // UnmapInternal() already handled waiting for PendingMap to be done so there must have diff --git a/src/dawn/tests/white_box/SharedBufferMemoryTests.cpp b/src/dawn/tests/white_box/SharedBufferMemoryTests.cpp index b7ec223f5..0f4a7c3 100644 --- a/src/dawn/tests/white_box/SharedBufferMemoryTests.cpp +++ b/src/dawn/tests/white_box/SharedBufferMemoryTests.cpp @@ -368,6 +368,27 @@ ASSERT_NE(state.fences[0], nullptr); } +// Validate that calling Unmap after EndAccess is an error +TEST_P(SharedBufferMemoryTests, UnmapAfterEndAccess) { + wgpu::SharedBufferMemory memory = GetParam().mBackend->CreateSharedBufferMemory( + device, wgpu::BufferUsage::CopySrc | wgpu::BufferUsage::CopyDst, kBufferSize); + + // Buffer state is SharedMemoryNoAccess + wgpu::Buffer buffer = memory.CreateBuffer(); + + // BeginAccess transitions buffer state from SharedMemoryNoAccess to Unmapped + wgpu::SharedBufferMemoryBeginAccessDescriptor beginDesc = {}; + beginDesc.initialized = true; + memory.BeginAccess(buffer, &beginDesc); + + // EndAccess transitions buffer state back to SharedMemoryNoAccess + wgpu::SharedBufferMemoryEndAccessState endState = {}; + memory.EndAccess(buffer, &endState); + + // Unmapping the buffer now should be an error + ASSERT_DEVICE_ERROR(buffer.Unmap()); +} + // Validate that calling BeginAccess twice produces an error. TEST_P(SharedBufferMemoryTests, EnsureNoDuplicateBeginAccessCalls) { wgpu::SharedBufferMemory memory =
Regression Test / PoC
diff --git a/src/dawn/tests/white_box/SharedBufferMemoryTests.cpp b/src/dawn/tests/white_box/SharedBufferMemoryTests.cpp
index b7ec223f5..0f4a7c3 100644
--- a/src/dawn/tests/white_box/SharedBufferMemoryTests.cpp
+++ b/src/dawn/tests/white_box/SharedBufferMemoryTests.cpp
@@ -368,6 +368,27 @@
ASSERT_NE(state.fences[0], nullptr);
}
+// Validate that calling Unmap after EndAccess is an error
+TEST_P(SharedBufferMemoryTests, UnmapAfterEndAccess) {
+ wgpu::SharedBufferMemory memory = GetParam().mBackend->CreateSharedBufferMemory(
+ device, wgpu::BufferUsage::CopySrc | wgpu::BufferUsage::CopyDst, kBufferSize);
+
+ // Buffer state is SharedMemoryNoAccess
+ wgpu::Buffer buffer = memory.CreateBuffer();
+
+ // BeginAccess transitions buffer state from SharedMemoryNoAccess to Unmapped
+ wgpu::SharedBufferMemoryBeginAccessDescriptor beginDesc = {};
+ beginDesc.initialized = true;
+ memory.BeginAccess(buffer, &beginDesc);
+
+ // EndAccess transitions buffer state back to SharedMemoryNoAccess
+ wgpu::SharedBufferMemoryEndAccessState endState = {};
+ memory.EndAccess(buffer, &endState);
+
+ // Unmapping the buffer now should be an error
+ ASSERT_DEVICE_ERROR(buffer.Unmap());
+}
+
// Validate that calling BeginAccess twice produces an error.
TEST_P(SharedBufferMemoryTests, EnsureNoDuplicateBeginAccessCalls) {
wgpu::SharedBufferMemory memory =
Original Bug Report
State machine flaw in Dawn BufferBase::Unmap() leads to potential synchronization bypass
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 state machine flaw in Dawn’s BufferBase::Unmap allows a buffer in the SharedMemoryNoAccess state to incorrectly transition to the Unmapped state. This bypasses state validation and synchronization logic, potentially enabling a compromised renderer to perform unauthorized concurrent access to shared GPU resources.
Affected files:
third_party/dawn/src/dawn/native/Buffer.cppthird_party/dawn/src/dawn/native/SharedResourceMemory.cppgpu/command_buffer/service/webgpu_decoder_impl.ccgpu/command_buffer/service/shared_image/d3d_image_backing.ccthird_party/dawn/src/dawn/native/d3d12/BufferD3D12.cpp
Estimated timestamp from git blame: 2025-03-25
Summary
A state machine vulnerability exists in Dawn’s BufferBase::Unmap() implementation. When a buffer is in the SharedMemoryNoAccess state (typically after a Shared Image has been dissociated), the Unmap function incorrectly falls through a switch statement, resulting in an unconditional transition to the Unmapped state. This bypasses state validation and synchronization logic intended for shared resources. A compromised renderer could potentially exploit this to perform unauthorized, unbarriered concurrent access to shared GPU resources, leading to cross-origin information leaks or driver-level undefined behavior.
Vulnerability Details
In third_party/dawn/src/dawn/native/Buffer.cpp, the BufferBase::Unmap(bool forDestroy) function contains the following logic:
switch (mState.load(std::memory_order::acquire)) {
// ... other cases ...
case BufferState::Unmapped:
return {};
case BufferState::SharedMemoryNoAccess:
break;
// ... error cases ...
}
mState.store(BufferState::Unmapped, std::memory_order::release);
When the state is SharedMemoryNoAccess, the break statement causes the execution to fall through to the unconditional mState.store(BufferState::Unmapped, ...) call at the end of the function.
Normally, the SharedMemoryNoAccess state indicates that the buffer’s access to shared memory has been terminated via EndAccess. Transitioning back to Unmapped (and subsequently to InUse) should only happen through BeginAccess, which enforces mutual exclusion and sets up necessary synchronization fences.
Potential Attack Scenario
A compromised renderer could potentially trigger this flaw through the following sequence of GPU command buffer operations:
- Associate Mailbox: The attacker issues an
AssociateMailboxForBufferImmediatecommand to the WebGPU decoder with a shared image mailbox. The decoder injects the buffer into the Dawn wire server, which retains a strong reference to the buffer object. - Dissociate Mailbox: The attacker sends
DissociateMailboxForBuffer. This callsEndAccesson the shared resource, which sets the buffer’s state toSharedMemoryNoAccess, drains the pending synchronization fences from the buffer, and releases the mutual exclusion lock on the Shared Image. - Bypass via Unmap: The attacker sends a
BufferUnmapwire command using the server-side ID retained in Step 1. This invokesBufferBase::APIUnmap(), which hits the vulnerable fall-through inBufferBase::Unmap()and erroneously sets the buffer state toUnmapped. - Concurrent Access: The attacker queues a GPU command (e.g.,
CopyBufferToBuffer) using the buffer.ValidateCanUseOnQueueNow()succeeds because the state isUnmapped(avoiding the validation error thatSharedMemoryNoAccesswould trigger). - Synchronization Bypass: When Dawn prepares the command,
SynchronizeBufferBeforeUseOnGPU()skips waiting on fences because they were already drained during Step 2. - Meanwhile, the attacker (or another process) legitimately begins access to the same Shared Image. This results in concurrent, unbarriered access to the same underlying GPU resource from different queues, violating synchronization guarantees and potentially leaking stale GPU memory.
(Note: These are potential steps based on code analysis; our tooling agent does not yet have the capability to execute a live proof of concept.)
Suggested Fix
In third_party/dawn/src/dawn/native/Buffer.cpp within BufferBase::Unmap(), the case BufferState::SharedMemoryNoAccess: should return a validation error rather than breaking out of the switch statement. For example:
case BufferState::SharedMemoryNoAccess:
return DAWN_VALIDATION_ERROR("%s cannot be unmapped without shared memory access.", this);
Evaluated with Chrome root at commit: eca8648a4e1cdfdda68c495a6003059fed641955
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.