Medium chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized Use in Dawn
DescriptionUninitialized Use in Dawn
ComponentDawn
Bug ClassUninitialized Memory
Tracker500443031
Fix commit9004229f3dd0 (dawn) +113/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
CopyTests_MemoryLeak
src/dawn/tests/end2end/CopyTests.cpp
modified
TEST_P
src/dawn/tests/end2end/CopyTests.cpp
modified
for
src/dawn/tests/end2end/CopyTests.cpp
modified

Files Changed

  • src/dawn/native/d3d12/CommandBufferD3D12.cpp
  • src/dawn/tests/end2end/CopyTests.cpp
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, &copySize);
+        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
Loading diff…

Regression Test / PoC

shipped with the fix
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, &copySize);
+        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
Loading diff…

Original Bug Report

reported by [email protected]

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.cpp
  • third_party/dawn/src/dawn/native/d3d12/PhysicalDeviceD3D12.cpp
  • third_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:

  1. A temporary buffer (tempBuffer) is created. Its size is calculated to accommodate the requested texels plus any row/image padding introduced by the user’s bytesPerRow and rowsPerImage arguments.
  2. Because Dawn typically enables the Toggle::D3D12CreateNotZeroedHeap toggle on D3D12, this new buffer is backed by raw, uninitialized GPU memory containing stale data.
  3. The code calls tempBuffer->UseInternal() but critically fails to call tempBuffer->EnsureDataInitialized(). The buffer remains filled with uninitialized VRAM.
  4. A texture-to-buffer copy is recorded (RecordBufferTextureCopy), translating to D3D12’s CopyTextureRegion. This operation correctly respects texture layout rules and only writes to the actual texel footprints, leaving the extensive padding regions inside tempBuffer completely untouched.
  5. Finally, commandList->CopyBufferRegion() is used to copy the entire tempBuffer linearly 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.

  1. Use a Windows system with a D3D12 GPU where ProgrammableSamplePositionsTier == 0 (e.g., an older Intel iGPU).
  2. Using WebGPU, create a depth/stencil GPUTexture and a destination GPUBuffer with MAP_READ | COPY_DST usage.
  3. Encode a copyTextureToBuffer() command from the texture to the buffer.
  4. Set offset to 4 (valid for depth/stencil, but triggers the workaround because it is not a multiple of 512). Set bytesPerRow to a very large value (e.g., 262144) to maximize padding size.
  5. Submit the command and wait for execution.
  6. 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.

View on issue tracker