CVE-2026-11077
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/dawn/native/vulkan/DeviceVk.cpp |
modified | |
MultisampledRenderingWithEmptyPassWorkaroundTestsrc/dawn/tests/end2end/MultisampledRenderingTests.cpp |
modified | |
TEST_Psrc/dawn/tests/end2end/MultisampledRenderingTests.cpp |
modified | |
MultisampledRenderingWithTransientAttachmentTestsrc/dawn/tests/end2end/MultisampledRenderingTests.cpp |
modified |
Files Changed
src/dawn/native/vulkan/CommandBufferVk.cppsrc/dawn/native/vulkan/DeviceVk.cppsrc/dawn/native/vulkan/DeviceVk.hsrc/dawn/tests/end2end/MultisampledRenderingTests.cpp
Patch
From 96214998aff466823040d1ed4418b578ad0225e7 Mon Sep 17 00:00:00 2001 From: Brandon Jones <[email protected]> Date: Wed, 22 Apr 2026 14:52:58 -0700 Subject: [PATCH] Vulkan: Cleanup EmptyPassQuerySet error handling Cleans up the error handling when creating an empy pass query set and adds a simple test to ensure that this branch is exercised. Bug: 499908918 Change-Id: Ibc07005f41ee19604c5014be63dfdaea0e5ec8af Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/304177 Commit-Queue: Brandon Jones <[email protected]> Reviewed-by: Corentin Wallez <[email protected]> Auto-Submit: Brandon Jones <[email protected]> --- diff --git a/src/dawn/native/vulkan/CommandBufferVk.cpp b/src/dawn/native/vulkan/CommandBufferVk.cpp index bd0de87..f726da9 100644 --- a/src/dawn/native/vulkan/CommandBufferVk.cpp +++ b/src/dawn/native/vulkan/CommandBufferVk.cpp @@ -1091,6 +1091,10 @@ ResetUsedQuerySetsOnRenderPass(device, recordingContext->commandBuffer, usages.querySets[i], usages.queryAvailabilities[i]); } + + if (device->IsToggleEnabled(Toggle::VulkanAddWorkToEmptyResolvePass)) { + DAWN_TRY(device->PrepareEmptyPassQuerySet(recordingContext)); + } return {}; }; @@ -1939,16 +1943,9 @@ // a driver bug that fails to resolve render targets in empty passes. if (workCommandCount == 0 && device->IsToggleEnabled(Toggle::VulkanAddWorkToEmptyResolvePass)) { - QuerySetBase* querySet = device->GetEmptyPassQuerySet(); - - // Ensure that the query was allocated. In the unlikely event that it wasn't - // (probably due to an OOM) empty render passes can't be safely completed using - // this workaround. - DAWN_INTERNAL_ERROR_IF(querySet->IsError(), - "Unable to allocate internal empty pass query set."); - - device->fn.CmdBeginQuery(commands, ToBackend(querySet)->GetHandle(), 0, 0); - device->fn.CmdEndQuery(commands, ToBackend(querySet)->GetHandle(), 0); + Ref<QuerySetBase> emptyQuerySet = device->UseEmptyPassQuerySet(); + device->fn.CmdBeginQuery(commands, ToBackend(emptyQuerySet)->GetHandle(), 0, 0); + device->fn.CmdEndQuery(commands, ToBackend(emptyQuerySet)->GetHandle(), 0); } RecordEndRenderPass(recordingContext, device); diff --git a/src/dawn/native/vulkan/DeviceVk.cpp b/src/dawn/native/vulkan/DeviceVk.cpp index 29b60ed..1fe3dbc 100644 --- a/src/dawn/native/vulkan/DeviceVk.cpp +++ b/src/dawn/native/vulkan/DeviceVk.cpp @@ -1222,17 +1222,36 @@ return true; } -// Gets or creates an occlusion Query object for use with Toggle::VulkanAddWorkToEmptyResolvePass. -QuerySetBase* Device::GetEmptyPassQuerySet() { +// Prepares the Empty Pass QuerySet, creating and resetting it if needed. +MaybeError Device::PrepareEmptyPassQuerySet(CommandRecordingContext* recordingContext) { DAWN_ASSERT(IsToggleEnabled(Toggle::VulkanAddWorkToEmptyResolvePass)); - if (!mEmptyPassQuerySet) { QuerySetDescriptor descriptor; descriptor.type = wgpu::QueryType::Occlusion; descriptor.count = 1; - mEmptyPassQuerySet = APICreateQuerySet(&descriptor); + DAWN_TRY_ASSIGN(mEmptyPassQuerySet, CreateQuerySet(&descriptor)); + mEmptyPassQuerySetNeedsReset = true; } - return mEmptyPassQuerySet.Get(); + + if (mEmptyPassQuerySetNeedsReset) { + fn.CmdResetQueryPool(recordingContext->commandBuffer, + ToBackend(mEmptyPassQuerySet)->GetHandle(), 0, 1); + mEmptyPassQuerySetNeedsReset = false; + } + + return {}; +} + +// Gets the occlusion Query object for use with Toggle::VulkanAddWorkToEmptyResolvePass. +// Must call PrepareEmptyPassQuerySet outside of recording a render pass prior to calling this every +// time. +Ref<QuerySetBase> Device::UseEmptyPassQuerySet() { + DAWN_ASSERT(IsToggleEnabled(Toggle::VulkanAddWorkToEmptyResolvePass)); + DAWN_ASSERT(mEmptyPassQuerySet); + DAWN_ASSERT(!mEmptyPassQuerySetNeedsReset); + + mEmptyPassQuerySetNeedsReset = true; + return mEmptyPassQuerySet; } } // namespace dawn::native::vulkan diff --git a/src/dawn/native/vulkan/DeviceVk.h b/src/dawn/native/vulkan/DeviceVk.h index bc6c8d3..5c74441 100644 --- a/src/dawn/native/vulkan/DeviceVk.h +++ b/src/dawn/native/vulkan/DeviceVk.h @@ -151,7 +151,8 @@ size_t bufferSize) const override; bool NeedsStaticSamplerForExternalTexture() const override; - QuerySetBase* GetEmptyPassQuerySet(); + MaybeError PrepareEmptyPassQuerySet(CommandRecordingContext* recordingContext); + Ref<QuerySetBase> UseEmptyPassQuerySet(); VulkanRenderPassType GetRenderPassType() { return mRenderPassType; } @@ -242,6 +243,7 @@ Ref<PipelineCache> mMonolithicPipelineCache; Ref<QuerySetBase> mEmptyPassQuerySet; + bool mEmptyPassQuerySetNeedsReset = true; std::atomic<uint64_t> mNextTextureViewId = 1; VulkanRenderPassType mRenderPassType = VulkanRenderPassType::CreateRenderPass; diff --git a/src/dawn/tests/end2end/MultisampledRenderingTests.cpp b/src/dawn/tests/end2end/MultisampledRenderingTests.cpp index 32a52a8..19dcd1b 100644 --- a/src/dawn/tests/end2end/MultisampledRenderingTests.cpp +++ b/src/dawn/tests/end2end/MultisampledRenderingTests.cpp @@ -1382,6 +1382,46 @@ VerifyResolveTarget(kGreen, mResolveTexture, 0, 0, kMSAACoverage, kGreenX, kGreenY); } +class MultisampledRenderingWithEmptyPassWorkaroundTest : public MultisampledRenderingTest {}; + +TEST_P(MultisampledRenderingWithEmptyPassWorkaroundTest, ResolveOneMultisampledTextureTwice) { + constexpr bool kTestDepth = false; + wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder(); + + wgpu::Texture resolveTexture2 = CreateTextureForRenderAttachment(kColorFormat, 1); + + // In first render pass clear the color view to green triangle resolve. + { + utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest( + {mMultisampledColorView}, {mResolveView}, wgpu::LoadOp::Clear, wgpu::LoadOp::Clear, + kTestDepth); + renderPass.cColorAttachments[0].clearValue = kGreen; + + wgpu::RenderPassEncoder renderPassEncoder = commandEncoder.BeginRenderPass(&renderPass); + // No work done during the pass + renderPassEncoder.End(); + } + + // In second render pass load the previously cleared color view and resolve. + { + wgpu::TextureView resolveView2 = resolveTexture2.CreateView(); + utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest( + {mMultisampledColorView}, {resolveView2}, wgpu::LoadOp::Load, wgpu::LoadOp::Load, + kTestDepth); + + wgpu::RenderPassEncoder renderPassEncoder = commandEncoder.BeginRenderPass(&renderPass); + // No work done during the pass + renderPassEncoder.End(); + } + + wgpu::CommandBuffer commandBuffer = commandEncoder.Finish(); + queue.Submit(1, &commandBuffer); + + constexpr float kMSAACoverage = 1.0f; + VerifyResolveTarget(kGreen, mResolveTexture, 0, 0, kMSAACoverage); + VerifyResolveTarget(kGreen, resolveTexture2, 0, 0, kMSAACoverage); +} + class MultisampledRenderingWithTransientAttachmentTest : public MultisampledRenderingTest {}; // Test using one multisampled color transient attachment with resolve target can render correctly. @@ -2888,6 +2928,9 @@ MetalBackend({"always_resolve_into_zero_level_and_layer", "emulate_store_and_msaa_resolve"})); +DAWN_INSTANTIATE_TEST(MultisampledRenderingWithEmptyPassWorkaroundTest, + VulkanBackend({"vulkan_add_work_to_empty_resolve_pass"})); + DAWN_INSTANTIATE_TEST(MultisampledRenderingWithTransientAttachmentTest, D3D11Backend(), D3D12Backend(),
Regression Test / PoC
diff --git a/src/dawn/tests/end2end/MultisampledRenderingTests.cpp b/src/dawn/tests/end2end/MultisampledRenderingTests.cpp
index 32a52a8..19dcd1b 100644
--- a/src/dawn/tests/end2end/MultisampledRenderingTests.cpp
+++ b/src/dawn/tests/end2end/MultisampledRenderingTests.cpp
@@ -1382,6 +1382,46 @@
VerifyResolveTarget(kGreen, mResolveTexture, 0, 0, kMSAACoverage, kGreenX, kGreenY);
}
+class MultisampledRenderingWithEmptyPassWorkaroundTest : public MultisampledRenderingTest {};
+
+TEST_P(MultisampledRenderingWithEmptyPassWorkaroundTest, ResolveOneMultisampledTextureTwice) {
+ constexpr bool kTestDepth = false;
+ wgpu::CommandEncoder commandEncoder = device.CreateCommandEncoder();
+
+ wgpu::Texture resolveTexture2 = CreateTextureForRenderAttachment(kColorFormat, 1);
+
+ // In first render pass clear the color view to green triangle resolve.
+ {
+ utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
+ {mMultisampledColorView}, {mResolveView}, wgpu::LoadOp::Clear, wgpu::LoadOp::Clear,
+ kTestDepth);
+ renderPass.cColorAttachments[0].clearValue = kGreen;
+
+ wgpu::RenderPassEncoder renderPassEncoder = commandEncoder.BeginRenderPass(&renderPass);
+ // No work done during the pass
+ renderPassEncoder.End();
+ }
+
+ // In second render pass load the previously cleared color view and resolve.
+ {
+ wgpu::TextureView resolveView2 = resolveTexture2.CreateView();
+ utils::ComboRenderPassDescriptor renderPass = CreateComboRenderPassDescriptorForTest(
+ {mMultisampledColorView}, {resolveView2}, wgpu::LoadOp::Load, wgpu::LoadOp::Load,
+ kTestDepth);
+
+ wgpu::RenderPassEncoder renderPassEncoder = commandEncoder.BeginRenderPass(&renderPass);
+ // No work done during the pass
+ renderPassEncoder.End();
+ }
+
+ wgpu::CommandBuffer commandBuffer = commandEncoder.Finish();
+ queue.Submit(1, &commandBuffer);
+
+ constexpr float kMSAACoverage = 1.0f;
+ VerifyResolveTarget(kGreen, mResolveTexture, 0, 0, kMSAACoverage);
+ VerifyResolveTarget(kGreen, resolveTexture2, 0, 0, kMSAACoverage);
+}
+
class MultisampledRenderingWithTransientAttachmentTest : public MultisampledRenderingTest {};
// Test using one multisampled color transient attachment with resolve target can render correctly.
@@ -2888,6 +2928,9 @@
MetalBackend({"always_resolve_into_zero_level_and_layer",
"emulate_store_and_msaa_resolve"}));
+DAWN_INSTANTIATE_TEST(MultisampledRenderingWithEmptyPassWorkaroundTest,
+ VulkanBackend({"vulkan_add_work_to_empty_resolve_pass"}));
+
DAWN_INSTANTIATE_TEST(MultisampledRenderingWithTransientAttachmentTest,
D3D11Backend(),
D3D12Backend(),
Original Bug Report
heap-buffer-overflow in Dawn Device::GetEmptyPassQuerySet
VULNERABILITY DETAILS
A heap OOB access vulnerability exists in the WebGPU Dawn Vulkan backend when handling a GPU device OOM condition during an empty render pass.
The issue stems from Device::GetEmptyPassQuerySet() which dynamically creates an occlusion QuerySet to inject dummy work into empty render passes [0].
QuerySetBase* Device::GetEmptyPassQuerySet() {
DAWN_ASSERT(IsToggleEnabled(Toggle::VulkanAddWorkToEmptyResolvePass));
if (!mEmptyPassQuerySet) {
QuerySetDescriptor descriptor;
descriptor.type = wgpu::QueryType::Occlusion;
descriptor.count = 1;
mEmptyPassQuerySet = APICreateQuerySet(&descriptor); // [0]
}
return mEmptyPassQuerySet.Get();
}
When the underlying Vulkan driver fails to create the query pool and returns an error such as VK_ERROR_OUT_OF_DEVICE_MEMORY or VK_ERROR_OUT_OF_HOST_MEMORY, Dawn catches this via its CheckVkOOMThenSuccess fault-tolerance mechanism and gracefully downgrades the allocation by silently emitting an ErrorQuerySet object instead of a full vulkan::QuerySet.
ErrorQuerySet only occupies the memory footprint of its base class QuerySetBase and completely lacks backend-specific variables, including the VkQueryPool mHandle.
However, the subsequent caller in CommandBuffer::RecordRenderPass does not verify if the returned QuerySetBase is actually an ErrorQuerySet (e.g., via querySet->IsError()). It immediately up-casts the memory instance directly to the backend representation via ToBackend and begins the query loop using the GetHandle() method [1] [2].
if (workCommandCount == 0 &&
device->IsToggleEnabled(Toggle::VulkanAddWorkToEmptyResolvePass)) {
QuerySetBase* querySet = device->GetEmptyPassQuerySet();
device->fn.CmdBeginQuery(commands, ToBackend(querySet)->GetHandle(), 0, 0); // [1]
device->fn.CmdEndQuery(commands, ToBackend(querySet)->GetHandle(), 0); // [2]
}
This blind up-casting to vulkan::QuerySet results in a Type Confusion. When GetHandle() attempts to load mHandle [3], it reads past the truncated allocation size of the ErrorQuerySet object, triggering a Heap OOB access memory corruption.
VkQueryPool QuerySet::GetHandle() const {
return mHandle; // [3]
}
BISECTION
Introduced by Dawn upstream commit [0] which added the VulkanAddWorkToEmptyResolvePass toggle and its workaround mechanism involving GetEmptyPassQuerySet().
This regression was rolled into Chromium in commit [1].
[0] https://dawn.googlesource.com/dawn/+/a76b32ec053451a479ba6989ba1d020a302580a4 (Add VulkanAddWorkToEmptyResolvePass Toggle)
[1] https://chromium.googlesource.com/chromium/src/+/7bc869b0e849bed0329997b711b9be73ffede928 (Roll Dawn from 1bdb72cd216e to d074fa41df60 (17 revisions))
VERSION
Chrome Version: HEAD
Operating System: Linux
REPRODUCTION CASE
To reliably simulate a GPU device OOM and prove the underlying vulnerability in Dawn’s error handling, inject_oom.patch is provided. It minimally modifies SwiftShader to force vkCreateQueryPool to return VK_ERROR_OUT_OF_HOST_MEMORY. The vulnerability itself resides within Dawn and is independent of the Vulkan backend driver.
- Apply
inject_oom.patchtothird_party/swiftshader, then build Chromium with ASan. - Host the
poc.htmlon an HTTP server. - Run Chrome against the PoC.
$ python3 -m http.server
$ ./out/asan/chrome --enable-unsafe-webgpu --enable-dawn-features=vulkan_add_work_to_empty_resolve_pass "http://localhost:8000/poc.html"
CRASH INFORMATION
Type of crash: GPU process
Crash log: see the attached asan.txt ASan trace.
CREDIT INFORMATION
Reporter credit: Anonymous
- http://localhost:8000/poc.html
- https://chromium.googlesource.com/chromium/src/+/7bc869b0e849bed0329997b711b9be73ffede928
- https://dawn.googlesource.com/dawn/+/a76b32ec053451a479ba6989ba1d020a302580a4
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/dawn/src/dawn/native/vulkan/CommandBufferVk.cpp;drc=cde13c6d417485b7cf3edadf6edc2605faf13e22;l=1929
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/dawn/src/dawn/native/vulkan/CommandBufferVk.cpp;drc=cde13c6d417485b7cf3edadf6edc2605faf13e22;l=1930
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/dawn/src/dawn/native/vulkan/DeviceVk.cpp;drc=cde13c6d417485b7cf3edadf6edc2605faf13e22;l=1233
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/dawn/src/dawn/native/vulkan/QuerySetVk.cpp;drc=cde13c6d417485b7cf3edadf6edc2605faf13e22;l=80