Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in Dawn
DescriptionOut of bounds read in Dawn
ComponentDawn
Bug ClassOOB
Tracker520972775
Fix commit2c581d1e5286 (dawn) +105/-6
CISA KEVNot listed
Creditedsm1ee, ksw9722
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
if
src/dawn/native/IndirectDrawMetadata.cpp
modified
TEST_P
src/dawn/tests/end2end/RenderBundleTests.cpp
modified

Files Changed

  • src/dawn/native/IndirectDrawMetadata.cpp
  • src/dawn/native/IndirectDrawMetadata.h
  • src/dawn/tests/end2end/RenderBundleTests.cpp
From 2c581d1e5286b0d0530dcf5bd2127230ee7401b5 Mon Sep 17 00:00:00 2001
From: Brandon Jones <[email protected]>
Date: Tue, 09 Jun 2026 11:03:17 -0700
Subject: [PATCH] Fix OOB read when validating repeated bundles

Stops de-duplicating render bundles when added to a render pass so
that any indirect draws are properly counted and validated. Fixes
an issue where repeating the same bundle multiple times in a render
pass could cause an OOB read on the validated indirect draw
side-table.

Fixed: 520972775
Change-Id: I7ce9e64d19aef31bd1394fa9e18fca911e07f573
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/315036
Reviewed-by: Corentin Wallez <[email protected]>
Commit-Queue: Brandon Jones <[email protected]>
---

diff --git a/src/dawn/native/IndirectDrawMetadata.cpp b/src/dawn/native/IndirectDrawMetadata.cpp
index f748cef..db9eaac 100644
--- a/src/dawn/native/IndirectDrawMetadata.cpp
+++ b/src/dawn/native/IndirectDrawMetadata.cpp
@@ -214,11 +214,6 @@
 }
 
 void IndirectDrawMetadata::AddBundle(RenderBundleBase* bundle) {
-    auto [_, inserted] = mAddedBundles.insert(bundle);
-    if (!inserted) {
-        return;
-    }
-
     IndirectDrawIndex bundleIndirectDrawCount{0u};
     for (const auto& [config, validationInfo] :
          bundle->GetIndirectDrawMetadata().mIndexedIndirectBufferValidationInfo) {
diff --git a/src/dawn/native/IndirectDrawMetadata.h b/src/dawn/native/IndirectDrawMetadata.h
index 71d090b..63aa76b 100644
--- a/src/dawn/native/IndirectDrawMetadata.h
+++ b/src/dawn/native/IndirectDrawMetadata.h
@@ -215,7 +215,6 @@
 
   private:
     IndexedIndirectBufferValidationInfoMap mIndexedIndirectBufferValidationInfo;
-    absl::flat_hash_set<RenderBundleBase*> mAddedBundles;
 
     std::vector<IndirectMultiDraw> mMultiDraws;
 
diff --git a/src/dawn/tests/end2end/RenderBundleTests.cpp b/src/dawn/tests/end2end/RenderBundleTests.cpp
index 45cd3fa..cc963a2 100644
--- a/src/dawn/tests/end2end/RenderBundleTests.cpp
+++ b/src/dawn/tests/end2end/RenderBundleTests.cpp
@@ -470,6 +470,111 @@
     EXPECT_BUFFER_U32_EQ(3, counterRead, 0);
 }
 
+// Tests a bug outlined in crbug.com/520972775 where repeated execution of bundles with an indirect
+// draw could de-duplicate the draw calls when validating, causing the ValidatedIndirectDraw side
+// table to have too few entries.
+TEST_P(RenderBundleIndirectValidationTest, SinglePassRepeatedIndirectDraw) {
+    // Render Pass
+    utils::BasicRenderPass renderPass = utils::CreateBasicRenderPass(device, kRTSize, kRTSize);
+
+    // Index Buffers
+    wgpu::Buffer smallIdx = CreateIndexBuffer({0, 1, 2});
+
+    // Indirect buffer
+    wgpu::Buffer indirect = CreateIndirectBuffer({3, 1, 0, 0, 0});
+
+    // Buffers to use for simple fragment counter
+    uint32_t data[] = {0};
+    wgpu::Buffer counterBuffer = utils::CreateBufferFromData(
+        device, data, sizeof(uint32_t),
+        wgpu::BufferUsage::Storage | wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::CopySrc);
+    wgpu::Buffer counterRead = utils::CreateBufferFromData(
+        device, data, sizeof(uint32_t), wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::CopySrc);
+
+    // Pipeline
+    wgpu::ShaderModule module = utils::CreateShaderModule(device, R"(
+        struct Ctr { n: atomic<u32>, };
+        @group(0) @binding(0) var<storage, read_write> ctr: Ctr;
+
+        @vertex fn vs() -> @builtin(position) vec4f {
+            return vec4f(0.0, 0.0, 0.0, 1.0);
+        }
+        // Simply adds one to the simple fragment counter with each draw
+        @fragment fn fs() -> @location(0) vec4f {
+            atomicAdd(&ctr.n, 1u);
+            return vec4f(1.0, 0.0, 0.0, 1.0);
+        })");
+
+    utils::ComboRenderPipelineDescriptor descriptor;
+    descriptor.vertex.module = module;
+    descriptor.cFragment.module = module;
+    descriptor.primitive.topology = wgpu::PrimitiveTopology::PointList;
+    descriptor.cTargets[0].format = renderPass.colorFormat;
+    wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&descriptor);
+
+    // Bind group
+    wgpu::BindGroup bindGroup = utils::MakeBindGroup(device, pipeline.GetBindGroupLayout(0),
+                                                     {{0, counterBuffer, 0, sizeof(float)}});
+
+    // Render bundles
+    utils::ComboRenderBundleEncoderDescriptor desc = {};
+    desc.colorFormatCount = 1;
+    desc.cColorFormats[0] = renderPass.colorFormat;
+
+    wgpu::RenderBundle directRenderBundle;
+    {
+        wgpu::RenderBundleEncoder renderBundleEncoder = device.CreateRenderBundleEncoder(&desc);
+        renderBundleEncoder.SetPipeline(pipeline);
+        renderBundleEncoder.SetBindGroup(0, bindGroup);
+        renderBundleEncoder.DrawIndirect(indirect, 0);
+        directRenderBundle = renderBundleEncoder.Finish();
+    }
+
+    wgpu::RenderBundle indexedRenderBundle;
+    {
+        wgpu::RenderBundleEncoder renderBundleEncoder = device.CreateRenderBundleEncoder(&desc);
+        renderBundleEncoder.SetPipeline(pipeline);
+        renderBundleEncoder.SetBindGroup(0, bindGroup);
+        renderBundleEncoder.SetIndexBuffer(smallIdx, wgpu::IndexFormat::Uint32);
+        renderBundleEncoder.DrawIndexedIndirect(indirect, 0);
+        indexedRenderBundle = renderBundleEncoder.Finish();
+    }
+
+    //
+    // Bug - Same bundle executed twice in a single render pass.
+    //
+    {
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+
+        // Test bundles with DrawIndirect calls
+        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo);
+        pass.ExecuteBundles(1, &directRenderBundle);
+        pass.ExecuteBundles(1, &directRenderBundle);
+        pass.End();
+
+        wgpu::CommandBuffer commands = encoder.Finish();
+        queue.Submit(1, &commands);
+    }
+
+    {
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+
+        // Test bundles with DrawIndexedIndirect calls
+        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo);
+        pass.ExecuteBundles(1, &indexedRenderBundle);
+        pass.ExecuteBundles(1, &indexedRenderBundle);
+        pass.End();
+
+        // Copy the fragment counter results of both passes to the readback buffer.
+        encoder.CopyBufferToBuffer(counterBuffer, 0, counterRead, 0, 4);
+        wgpu::CommandBuffer commands = encoder.Finish();
+        queue.Submit(1, &commands);
+    }
+
+    // Each bundle should produce 3 fragments, for a total of 12.
+    EXPECT_BUFFER_U32_EQ(12, counterRead, 0);
+}
+
 DAWN_INSTANTIATE_TEST(RenderBundleIndirectValidationTest,
                       D3D11Backend(),
                       D3D12Backend(),
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/dawn/tests/end2end/RenderBundleTests.cpp b/src/dawn/tests/end2end/RenderBundleTests.cpp
index 45cd3fa..cc963a2 100644
--- a/src/dawn/tests/end2end/RenderBundleTests.cpp
+++ b/src/dawn/tests/end2end/RenderBundleTests.cpp
@@ -470,6 +470,111 @@
     EXPECT_BUFFER_U32_EQ(3, counterRead, 0);
 }
 
+// Tests a bug outlined in crbug.com/520972775 where repeated execution of bundles with an indirect
+// draw could de-duplicate the draw calls when validating, causing the ValidatedIndirectDraw side
+// table to have too few entries.
+TEST_P(RenderBundleIndirectValidationTest, SinglePassRepeatedIndirectDraw) {
+    // Render Pass
+    utils::BasicRenderPass renderPass = utils::CreateBasicRenderPass(device, kRTSize, kRTSize);
+
+    // Index Buffers
+    wgpu::Buffer smallIdx = CreateIndexBuffer({0, 1, 2});
+
+    // Indirect buffer
+    wgpu::Buffer indirect = CreateIndirectBuffer({3, 1, 0, 0, 0});
+
+    // Buffers to use for simple fragment counter
+    uint32_t data[] = {0};
+    wgpu::Buffer counterBuffer = utils::CreateBufferFromData(
+        device, data, sizeof(uint32_t),
+        wgpu::BufferUsage::Storage | wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::CopySrc);
+    wgpu::Buffer counterRead = utils::CreateBufferFromData(
+        device, data, sizeof(uint32_t), wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::CopySrc);
+
+    // Pipeline
+    wgpu::ShaderModule module = utils::CreateShaderModule(device, R"(
+        struct Ctr { n: atomic<u32>, };
+        @group(0) @binding(0) var<storage, read_write> ctr: Ctr;
+
+        @vertex fn vs() -> @builtin(position) vec4f {
+            return vec4f(0.0, 0.0, 0.0, 1.0);
+        }
+        // Simply adds one to the simple fragment counter with each draw
+        @fragment fn fs() -> @location(0) vec4f {
+            atomicAdd(&ctr.n, 1u);
+            return vec4f(1.0, 0.0, 0.0, 1.0);
+        })");
+
+    utils::ComboRenderPipelineDescriptor descriptor;
+    descriptor.vertex.module = module;
+    descriptor.cFragment.module = module;
+    descriptor.primitive.topology = wgpu::PrimitiveTopology::PointList;
+    descriptor.cTargets[0].format = renderPass.colorFormat;
+    wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&descriptor);
+
+    // Bind group
+    wgpu::BindGroup bindGroup = utils::MakeBindGroup(device, pipeline.GetBindGroupLayout(0),
+                                                     {{0, counterBuffer, 0, sizeof(float)}});
+
+    // Render bundles
+    utils::ComboRenderBundleEncoderDescriptor desc = {};
+    desc.colorFormatCount = 1;
+    desc.cColorFormats[0] = renderPass.colorFormat;
+
+    wgpu::RenderBundle directRenderBundle;
+    {
+        wgpu::RenderBundleEncoder renderBundleEncoder = device.CreateRenderBundleEncoder(&desc);
+        renderBundleEncoder.SetPipeline(pipeline);
+        renderBundleEncoder.SetBindGroup(0, bindGroup);
+        renderBundleEncoder.DrawIndirect(indirect, 0);
+        directRenderBundle = renderBundleEncoder.Finish();
+    }
+
+    wgpu::RenderBundle indexedRenderBundle;
+    {
+        wgpu::RenderBundleEncoder renderBundleEncoder = device.CreateRenderBundleEncoder(&desc);
+        renderBundleEncoder.SetPipeline(pipeline);
+        renderBundleEncoder.SetBindGroup(0, bindGroup);
+        renderBundleEncoder.SetIndexBuffer(smallIdx, wgpu::IndexFormat::Uint32);
+        renderBundleEncoder.DrawIndexedIndirect(indirect, 0);
+        indexedRenderBundle = renderBundleEncoder.Finish();
+    }
+
+    //
+    // Bug - Same bundle executed twice in a single render pass.
+    //
+    {
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+
+        // Test bundles with DrawIndirect calls
+        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo);
+        pass.ExecuteBundles(1, &directRenderBundle);
+        pass.ExecuteBundles(1, &directRenderBundle);
+        pass.End();
+
+        wgpu::CommandBuffer commands = encoder.Finish();
+        queue.Submit(1, &commands);
+    }
+
+    {
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+
+        // Test bundles with DrawIndexedIndirect calls
+        wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo);
+        pass.ExecuteBundles(1, &indexedRenderBundle);
+        pass.ExecuteBundles(1, &indexedRenderBundle);
+        pass.End();
+
+        // Copy the fragment counter results of both passes to the readback buffer.
+        encoder.CopyBufferToBuffer(counterBuffer, 0, counterRead, 0, 4);
+        wgpu::CommandBuffer commands = encoder.Finish();
+        queue.Submit(1, &commands);
+    }
+
+    // Each bundle should produce 3 fragments, for a total of 12.
+    EXPECT_BUFFER_U32_EQ(12, counterRead, 0);
+}
+
 DAWN_INSTANTIATE_TEST(RenderBundleIndirectValidationTest,
                       D3D11Backend(),
                       D3D12Backend(),
Loading diff…

Original Bug Report

reported by [email protected]

Dawn/WebGPU GPU-process OOB read in RenderBundle indirect-draw validation after incomplete 495489174 side-table fix


Report description

Dawn/WebGPU GPU-process OOB read in RenderBundle indirect-draw validation after incomplete 495489174 side-table fix


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules

Which URL (or repository) have you found the vulnerability in?

https://dawn.googlesource.com/dawn


The problem

Please describe the technical details of the vulnerability

Dawn’s validated indirect-draw side table can be indexed out of bounds when the same WebGPU render bundle is executed more than once in one render pass.

The 495489174 family introduced the validated indirect-draw side table. In Dawn revision 58263faefe3c52fac4656825c6d55f85ee3c7536, IndirectDrawMetadata::AddBundle() de-duplicates repeated RenderBundleBase* entries, so validation metadata is populated once for one unique bundle. During backend replay, the Vulkan path still advances a render-pass-wide indirectDrawIndex++ for every replayed indirect draw. Therefore, if one bundle contains one drawIndirect() and is executed twice, the side table has one entry while the second replay reads index 1.

Relevant source:

The attached production_poc.html executes the same render bundle twice in one render pass and causes a GPU-process crash:

GPU process exited unexpectedly: exit_code=5
JS_ERROR: OperationError: A valid external Instance reference no longer exists.
Reinitialized the GPU process after a crash.

The attached asan.log confirms the memory effect:

ERROR: AddressSanitizer: use-after-poison
READ of size 8
dawn::native::IndirectDrawMetadata::GetValidatedIndirectDraw(...)
dawn::native::vulkan::CommandBuffer::RecordRenderPass(...) third_party/dawn/src/dawn/native/vulkan/CommandBufferVk.cpp:1768
0 bytes after 16-byte region
std::vector<...ValidatedIndirectDraw...>::resize
dawn::native::IndirectDrawMetadata::SetValidatedIndirectDrawArgs(...)

The ASAN trace is attached as asan.log.

Reproduction:

python3 -m http.server 8000

Open:

http://127.0.0.1:8000/production_poc.html?n=2

ASAN command used for the attached asan.log:

ASAN_OPTIONS=detect_container_overflow=0:detect_odr_violation=0:detect_leaks=0:allocator_may_return_null=1:halt_on_error=1:symbolize=1 \
timeout 25 \
out/gpu-asan/content_shell \
  --no-sandbox \
  --disable-gpu-sandbox \
  --enable-unsafe-webgpu \
  --ignore-gpu-blocklist \
  --enable-features=Vulkan \
  --use-angle=swiftshader \
  --use-webgpu-adapter=swiftshader \
  http://127.0.0.1:8000/production_poc.html?n=2

The WebGPU/Vulkan flags above were only needed for the ASAN environment used to collect the attached sanitizer trace.

Impact analysis

An attacker-controlled web page can submit ordinary WebGPU commands that make the Chrome GPU process read past Dawn’s validated indirect-draw side table. The demonstrated impact is a renderer-reachable GPU-process OOB read and crash. I have not demonstrated a web-observable information leak or code execution primitive.


The cause

What version of Chrome have you found the security issue in?

151.0.7878.0 / 0be65f3c067d627156a9d4954715bad598e4d6c7 / DEPS pins Dawn 58263faefe3c52fac4656825c6d55f85ee3c7536

Yes, it is related to a crash.

Choose the type of vulnerability

Memory Corruption (in a sandboxed process)

How would you like to be publicly acknowledged for your report?

sm1ee, ksw9722

View on issue tracker