CVE-2026-11067
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/dawn/native/CommandEncoder.cpp |
modified | |
ifsrc/dawn/native/CommandValidation.cpp |
modified |
Files Changed
src/dawn/native/CommandEncoder.cppsrc/dawn/native/CommandEncoder.hsrc/dawn/native/CommandValidation.cpp
Patch
From 1b3cc21d810e9f1cd7ffc7eb6734a08f7265b90b Mon Sep 17 00:00:00 2001
From: Corentin Wallez <cwallez@chromium.org>
Date: Tue, 28 Apr 2026 05:57:35 -0700
Subject: [PATCH] [dawn][native] Use a TypedInteger for QueryIndex
It is used for the indices of individual queries in QuerySets. It is
added to QuerySet's interfaces and propagated all the way to the WebGPU
API boundary.
Other cleanups:
- Add a ToQueryStorageSize instead of multiplying everywhere by
sizeof(uint64_t). (This helps convert from the typed integer as
well).
- Change QuerySet getters to not return the whole availability vector
and instead provide queries on it.
- Simplify code in CommandBufferD3D12 and CommandBufferVk that iterates
ranges of available queries. Instead of using iterators in an
std::vector, use iteration on the range of QueryIndex.
No functional changes intended.
Bug: 499140183
Change-Id: I4bcb4effc3fc37f002066367480edbc6277c2fca
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/305057
Reviewed-by: Brandon Jones <bajones@chromium.org>
Reviewed-by: Kai Ninomiya <kainino@chromium.org>
Commit-Queue: Corentin Wallez <cwallez@chromium.org>
---
diff --git a/src/dawn/native/CommandEncoder.cpp b/src/dawn/native/CommandEncoder.cpp
index 48b5986..def90aa 100644
--- a/src/dawn/native/CommandEncoder.cpp
+++ b/src/dawn/native/CommandEncoder.cpp
@@ -1026,8 +1026,8 @@
}
MaybeError ValidateQuerySetResolve(const QuerySetBase* querySet,
- uint32_t firstQuery,
- uint32_t queryCount,
+ QueryIndex firstQuery,
+ QueryIndex queryCount,
const BufferBase* destination,
uint64_t destinationOffset) {
DAWN_INVALID_IF(firstQuery >= querySet->GetQueryCount(),
@@ -1061,8 +1061,8 @@
MaybeError EncodeTimestampsToNanosecondsConversion(CommandEncoder* encoder,
QuerySetBase* querySet,
- uint32_t firstQuery,
- uint32_t queryCount,
+ QueryIndex firstQuery,
+ QueryIndex queryCount,
BufferBase* destination,
uint64_t destinationOffset) {
DeviceBase* device = encoder->GetDevice();
@@ -1072,8 +1072,8 @@
: 0xFFFFFFFF;
// Timestamp params uniform buffer
- TimestampParams params(queryCount, static_cast<uint32_t>(destinationOffset), quantization_mask,
- device->GetTimestampPeriodInNS());
+ TimestampParams params(uint32_t{queryCount}, static_cast<uint32_t>(destinationOffset),
+ quantization_mask, device->GetTimestampPeriodInNS());
BufferDescriptor parmsDesc = {};
parmsDesc.usage = wgpu::BufferUsage::Uniform | wgpu::BufferUsage::CopyDst;
@@ -1083,7 +1083,7 @@
DAWN_TRY(device->GetQueue()->WriteBuffer(paramsBuffer.Get(), 0, ¶ms, sizeof(params)));
- return EncodeConvertTimestampsToNanoseconds(encoder, queryCount, destination,
+ return EncodeConvertTimestampsToNanoseconds(encoder, uint32_t{queryCount}, destination,
paramsBuffer.Get());
}
@@ -1277,7 +1277,7 @@
mUsedQuerySets.insert(querySet);
}
-void CommandEncoder::TrackQueryAvailability(QuerySetBase* querySet, uint32_t queryIndex) {
+void CommandEncoder::TrackQueryAvailability(QuerySetBase* querySet, QueryIndex queryIndex) {
DAWN_CHECK(querySet != nullptr);
TrackUsedQuerySet(querySet);
@@ -1323,17 +1323,17 @@
if (descriptor->timestampWrites != nullptr) {
QuerySetBase* querySet = descriptor->timestampWrites->querySet;
- uint32_t beginningOfPassWriteIndex =
- descriptor->timestampWrites->beginningOfPassWriteIndex;
- uint32_t endOfPassWriteIndex = descriptor->timestampWrites->endOfPassWriteIndex;
+ QueryIndex beginningOfPassWriteIndex{
+ descriptor->timestampWrites->beginningOfPassWriteIndex};
+ QueryIndex endOfPassWriteIndex{descriptor->timestampWrites->endOfPassWriteIndex};
cmd->timestampWrites.querySet = querySet;
cmd->timestampWrites.beginningOfPassWriteIndex = beginningOfPassWriteIndex;
cmd->timestampWrites.endOfPassWriteIndex = endOfPassWriteIndex;
- if (beginningOfPassWriteIndex != wgpu::kQuerySetIndexUndefined) {
+ if (beginningOfPassWriteIndex != kQuerySetIndexUndefinedTyped) {
TrackQueryAvailability(querySet, beginningOfPassWriteIndex);
}
- if (endOfPassWriteIndex != wgpu::kQuerySetIndexUndefined) {
+ if (endOfPassWriteIndex != kQuerySetIndexUndefinedTyped) {
TrackQueryAvailability(querySet, endOfPassWriteIndex);
}
}
@@ -1566,20 +1566,20 @@
if (descriptor->timestampWrites != nullptr) {
QuerySetBase* querySet = descriptor->timestampWrites->querySet;
- uint32_t beginningOfPassWriteIndex =
- descriptor->timestampWrites->beginningOfPassWriteIndex;
- uint32_t endOfPassWriteIndex = descriptor->timestampWrites->endOfPassWriteIndex;
+ QueryIndex beginningOfPassWriteIndex{
+ descriptor->timestampWrites->beginningOfPassWriteIndex};
+ QueryIndex endOfPassWriteIndex{descriptor->timestampWrites->endOfPassWriteIndex};
cmd->timestampWrites.querySet = querySet;
cmd->timestampWrites.beginningOfPassWriteIndex = beginningOfPassWriteIndex;
cmd->timestampWrites.endOfPassWriteIndex = endOfPassWriteIndex;
- if (beginningOfPassWriteIndex != wgpu::kQuerySetIndexUndefined) {
+ if (beginningOfPassWriteIndex != kQuerySetIndexUndefinedTyped) {
TrackQueryAvailability(querySet, beginningOfPassWriteIndex);
// Track the query availability with true on render pass again for rewrite
// validation and query reset on Vulkan
usageTracker.TrackQueryAvailability(querySet, beginningOfPassWriteIndex);
}
- if (endOfPassWriteIndex != wgpu::kQuerySetIndexUndefined) {
+ if (endOfPassWriteIndex != kQuerySetIndexUndefinedTyped) {
TrackQueryAvailability(querySet, endOfPassWriteIndex);
// Track the query availability with true on render pass again for rewrite
// validation and query reset on Vulkan
@@ -2170,10 +2170,13 @@
}
void CommandEncoder::APIResolveQuerySet(QuerySetBase* querySet,
- uint32_t firstQuery,
- uint32_t queryCount,
+ uint32_t firstQueryUntyped,
+ uint32_t queryCountUntyped,
BufferBase* destination,
uint64_t destinationOffset) {
+ QueryIndex firstQuery{firstQueryUntyped};
+ QueryIndex queryCount{queryCountUntyped};
+
mEncodingContext.TryEncode(
this,
[&](CommandAllocator* allocator) -> MaybeError {
@@ -2219,7 +2222,6 @@
destination, destinationOffset);
}
-
void CommandEncoder::APIWriteBuffer(BufferBase* buffer,
uint64_t bufferOffset,
const uint8_t* data,
@@ -2246,7 +2248,9 @@
"encoding %s.WriteBuffer(%s, %u, ..., %u).", this, buffer, bufferOffset, size);
}
-void CommandEncoder::APIWriteTimestamp(QuerySetBase* querySet, uint32_t queryIndex) {
+void CommandEncoder::APIWriteTimestamp(QuerySetBase* querySet, uint32_t queryIndexUntyped) {
+ QueryIndex queryIndex{queryIndexUntyped};
+
mEncodingContext.TryEncode(
this,
[&](CommandAllocator* allocator) -> MaybeError {
diff --git a/src/dawn/native/CommandEncoder.h b/src/dawn/native/CommandEncoder.h
index a40d3ba..3485e6f 100644
--- a/src/dawn/native/CommandEncoder.h
+++ b/src/dawn/native/CommandEncoder.h
@@ -64,7 +64,7 @@
std::vector<IndirectDrawMetadata> AcquireIndirectDrawMetadata();
void TrackUsedQuerySet(QuerySetBase* querySet);
- void TrackQueryAvailability(QuerySetBase* querySet, uint32_t queryIndex);
+ void TrackQueryAvailability(QuerySetBase* querySet, QueryIndex queryIndex);
// Dawn API
ComputePassEncoder* APIBeginComputePass(const ComputePassDescriptor* descriptor);
diff --git a/src/dawn/native/CommandValidation.cpp b/src/dawn/native/CommandValidation.cpp
index 68ae6fb..7bea811 100644
--- a/src/dawn/native/CommandValidation.cpp
+++ b/src/dawn/native/CommandValidation.cpp
@@ -169,7 +169,7 @@
MaybeError ValidateTimestampQuery(const DeviceBase* device,
const QuerySetBase* querySet,
- uint32_t queryIndex,
+ QueryIndex queryIndex,
Feature requiredFeature) {
DAWN_TRY(device->ValidateObject(querySet));
@@ -202,12 +202,13 @@
"The type of %s is not %s.", querySet, wgpu::QueryType::Timestamp);
if (unpacked->beginningOfPassWriteIndex != wgpu::kQuerySetIndexUndefined) {
- DAWN_INVALID_IF(unpacked->beginningOfPassWriteIndex >= querySet->GetQueryCount(),
Original Bug Report
Potential GPU memory leak via QuerySet availability state desynchronization
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: Dawn tracks WebGPU QuerySet availability state during command encoding rather than submission. An attacker can record queries on an encoder, drop the encoder without submitting it, and then resolve the queries using a second encoder. This bypasses zero-initialization checks, resulting in a cross-origin read of uninitialized GPU memory.
Affected files:
third_party/dawn/src/dawn/native/d3d12/CommandBufferD3D12.cppthird_party/dawn/src/dawn/native/CommandEncoder.cppthird_party/dawn/src/dawn/native/QuerySet.cppthird_party/dawn/src/dawn/native/RenderPassEncoder.cppthird_party/dawn/src/dawn/native/d3d12/QuerySetD3D12.cppthird_party/dawn/src/dawn/native/QuerySet.h
Estimated timestamp from git blame: 2025-12-07
Background
In WebGPU, QuerySet objects store results from GPU measurements like occlusion or timestamps. These results must be resolved into a GPUBuffer before being mapped to the CPU. Dawn tracks which queries are “available” (i.e., have had a result written) to prevent resolving uninitialized data. If an unavailable query is resolved, Dawn is supposed to zero-clear the destination buffer to prevent leaking uninitialized GPU memory.
Vulnerability Description
Dawn suffers from a state desynchronization vulnerability because query availability is updated immediately during command encoding on the globally shared QuerySetBase object, rather than when the command buffer is successfully submitted and executed by the GPU.
In third_party/dawn/src/dawn/native/CommandEncoder.cpp (and related pass encoders), functions like TrackQueryAvailability immediately modify the QuerySetBase::mQueryAvailability vector:
// third_party/dawn/src/dawn/native/CommandEncoder.cpp
void CommandEncoder::TrackQueryAvailability(QuerySetBase* querySet, uint32_t queryIndex) {
// ...
// Set the query at queryIndex to available for resolving in query set.
querySet->SetQueryAvailability(queryIndex, true);
}
Because a CommandEncoder can be discarded without ever being submitted to the GPUQueue, the global QuerySet state can be permanently polluted. An attacker can record commands that mark queries as available, drop the encoder, and then issue a resolveQuerySet command via a new encoder.
During processing of the submitted resolve command in backends like D3D12 and Vulkan (e.g., CommandBufferD3D12.cpp:1299-1301), Dawn relies on this polluted state:
// third_party/dawn/src/dawn/native/d3d12/CommandBufferD3D12.cpp
auto startIt = querySet->GetQueryAvailability().begin() + firstQuery;
auto endIt = querySet->GetQueryAvailability().begin() + firstQuery + queryCount;
bool hasUnavailableQueries = std::find(startIt, endIt, false) != endIt;
if (hasUnavailableQueries || clearNeeded) {
DAWN_TRY(device->ClearBufferToZero(...));
}
Since the queries appear “available” to Dawn, hasUnavailableQueries evaluates to false. The safety ClearBufferToZero is bypassed. The GPU then copies uninitialized memory from the QueryHeap (which was never actually written to) into the destination buffer.
(Note: This issue might also be accompanied by a data race, as std::vector<bool> is modified concurrently without locking if multiple encoders record to the same QuerySet on different threads).
Potential Attack Steps
- Use WebGPU to create a
GPUQuerySet(e.g., occlusion, 1024 queries) and a mappable destinationGPUBuffer. - Create a “Phantom”
GPUCommandEncoder. Begin a render pass and callbeginOcclusionQuery(0)andendOcclusionQuery(). - Discard the Phantom encoder (do not call
finish()or submit it). The globalQuerySetnow falsely marks query 0 as available. - Create a “Resolving”
GPUCommandEncoder. CallresolveQuerySet(queryset, 0, 1, destinationBuffer, 0). finish()and submit the Resolving encoder to theGPUQueue.- Use
copyBufferToBufferandmapAsync(READ)to extract the contents ofdestinationBuffer. It will contain uninitialized GPU memory from the native query heap allocation, which may belong to other processes.
Recommended Fix
Query availability state should not be permanently committed to the QuerySetBase during command encoding.
- Track Per-Pass/Encoder:
CommandEncodershould track query availability changes locally in itsCommandBufferResourceUsagestate. - Commit on Submit: Only update the global
QuerySetBaseavailability state when the command buffer is successfully submitted to theGPUQueue(e.g., inQueue::Submit). - Thread Safety: Ensure that concurrent writes and reads to the
QuerySetstate during execution are properly synchronized or transitioned away fromstd::vector<bool>, which is inherently unsafe for concurrent element access.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
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.