CVE-2026-11101
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
CopyTests_MemoryLeaksrc/dawn/tests/end2end/CopyTests.cpp |
modified | |
TEST_Psrc/dawn/tests/end2end/CopyTests.cpp |
modified | |
forsrc/dawn/tests/end2end/CopyTests.cpp |
modified |
Files Changed
src/dawn/native/d3d12/CommandBufferD3D12.cppsrc/dawn/tests/end2end/CopyTests.cpp
Patch
From 9004229f3dd0e900e4bcf5e8865ada1644ca409b Mon Sep 17 00:00:00 2001 From: Antonio Maiorano <[email protected]> Date: Wed, 15 Apr 2026 08:13:07 -0700 Subject: [PATCH] [native][d3d12] Fix potential leak in T2B copy when temp buffer workaround active When D3D12UseTempBufferInDepthStencilTextureAndBufferCopyWithNonZeroBufferOffset is active, which happens on platforms that do not support programmable sampler positions, and a T2B copy is made that triggers the use of this workaround, it was possible to extract uninitialized data from the padding bytes of texture to temp buffer copy. This CL fixes this by making sure to initialize the temp buffer used in the copy. Bug: 500443031 Change-Id: Id3a9912c76b438568de28a8b5a206d2551509516 Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/302635 Reviewed-by: Loko Kung <[email protected]> Commit-Queue: Antonio Maiorano <[email protected]> --- diff --git a/src/dawn/native/d3d12/CommandBufferD3D12.cpp b/src/dawn/native/d3d12/CommandBufferD3D12.cpp index f3e1cdd..8945ce1 100644 --- a/src/dawn/native/d3d12/CommandBufferD3D12.cpp +++ b/src/dawn/native/d3d12/CommandBufferD3D12.cpp @@ -238,7 +238,7 @@ BufferCopy bufferCopy; bufferCopy.buffer = tempBuffer; bufferCopy.offset = 0; - bufferCopy.blocksPerRow = blockInfo.BytesToBlocks(bytesPerRow); + bufferCopy.blocksPerRow = blocksPerRow; bufferCopy.rowsPerImage = rowsPerImage; // Copy from source texture into tempBuffer @@ -297,6 +297,8 @@ DAWN_ASSERT(tempBuffer->GetVA() % D3D12_TEXTURE_DATA_PLACEMENT_ALIGNMENT == 0); auto scopedUseStaging = tempBuffer->UseInternal(); + DAWN_TRY(tempBuffer->EnsureDataInitialized(recordingContext)); + BufferCopy tempBufferCopy; tempBufferCopy.buffer = tempBuffer; tempBufferCopy.offset = 0; diff --git a/src/dawn/tests/end2end/CopyTests.cpp b/src/dawn/tests/end2end/CopyTests.cpp index a9ee5f2..cd60af6 100644 --- a/src/dawn/tests/end2end/CopyTests.cpp +++ b/src/dawn/tests/end2end/CopyTests.cpp @@ -4074,5 +4074,115 @@ VulkanBackend(), WebGPUBackend()); +class CopyTests_MemoryLeak : public DawnTest {}; + +// Test that reproduced a memory leak triggered by the D3D12 temporary buffer workaround +// (D3D12UseTempBufferInDepthStencilTextureAndBufferCopyWithNonZeroBufferOffset) for depth/stencil +// texture-to-buffer copies with a non-zero buffer offset. See crbug.com/500443031 +TEST_P(CopyTests_MemoryLeak, T2BLeakUninitializedPadding) { + // Dirty the GPU heap with a recognizable pattern. + // Use a large enough size to likely hit the same heap as the upcoming temporary buffer. + { + constexpr uint64_t kDirtySize = 64 * 1024; + constexpr uint32_t kPattern = 0xDEADBEEF; + + wgpu::BufferDescriptor descriptor; + descriptor.size = kDirtySize; + descriptor.usage = wgpu::BufferUsage::CopySrc | wgpu::BufferUsage::CopyDst; + wgpu::Buffer buffer = device.CreateBuffer(&descriptor); + + std::vector<uint32_t> data(kDirtySize / sizeof(uint32_t), kPattern); + queue.WriteBuffer(buffer, 0, data.data(), data.size() * sizeof(uint32_t)); + + // Submit and wait for idle to ensure the data is written and then the buffer can be freed. + queue.Submit(0, nullptr); + WaitForAllOperations(); + } + + // Initialize the texture with a known value. + constexpr float kClearDepthValue = 0.5f; + constexpr auto kTexFormat = wgpu::TextureFormat::Depth16Unorm; + constexpr uint32_t kTexWidth = 1; + constexpr uint32_t kTexHeight = 2; + + // Create a 1x2 depth texture and clear it to kClearDepthValue. If we copy a 1x2 texture, we get + // padding between row 0 and row 1. We set bytesPerRow high when copying to force padding bytes + // on row 0. + wgpu::Texture texture; + { + wgpu::TextureDescriptor texDesc = {}; + texDesc.size = {kTexWidth, kTexHeight, 1}; + texDesc.format = kTexFormat; + texDesc.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc; + texture = device.CreateTexture(&texDesc); + + utils::ComboRenderPassDescriptor renderPassDesc({}, texture.CreateView()); + renderPassDesc.UnsetDepthStencilLoadStoreOpsForFormat(kTexFormat); + renderPassDesc.cDepthStencilAttachmentInfo.depthClearValue = kClearDepthValue; + renderPassDesc.cDepthStencilAttachmentInfo.depthLoadOp = wgpu::LoadOp::Clear; + + wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); + wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPassDesc); + pass.End(); + wgpu::CommandBuffer commands = encoder.Finish(); + queue.Submit(1, &commands); + WaitForAllOperations(); + } + + // Create a destination buffer with large bytesPerRow to maximize padding. + // 256KB padding per row (we only have 1 row though) + constexpr uint32_t kBytesPerRow = 256 * 1024; + // The D3D12UseTempBufferInDepthStencilTextureAndBufferCopyWithNonZeroBufferOffset workaround + // triggers when offset is not a multiple of 512. + constexpr uint32_t kOffset = 4; + + wgpu::Buffer destinationBuffer; + const uint32_t destinationBufferSize = kOffset + kBytesPerRow * kTexHeight; + { + wgpu::BufferDescriptor bufferDesc = {}; + bufferDesc.size = destinationBufferSize; + bufferDesc.usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::MapRead; + destinationBuffer = device.CreateBuffer(&bufferDesc); + } + + // Perform the copy texture to buffer + { + wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); + wgpu::TexelCopyTextureInfo srcInfo = utils::CreateTexelCopyTextureInfo( + texture, 0, {0, 0, 0}, wgpu::TextureAspect::DepthOnly); + wgpu::TexelCopyBufferInfo dstInfo = + utils::CreateTexelCopyBufferInfo(destinationBuffer, kOffset, kBytesPerRow, kTexHeight); + wgpu::Extent3D copySize = {kTexWidth, kTexHeight, 1}; + encoder.CopyTextureToBuffer(&srcInfo, &dstInfo, ©Size); + wgpu::CommandBuffer commands = encoder.Finish(); + queue.Submit(1, &commands); + } + + // Map and inspect the padding. + { + MapAsyncAndWait(destinationBuffer, wgpu::MapMode::Read, 0, destinationBufferSize); + const uint8_t* readbackData = + static_cast<const uint8_t*>(destinationBuffer.GetConstMappedRange()); + + // The first texel is at ptr[kOffset]. Depth16Unorm is 2 bytes. + // Row 0 texel: ptr[kOffset] ... ptr[kOffset + 1] + // Padding after row 0: ptr[kOffset + 2] ... ptr[kOffset + kBytesPerRow - 1] + // Row 1 texel: ptr[kOffset + kBytesPerRow] ... ptr[kOffset + kBytesPerRow + 1] + + for (uint32_t i = kOffset + 2; i < kOffset + kBytesPerRow; ++i) { + ASSERT_EQ(readbackData[i], 0u); + } + + destinationBuffer.Unmap(); + } +} + +DAWN_INSTANTIATE_TEST(CopyTests_MemoryLeak, + D3D12Backend({ + // clang-format off + "d3d12_use_temp_buffer_in_depth_stencil_texture_and_buffer_copy_with_non_zero_buffer_offset", + // clang-format on + })); + } // anonymous namespace } // namespace dawn
Regression Test / PoC
diff --git a/src/dawn/tests/end2end/CopyTests.cpp b/src/dawn/tests/end2end/CopyTests.cpp
index a9ee5f2..cd60af6 100644
--- a/src/dawn/tests/end2end/CopyTests.cpp
+++ b/src/dawn/tests/end2end/CopyTests.cpp
@@ -4074,5 +4074,115 @@
VulkanBackend(),
WebGPUBackend());
+class CopyTests_MemoryLeak : public DawnTest {};
+
+// Test that reproduced a memory leak triggered by the D3D12 temporary buffer workaround
+// (D3D12UseTempBufferInDepthStencilTextureAndBufferCopyWithNonZeroBufferOffset) for depth/stencil
+// texture-to-buffer copies with a non-zero buffer offset. See crbug.com/500443031
+TEST_P(CopyTests_MemoryLeak, T2BLeakUninitializedPadding) {
+ // Dirty the GPU heap with a recognizable pattern.
+ // Use a large enough size to likely hit the same heap as the upcoming temporary buffer.
+ {
+ constexpr uint64_t kDirtySize = 64 * 1024;
+ constexpr uint32_t kPattern = 0xDEADBEEF;
+
+ wgpu::BufferDescriptor descriptor;
+ descriptor.size = kDirtySize;
+ descriptor.usage = wgpu::BufferUsage::CopySrc | wgpu::BufferUsage::CopyDst;
+ wgpu::Buffer buffer = device.CreateBuffer(&descriptor);
+
+ std::vector<uint32_t> data(kDirtySize / sizeof(uint32_t), kPattern);
+ queue.WriteBuffer(buffer, 0, data.data(), data.size() * sizeof(uint32_t));
+
+ // Submit and wait for idle to ensure the data is written and then the buffer can be freed.
+ queue.Submit(0, nullptr);
+ WaitForAllOperations();
+ }
+
+ // Initialize the texture with a known value.
+ constexpr float kClearDepthValue = 0.5f;
+ constexpr auto kTexFormat = wgpu::TextureFormat::Depth16Unorm;
+ constexpr uint32_t kTexWidth = 1;
+ constexpr uint32_t kTexHeight = 2;
+
+ // Create a 1x2 depth texture and clear it to kClearDepthValue. If we copy a 1x2 texture, we get
+ // padding between row 0 and row 1. We set bytesPerRow high when copying to force padding bytes
+ // on row 0.
+ wgpu::Texture texture;
+ {
+ wgpu::TextureDescriptor texDesc = {};
+ texDesc.size = {kTexWidth, kTexHeight, 1};
+ texDesc.format = kTexFormat;
+ texDesc.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc;
+ texture = device.CreateTexture(&texDesc);
+
+ utils::ComboRenderPassDescriptor renderPassDesc({}, texture.CreateView());
+ renderPassDesc.UnsetDepthStencilLoadStoreOpsForFormat(kTexFormat);
+ renderPassDesc.cDepthStencilAttachmentInfo.depthClearValue = kClearDepthValue;
+ renderPassDesc.cDepthStencilAttachmentInfo.depthLoadOp = wgpu::LoadOp::Clear;
+
+ wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+ wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPassDesc);
+ pass.End();
+ wgpu::CommandBuffer commands = encoder.Finish();
+ queue.Submit(1, &commands);
+ WaitForAllOperations();
+ }
+
+ // Create a destination buffer with large bytesPerRow to maximize padding.
+ // 256KB padding per row (we only have 1 row though)
+ constexpr uint32_t kBytesPerRow = 256 * 1024;
+ // The D3D12UseTempBufferInDepthStencilTextureAndBufferCopyWithNonZeroBufferOffset workaround
+ // triggers when offset is not a multiple of 512.
+ constexpr uint32_t kOffset = 4;
+
+ wgpu::Buffer destinationBuffer;
+ const uint32_t destinationBufferSize = kOffset + kBytesPerRow * kTexHeight;
+ {
+ wgpu::BufferDescriptor bufferDesc = {};
+ bufferDesc.size = destinationBufferSize;
+ bufferDesc.usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::MapRead;
+ destinationBuffer = device.CreateBuffer(&bufferDesc);
+ }
+
+ // Perform the copy texture to buffer
+ {
+ wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+ wgpu::TexelCopyTextureInfo srcInfo = utils::CreateTexelCopyTextureInfo(
+ texture, 0, {0, 0, 0}, wgpu::TextureAspect::DepthOnly);
+ wgpu::TexelCopyBufferInfo dstInfo =
+ utils::CreateTexelCopyBufferInfo(destinationBuffer, kOffset, kBytesPerRow, kTexHeight);
+ wgpu::Extent3D copySize = {kTexWidth, kTexHeight, 1};
+ encoder.CopyTextureToBuffer(&srcInfo, &dstInfo, ©Size);
+ wgpu::CommandBuffer commands = encoder.Finish();
+ queue.Submit(1, &commands);
+ }
+
+ // Map and inspect the padding.
+ {
+ MapAsyncAndWait(destinationBuffer, wgpu::MapMode::Read, 0, destinationBufferSize);
+ const uint8_t* readbackData =
+ static_cast<const uint8_t*>(destinationBuffer.GetConstMappedRange());
+
+ // The first texel is at ptr[kOffset]. Depth16Unorm is 2 bytes.
+ // Row 0 texel: ptr[kOffset] ... ptr[kOffset + 1]
+ // Padding after row 0: ptr[kOffset + 2] ... ptr[kOffset + kBytesPerRow - 1]
+ // Row 1 texel: ptr[kOffset + kBytesPerRow] ... ptr[kOffset + kBytesPerRow + 1]
+
+ for (uint32_t i = kOffset + 2; i < kOffset + kBytesPerRow; ++i) {
+ ASSERT_EQ(readbackData[i], 0u);
+ }
+
+ destinationBuffer.Unmap();
+ }
+}
+
+DAWN_INSTANTIATE_TEST(CopyTests_MemoryLeak,
+ D3D12Backend({
+ // clang-format off
+ "d3d12_use_temp_buffer_in_depth_stencil_texture_and_buffer_copy_with_non_zero_buffer_offset",
+ // clang-format on
+ }));
+
} // anonymous namespace
} // namespace dawn
Original Bug Report
Potential uninitialized GPU memory leak in Dawn D3D12 T2B copies via workaround buffer
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 potential vulnerability in the Dawn D3D12 backend allows leaking uninitialized GPU heap memory. A temporary buffer used as a workaround for depth/stencil texture-to-buffer copies is not zero-initialized before use. Because texture copies only overwrite valid texels, uninitialized VRAM in the padding regions is copied to the user’s destination buffer.
Affected files:
third_party/dawn/src/dawn/native/d3d12/CommandBufferD3D12.cppthird_party/dawn/src/dawn/native/d3d12/PhysicalDeviceD3D12.cppthird_party/dawn/src/dawn/native/d3d12/BufferD3D12.cpp
Estimated timestamp from git blame: 2026-01-23
Summary
A potential vulnerability in the Dawn D3D12 backend allows an attacker to leak uninitialized GPU heap memory. When the D3D12UseTempBufferInDepthStencilTextureAndBufferCopyWithNonZeroBufferOffset workaround is active, Dawn uses an internal temporary buffer for depth/stencil texture-to-buffer copies. This temporary buffer is allocated from a non-zeroed GPU heap and is not explicitly initialized before being partially filled with texture data and then copied in its entirety to a user-visible buffer. This results in the leakage of stale GPU memory contents, including padding regions between rows and images, which can be read by web content.
Vulnerability Details
On D3D12 hardware lacking programmable sample positions, Dawn natively enables the Toggle::D3D12UseTempBufferInDepthStencilTextureAndBufferCopyWithNonZeroBufferOffset workaround. When a WebGPU copyTextureToBuffer() call is made from a depth/stencil texture to a buffer with an offset that is not a multiple of 512, ShouldCopyUsingTemporaryBuffer() evaluates to true.
The execution routes to RecordBufferTextureCopyWithTemporaryBuffer() (in third_party/dawn/src/dawn/native/d3d12/CommandBufferD3D12.cpp), which exhibits the following flow:
- A temporary buffer (
tempBuffer) is created. Its size is calculated to accommodate the requested texels plus any row/image padding introduced by the user’sbytesPerRowandrowsPerImagearguments. - Because Dawn typically enables the
Toggle::D3D12CreateNotZeroedHeaptoggle on D3D12, this new buffer is backed by raw, uninitialized GPU memory containing stale data. - The code calls
tempBuffer->UseInternal()but critically fails to calltempBuffer->EnsureDataInitialized(). The buffer remains filled with uninitialized VRAM. - A texture-to-buffer copy is recorded (
RecordBufferTextureCopy), translating to D3D12’sCopyTextureRegion. This operation correctly respects texture layout rules and only writes to the actual texel footprints, leaving the extensive padding regions insidetempBuffercompletely untouched. - Finally,
commandList->CopyBufferRegion()is used to copy the entiretempBufferlinearly into the user’s destination buffer.
Because the destination buffer can be mapped for reading via JavaScript, an attacker can intentionally configure a massive bytesPerRow to create huge uninitialized padding gaps, and then extract cross-origin VRAM data from those gaps.
Suggested Reproduction Steps
Note: Our tooling agent does not have the ability to run code, so these are potential steps based on source code analysis.
- Use a Windows system with a D3D12 GPU where
ProgrammableSamplePositionsTier == 0(e.g., an older Intel iGPU). - Using WebGPU, create a depth/stencil
GPUTextureand a destinationGPUBufferwithMAP_READ | COPY_DSTusage. - Encode a
copyTextureToBuffer()command from the texture to the buffer. - Set
offsetto 4 (valid for depth/stencil, but triggers the workaround because it is not a multiple of 512). SetbytesPerRowto a very large value (e.g., 262144) to maximize padding size. - Submit the command and wait for execution.
- Map the destination buffer for reading and inspect the bytes in the padding regions between the copied rows. These regions will contain leaked GPU memory data.
Suggested Fix
In third_party/dawn/src/dawn/native/d3d12/CommandBufferD3D12.cpp, modify RecordBufferTextureCopyWithTemporaryBuffer to explicitly ensure the temporary buffer is initialized before it is used as a copy destination. Add DAWN_TRY(tempBuffer->EnsureDataInitializedAsDestination(recordingContext, ...)); or simply DAWN_TRY(tempBuffer->EnsureDataInitialized(recordingContext)); immediately after creating the tempBuffer.
Evaluated with Chrome root at commit: 137d451a126685dd5010e6609db9f6d4a78d8234
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.