CVE-2026-11006
Overview
Files Changed
src/dawn/native/IndirectDrawMetadata.cppsrc/dawn/native/IndirectDrawValidationEncoder.cppsrc/dawn/native/d3d11/CommandBufferD3D11.cppsrc/dawn/native/d3d11/CommandBufferD3D11.hsrc/dawn/native/d3d12/CommandBufferD3D12.cpp
Patch
From c2b4b17e3faf48841676b75621cd420d91371de2 Mon Sep 17 00:00:00 2001 From: Brandon Jones <[email protected]> Date: Tue, 05 May 2026 17:02:01 -0700 Subject: [PATCH] Fix OOB read due to RenderBundle indirect draw validation Fixes a potential OOB VRAM read due to overlapping validation of indirect draws in a render bundle. Does this by keeping a side table of the altered buffers and offsets used when validating the indirect draws rather than overwriting the base DrawIndirectCmd stored in the render bundle. Bug: 495489174 Change-Id: I2189c9970fc237abbecf2726f09a087e5ec85359 Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/305635 Auto-Submit: Brandon Jones <[email protected]> Commit-Queue: Brandon Jones <[email protected]> Reviewed-by: Loko Kung <[email protected]> --- diff --git a/src/dawn/native/IndirectDrawMetadata.cpp b/src/dawn/native/IndirectDrawMetadata.cpp index 339062a..3f1e01b 100644 --- a/src/dawn/native/IndirectDrawMetadata.cpp +++ b/src/dawn/native/IndirectDrawMetadata.cpp @@ -192,10 +192,11 @@ validatedDraw.indirectBuffer = indirectBuffer; validatedDraw.indirectOffset = indirectOffset; - // TODO(crbug.com/495489174): Altering these values on the original draw command does not work - // for render bundles. A future change will remove these in favor of relying only on the - // mValidatedIndirectDraws array. - draw.cmd->indirectBuffer = indirectBuffer; + // TODO(crbug.com/495489174): Currently running without validation does not populate the + // validated indirect draws array. Setting the indirectBuffer of the command to null is used as + // a signifier that the validated array should be used. This should be replaced in the future + // with code that explicitly sets the validated indirect draw array in all cases. + draw.cmd->indirectBuffer = nullptr; draw.cmd->indirectOffset = indirectOffset; } @@ -268,7 +269,6 @@ config, IndexedIndirectBufferValidationInfo(indirectBuffer)); it = result.first; } - IndirectDraw draw{}; draw.validatedDrawIndex = mNextIndirectDrawIndex++; draw.inputBufferOffset = indirectOffset; diff --git a/src/dawn/native/IndirectDrawValidationEncoder.cpp b/src/dawn/native/IndirectDrawValidationEncoder.cpp index bcf70b0..10fb2e1 100644 --- a/src/dawn/native/IndirectDrawValidationEncoder.cpp +++ b/src/dawn/native/IndirectDrawValidationEncoder.cpp @@ -880,6 +880,10 @@ // Update the draw command to use the validated indirect buffer. // The drawCountBuffer doesn't need to be updated because if it exceeds the // maxDrawCount it will be clamped to maxDrawCount. + // TODO(crbug.com/495489174): This will suffer from the same problem with render bundles + // as we saw with regular draw{Indexed}Indirect calls. The same fix, storing the + // validated buffer and offset in the ValidatedIndirectDraw array and looking it up when + // the native call is made in the CommandBuffer backends. cmd->indirectBuffer = outputParamsBuffer.GetBuffer(); cmd->indirectOffset = outputOffset; diff --git a/src/dawn/native/d3d11/CommandBufferD3D11.cpp b/src/dawn/native/d3d11/CommandBufferD3D11.cpp index afd32c8..1243418 100644 --- a/src/dawn/native/d3d11/CommandBufferD3D11.cpp +++ b/src/dawn/native/d3d11/CommandBufferD3D11.cpp @@ -339,7 +339,8 @@ } DAWN_TRY( LazyClearSyncScope(GetResourceUsages().renderPasses[nextRenderPassNumber])); - DAWN_TRY(ExecuteRenderPass(cmd, commandContext, &pipelineStateTracker)); + DAWN_TRY(ExecuteRenderPass(cmd, commandContext, &pipelineStateTracker, + nextRenderPassNumber)); nextRenderPassNumber++; break; @@ -639,7 +640,11 @@ MaybeError CommandBuffer::ExecuteRenderPass( BeginRenderPassCmd* renderPass, const ScopedSwapStateCommandRecordingContext* commandContext, - PipelineStateTracker* pipelineStateTracker) { + PipelineStateTracker* pipelineStateTracker, + PassIndex renderPassIndex) { + const IndirectDrawMetadata& metadata = GetIndirectDrawMetadata()[renderPassIndex]; + IndirectDrawIndex indirectDrawIndex{0}; + // For the color attachments that the clear_color_with_draw workaround has applied, we can skip // the clear for them. for (auto i : ClearWithDrawHelper::GetAppliedColorAttachments(GetDevice(), renderPass)) { @@ -773,7 +778,10 @@ case Command::DrawIndirect: { DrawIndirectCmd* draw = iter->NextCommand<DrawIndirectCmd>(); - auto* indirectBuffer = ToGPUUsableBuffer(draw->indirectBuffer.Get()); + IndirectDrawMetadata::ValidatedIndirectDraw validatedDraw = + metadata.GetValidatedIndirectDraw(draw, indirectDrawIndex++); + + auto* indirectBuffer = ToGPUUsableBuffer(validatedDraw.indirectBuffer.Get()); DAWN_ASSERT(indirectBuffer != nullptr); DAWN_TRY(bindGroupTracker.Apply()); @@ -784,7 +792,7 @@ // Copy StartVertexLocation and StartInstanceLocation into the uniform buffer // for built-in variables. uint64_t offset = - draw->indirectOffset + + validatedDraw.indirectOffset + offsetof(D3D11_DRAW_INSTANCED_INDIRECT_ARGS, StartVertexLocation); DAWN_TRY(Buffer::Copy(commandContext, indirectBuffer, offset, sizeof(uint32_t) * 2, @@ -796,7 +804,7 @@ DAWN_TRY_ASSIGN(d3dBuffer, indirectBuffer->GetD3D11NonConstantBuffer(commandContext)); commandContext->GetD3D11DeviceContext3()->DrawInstancedIndirect( - d3dBuffer, draw->indirectOffset); + d3dBuffer, validatedDraw.indirectOffset); break; } @@ -804,7 +812,10 @@ case Command::DrawIndexedIndirect: { DrawIndexedIndirectCmd* draw = iter->NextCommand<DrawIndexedIndirectCmd>(); - auto* indirectBuffer = ToGPUUsableBuffer(draw->indirectBuffer.Get()); + IndirectDrawMetadata::ValidatedIndirectDraw validatedDraw = + metadata.GetValidatedIndirectDraw(draw, indirectDrawIndex++); + + auto* indirectBuffer = ToGPUUsableBuffer(validatedDraw.indirectBuffer.Get()); DAWN_ASSERT(indirectBuffer != nullptr); DAWN_TRY(bindGroupTracker.Apply()); @@ -815,7 +826,7 @@ // Copy StartVertexLocation and StartInstanceLocation into the uniform buffer // for built-in variables. uint64_t offset = - draw->indirectOffset + + validatedDraw.indirectOffset + offsetof(D3D11_DRAW_INDEXED_INSTANCED_INDIRECT_ARGS, BaseVertexLocation); DAWN_TRY(Buffer::Copy(commandContext, indirectBuffer, offset, sizeof(uint32_t) * 2, @@ -827,7 +838,7 @@ DAWN_TRY_ASSIGN(d3dBuffer, indirectBuffer->GetD3D11NonConstantBuffer(commandContext)); commandContext->GetD3D11DeviceContext3()->DrawIndexedInstancedIndirect( - d3dBuffer, draw->indirectOffset); + d3dBuffer, validatedDraw.indirectOffset); break; } diff --git a/src/dawn/native/d3d11/CommandBufferD3D11.h b/src/dawn/native/d3d11/CommandBufferD3D11.h index 5d5048c..262c2bc 100644 --- a/src/dawn/native/d3d11/CommandBufferD3D11.h +++ b/src/dawn/native/d3d11/CommandBufferD3D11.h @@ -56,7 +56,8 @@ PipelineStateTracker* pipelineStateTracker); MaybeError ExecuteRenderPass(BeginRenderPassCmd* renderPass, const ScopedSwapStateCommandRecordingContext* commandContext, - PipelineStateTracker* pipelineStateTracker); + PipelineStateTracker* pipelineStateTracker, + PassIndex renderPassIndex); void HandleDebugCommands(const ScopedSwapStateCommandRecordingContext* commandContext, CommandIterator* iter, Command command); diff --git a/src/dawn/native/d3d12/CommandBufferD3D12.cpp b/src/dawn/native/d3d12/CommandBufferD3D12.cpp index d496c22..4d8d4a4 100644 --- a/src/dawn/native/d3d12/CommandBufferD3D12.cpp +++ b/src/dawn/native/d3d12/CommandBufferD3D12.cpp @@ -1026,7 +1026,7 @@ DAWN_TRY(RecordRenderPass(commandContext, descriptorHeapState.GetGraphicsBindingTracker(), - beginRenderPassCmd, passHasUAV)); + beginRenderPassCmd, nextRenderPassNumber, passHasUAV)); nextRenderPassNumber++; break; @@ -1734,10 +1734,14 @@ MaybeError CommandBuffer::RecordRenderPass(CommandRecordingContext* commandContext, BindGroupStateTracker<RenderPipeline>* bindingTracker, BeginRenderPassCmd* renderPass, + PassIndex renderPassIndex, const bool passHasUAV) { Device* device = ToBackend(GetDevice()); const bool useRenderPass = device->IsToggleEnabled(Toggle::UseD3D12RenderPass); + const IndirectDrawMetadata& metadata = GetIndirectDrawMetadata()[renderPassIndex]; + IndirectDrawIndex indirectDrawIndex{0}; + // renderPassBuilder must be scoped to RecordRenderPass because any underlying // D3D12_RENDER_PASS_ENDING_ACCESS_RESOLVE_SUBRESOURCE_PARAMETERS structs must remain // valid until after EndRenderPass() has been called. @@ -1823,11 +1827,16 @@ vertexBufferTracker.Apply(commandList, lastPipeline); immediates.Apply(commandContext); - Buffer* buffer = ToBackend(draw->indirectBuffer.Get()); + IndirectDrawMetadata::ValidatedIndirectDraw validatedDraw = + metadata.GetValidatedIndirectDraw(draw, indirectDrawIndex++); + + Buffer* indirectBuffer = ToBackend(validatedDraw.indirectBuffer.Get());
Regression Test / PoC
diff --git a/src/dawn/tests/end2end/RenderBundleTests.cpp b/src/dawn/tests/end2end/RenderBundleTests.cpp
index a954888..e834d28 100644
--- a/src/dawn/tests/end2end/RenderBundleTests.cpp
+++ b/src/dawn/tests/end2end/RenderBundleTests.cpp
@@ -341,9 +341,6 @@
// a specific scenario where the a render bundle with the indirect draw was executed multiple times
// in a single encoder. Test based on a POC produced for that issue.
TEST_P(RenderBundleIndirectValidationTest, RepeatedIndirectDrawValidation) {
- // Test currently fails on all backends
- DAWN_TEST_UNSUPPORTED_IF(true);
-
const uint32_t OOB_COUNT = 100000;
// Render Pass
Original Bug Report
OOB VRAM access via RenderBundle indirect draw command mutation
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: Dawn’s indirect draw validation pipeline incorrectly mutates persistent command objects owned by RenderBundles. If a bundle is executed in multiple render passes, later passes overwrite the validation buffer pointers used by earlier passes, leading to the GPU reading unvalidated or uninitialized indirect draw parameters. This bypasses WebGPU’s security clamping and can lead to out-of-bounds VRAM access.
Affected files:
third_party/dawn/src/dawn/native/IndirectDrawValidationEncoder.cppthird_party/dawn/src/dawn/native/IndirectDrawMetadata.hthird_party/dawn/src/dawn/native/ScratchBuffer.cppthird_party/dawn/src/dawn/native/RenderBundle.cpp
Estimated timestamp from git blame: 2024-08-16
Summary
A logic flaw exists in Dawn’s indirect draw validation pipeline where validation-time code mutates persistent command objects owned by RenderBundles. When an indirect draw is processed, Dawn rewrites the command’s indirectBuffer and indirectOffset to point into a device-level scratch buffer that holds compute-shader-validated (clamped) parameters. However, for RenderBundles, these mutations persist across render passes. This results in state corruption that allows bypassing WebGPU’s security clamping.
Technical Details
1. Persistent Mutation of RenderBundle Commands
In IndirectDrawValidationEncoder.cpp, the validation logic rewrites the indirect draw command directly in the command stream:
// IndirectDrawValidationEncoder.cpp
draw.cmd->indirectBuffer = outputParamsBuffer.GetBuffer();
draw.cmd->indirectOffset = outputParamsOffset;
The draw.cmd pointer is a raw_ptr that points into the command stream baked into a RenderBundle (RenderBundleBase::mCommands). Unlike a RenderPassEncoder where the command stream is transient, a RenderBundle’s command stream persists until the bundle is destroyed.
2. Cross-Draw Parameter Confusion and Uninitialized VRAM Access
When a CommandEncoder encodes multiple render passes that use the same RenderBundle, the following occurs:
- Pass 1: Validation code mutates the bundle’s
DrawIndirectCmdto point to the current scratch buffer at Offset A. Pass 1’s compute shader is recorded to write clamped parameters to this location. - Pass 2: Validation code runs again. If Pass 2 requires more validation space,
ScratchBuffer::EnsureCapacityreallocates the scratch buffer. It explicitly callsmBuffer->SetInitialized(true), suppressing Dawn’s lazy-clearing mechanism. The new scratch buffer contains uninitialized VRAM. - Pass 2 Mutation: Validation code mutates the same
DrawIndirectCmdin the bundle to point to the new scratch buffer at Offset B.
When the CommandBuffer is submitted to the GPU:
- Pass 1’s compute shader writes clamped parameters to the old scratch buffer.
- Pass 1’s render pass executes the bundle. However, the bundle’s
DrawIndirectCmdnow points to the new scratch buffer at Offset B. - The GPU reads uninitialized, unvalidated VRAM from the new scratch buffer and uses it as the
vertexCount,instanceCount, etc.
Impact
An attacker can trigger out-of-bounds GPU memory access from the sandboxed GPU process. By bypassing WebGPU’s indirect draw security clamping, the attacker can cause the GPU to read or write arbitrary VRAM, potentially leaking cross-origin data or triggering driver-level vulnerabilities.
Suggested Steps to Trigger
(Note: These are potential steps as our setup cannot run code)
- Create a
RenderBundlecontaining anindirectDrawcommand. - Create a
CommandEncoder. - Begin a
RenderPassEncoder(Pass 1). CallexecuteBundles()with the created bundle. End Pass 1. - Begin a second
RenderPassEncoder(Pass 2). Encode additional indirect draws to force the scratch buffer to grow and reallocate. CallexecuteBundles()with the same bundle. End Pass 2. - Call
finish()on theCommandEncoderandsubmit()the resulting command buffer to the queue. - Pass 1 will execute using the uninitialized scratch buffer allocated during Pass 2’s validation, using unvalidated parameters for its draw call.
Suggested Fix
Do not mutate DrawIndirectCmd or MultiDrawIndirectCmd objects within a RenderBundle’s persistent command stream. Instead, maintain a side-table in the per-pass state that maps bundle commands to their validated scratch buffer and offset, and have the backend apply these validated parameters dynamically during bundle execution. Alternatively, RenderBundles may need their own dedicated, immutable validation buffers that are updated by the compute shader prior to execution.
Evaluated with Chrome root at commit: 9760e6c70cd33a320713361f17c6dcca85648c0f
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. Please feel free to reach out to me if you have concerns or feedback.