CVE-2026-8582
Overview
Background
- `BlitTextureToBuffer`
- Dawn’s compute-shader workaround that copies a texture into a buffer on backends lacking a native path.
- `SetInitialized`
- marks a buffer’s data as initialized so Dawn can skip lazy zero-clearing on first use.
- `fullSizeCopy`
- a flag meaning the planned copy overwrites the whole destination buffer, in which case pre-clearing would be redundant.
- `DAWN_TRY` / `DAWN_TRY_ASSIGN`
- macros that return early from the encoding function when an internal operation (such as
device->CreateBuffer) fails.
Root Cause Analysis
In the pre-fix BlitTextureToBuffer (src/dawn/native/BlitTextureToBuffer.cpp), the destination buffer was marked initialized very early: at lines 1166-1167, right after computing fullSizeCopy, the code ran dst.buffer->SetInitialized(fullSizeCopy || dst.buffer->IsInitialized()). Only afterward did the function perform the fallible work that can abort the encode — for example the intermediate-buffer path at line 1199 (DAWN_TRY_ASSIGN(destinationBuffer, device->CreateBuffer(&descriptor))) and later DAWN_TRY_ASSIGN calls for the source view and bind groups (lines 1324, 1327, 1329). The invariant that must hold is “a buffer is only marked initialized once the operation that will actually fill it is guaranteed to be recorded.” Because SetInitialized(true) ran first, a failure in a later device->Create* step would return early with the buffer already flagged initialized even though no clearing or copy was recorded, so a subsequent read could observe stale, un-cleared memory.
The fix removes the early call at lines 1166-1167 and re-inserts the same SetInitialized(fullSizeCopy || dst.buffer->IsInitialized()) after those fallible creation steps (near line 1353), immediately before commandEncoder->BeginComputePass() at line 1356. This TIGHTENS ordering so the initialized flag is only set once the fallible setup has succeeded, while still being set before encoding so the clear can still be skipped when unneeded.
SetInitialized to after those allocations succeed.Attack Path
- Target a blit-workaround copy
Content issues a texture-to-buffer copy that routes through
BlitTextureToBufferand fully overwrites the destination, settingfullSizeCopy. (inferred) - Force an internal allocation failure
An internal
device->CreateBufferor similar step fails, triggering aDAWN_TRY/DAWN_TRY_ASSIGNearly return after the buffer was already marked initialized (pre-fixBlitTextureToBuffer.cpp:1166-1167,:1199). (inferred) - Abandon the encode The failing encode is discarded so no clear/copy is ever recorded for the buffer. (inferred)
- Read uninitialized memory A later use of the buffer skips lazy clearing because it is flagged initialized, exposing previous GPU memory contents. (inferred)
Impact Assessment
(inferred).Files Changed
src/dawn/native/BlitTextureToBuffer.cpp
Audit Directions
- State-before-fallible-opFlag code that sets a success/initialized flag before subsequent operations that can fail and early-return, leaving the flag inconsistent with reality.
- Lazy-clear bypassAudit any
SetInitialized(true)that governs skipping zero-initialization to ensure the actual fill is guaranteed before the flag is set. - Early-return error macrosWhere
DAWN_TRY/DAWN_TRY_ASSIGN(or equivalent) can bail mid-function, check that no externally visible object state was committed earlier in the function.
Patch
From 8d862f1501d19ebe7e1e030fc71158a015d0db64 Mon Sep 17 00:00:00 2001
From: Shrek Shao <shrekshao@google.com>
Date: Tue, 31 Mar 2026 01:18:05 -0700
Subject: [PATCH] BlitTextureToBuffer.cpp: slightly defer dst.buffer->SetInitialized call
This CL slightly makes the code nicer and more correct.
Moves `dst.buffer->SetInitialized` to
- After several `device->Create*` that may throw error so the internal
mIsDataInitialized is less likely leaking.
- Before the commandEncoder encoding so it can still skip the extra
clearing when not needed.
Bug: 497594413
Change-Id: Ic7e5e14d5956500edbda017cf9eba910b15d777c
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/300316
Commit-Queue: Corentin Wallez <cwallez@chromium.org>
Auto-Submit: Shrek Shao <shrekshao@google.com>
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
---
diff --git a/src/dawn/native/BlitTextureToBuffer.cpp b/src/dawn/native/BlitTextureToBuffer.cpp
index b7c606a..2e84864 100644
--- a/src/dawn/native/BlitTextureToBuffer.cpp
+++ b/src/dawn/native/BlitTextureToBuffer.cpp
@@ -1163,8 +1163,6 @@
const bool fullSizeCopy = IsFullBufferOverwrittenInTextureToBufferCopy(
src, dst, blockInfo.ToTexel(copyExtent).ToExtent3D());
- // Skip clearing the buffer if this is full size copy.
- dst.buffer->SetInitialized(fullSizeCopy || dst.buffer->IsInitialized());
Ref<BufferBase> destinationBuffer = dst.buffer.Get();
const uint64_t numBytesToCopy =
@@ -1353,6 +1351,9 @@
UsageValidationMode::Internal));
}
+ // Skip clearing the buffer if this is full size copy.
+ dst.buffer->SetInitialized(fullSizeCopy || dst.buffer->IsInitialized());
+
Ref<ComputePassEncoder> pass = commandEncoder->BeginComputePass();
pass->APISetPipeline(pipeline.Get());
pass->APISetBindGroup(0, bindGroup0.Get());
Original Bug Report
GPU memory information leak via premature SetInitialized in BlitTextureToBuffer
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: Dawn’s BlitTextureToBuffer prematurely marks destination buffers as initialized before performing fallible internal operations like memory allocation. If an allocation fails (e.g., OOM) and the CommandEncoder is abandoned, the error is silently discarded while the buffer remains marked as initialized, bypassing LazyClearResourceOnFirstUse and allowing an attacker to map uninitialized GPU memory.
Affected files:
third_party/dawn/src/dawn/native/BlitTextureToBuffer.cppthird_party/dawn/src/dawn/native/Buffer.cppthird_party/dawn/src/dawn/native/EncodingContext.cpp
Estimated timestamp from git blame: 2024-07-02
Description
In third_party/dawn/src/dawn/native/BlitTextureToBuffer.cpp, the BlitTextureToBuffer function is used as a workaround for certain texture-to-buffer copies on backends like OpenGL, GLES, and D3D11. This function prematurely updates the initialized state of the destination buffer before performing fallible internal operations.
At line 1167 in BlitTextureToBuffer.cpp:
dst.buffer->SetInitialized(fullSizeCopy || dst.buffer->IsInitialized());
If fullSizeCopy is true (meaning the copy operation is expected to overwrite the entire buffer), the destination buffer is immediately marked as initialized.
Following this, several internal operations occur that can fail, most notably device->CreateBuffer (e.g., at lines 1199 and 1253) which may fail with an Out Of Memory (OOM) error if GPU memory is exhausted. When such an error occurs, the DAWN_TRY or DAWN_TRY_ASSIGN macros cause the function to return early. In the context of a CommandEncoder, this error is deferred into the EncodingContext.
If the attacker then releases the CommandEncoder without calling finish(), the deferred error is silently discarded during EncodingContext::Destroy(). No device-lost event occurs, and the destination buffer remains in the incorrectly marked ‘initialized’ state.
Because the recording of the BlitTextureToBuffer commands was aborted, the destination buffer’s contents are never actually updated or cleared. However, since the buffer is marked as initialized, subsequent operations (such as MapAsync or using the buffer as a source in another copy) will evaluate NeedsInitialization() as false. This skips the LazyClearResourceOnFirstUse step (verified in Buffer.cpp), which is intended to zero-initialize buffers before their first use. This allows an attacker to read uninitialized GPU memory, potentially containing sensitive data from other WebGPU contexts or previous allocations in the same GPU process.
Note: This analysis relies on source code review. The steps below outline a potential exploit strategy but have not been verified with a live Proof of Concept.
Potential Reproduction Steps
- Use a WebGPU-enabled browser on a platform using the OpenGLES or D3D11 backend.
- Create a
GPUBuffer(B) withCOPY_DST | MAP_READusage and a size matching a planned copy. - Create a
GPUTexture(T) with a format that triggers a blit workaround (e.g.,depth16unormon GLES). - Induce GPU memory pressure to ensure internal allocations fail (e.g., by allocating many large buffers until near the driver limit).
- Use a
GPUCommandEncoderto callcopyTextureToBuffer({texture: T}, {buffer: B}, ...)such that it covers the full buffer (fullSizeCopybecomes true). - Internal
CreateBuffercalls withinBlitTextureToBufferfail due to OOM. The error is deferred in the encoder’sEncodingContext. - Destroy/Release the
GPUCommandEncoderwithout callingfinish(). The deferred error is silently dropped duringEncodingContext::Destroy(). - Release the pressure buffers from step 4.
- Call
B.mapAsync(GPUMapMode.READ)and read the buffer’s contents viagetMappedRange(). The contents will be uninitialized GPU memory instead of zeros, asLazyClearResourceOnFirstUseis bypassed.
Suggested Fix
Ensure that the destination buffer is only marked as initialized after all fallible operations in BlitTextureToBuffer have succeeded, or defer the SetInitialized(true) call until the command is actually submitted or executed. Alternatively, track whether the initialization was performed by a specific command and revert the state if the command fails to encode or is discarded.
Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0
Results from 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.