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 <bajones@chromium.org>
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 <bajones@chromium.org>
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Auto-Submit: Brandon Jones <bajones@chromium.org>
---
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