Medium watch CVSS 6.5 chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized Use in Dawn
DescriptionUninitialized Use in Dawn
ComponentDawn
Sub-componentDawn
Bug ClassUninitialized Memory
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker499140183
Fix commit1b3cc21d810e (dawn) +287/-278
CWECWE-457 (Use of uninitialized)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-04-03 (per vendor advisory)
Fix landed2026-04-28 (public git)
Disclosed2026-06-02
Patch gap35 days — fix public in source before the advisory

Background

`QuerySet`
a Dawn/WebGPU object holding a fixed-size pool of query slots (timestamp or occlusion) plus an availability vector tracking which slots have been written.
`QueryIndex`
the new TypedInteger distinguishing an individual query’s index/count from buffer offsets, availability-vector positions, and byte sizes.
`TypedInteger`
a Dawn wrapper type that gives an integer a distinct compile-time identity so it cannot be implicitly interchanged with other same-width integers.
`ToQueryStorageSize`
a new helper converting a QueryIndex count into a byte size, replacing ad-hoc * sizeof(uint64_t) arithmetic that mixed counts with byte sizes.

Root Cause Analysis

Query indices and counts in the resolve path were plain uint32_t, so the same weakly-typed value could stand in for a query index, an availability-vector index, a buffer offset, or a byte size without any type barrier. Byte sizes were computed inline as count * sizeof(uint64_t), and availability was exposed by returning the raw vector rather than a checked per-index query, allowing a wrong index or a mis-sized range to slip through unnoticed. In ValidateQuerySetResolve, EncodeTimestampsToNanosecondsConversion, and CommandEncoder::TrackQueryAvailability, firstQuery/queryCount/queryIndex could therefore be confused, letting a resolve validate one range while reading or copying another. When the effective read range extended past the slots that had actually been written, ResolveQuerySet copied never-initialized QuerySet backing storage into the destination buffer. Because that buffer is readable by the page, the uninitialized bytes were disclosed.

The fix replaces these uint32_t parameters with the QueryIndex TypedInteger, routes byte-size math through ToQueryStorageSize, and accesses availability through checked queries, so counts, indices, and sizes can no longer be interchanged.

Key insight
A single weakly-typed uint32_t served as query index, availability index, buffer offset, and byte-size multiplier interchangeably, so one off-by-role confusion could size or position a resolve against unwritten slots. The QueryIndex TypedInteger makes each role a distinct type, turning that class of confusion into a compile error.

Attack Path

  1. The page creates a QuerySet (timestamp or occlusion) and a destination buffer readable back via WebGPU.
  2. It writes only some query slots, leaving other slots’ backing storage uninitialized, then issues a resolve whose parameters place the read/validate ranges out of agreement.
  3. Because the uint32_t index/count/size are interchangeable, validation passes for one range while ResolveQuerySet copies a differently-sized or differently-positioned range.
  4. The copy reads never-written QuerySet storage into the destination buffer.
  5. The page maps or copies the destination buffer and reads back the leaked uninitialized memory.

Impact Assessment

Uninitialized process memory from QuerySet backing storage is disclosed to a WebGPU-capable page, defeating memory-content confidentiality and potentially leaking pointers or secrets useful for further exploitation. Severity is bounded to information disclosure with no direct write or control-flow corruption, but it weakens ASLR and can seed subsequent attacks.

Changed Functions

FunctionChangeNotes
if
src/dawn/native/CommandEncoder.cpp
modified
if
src/dawn/native/CommandValidation.cpp
modified

Files Changed

  • src/dawn/native/CommandEncoder.cpp
  • src/dawn/native/CommandEncoder.h
  • src/dawn/native/CommandValidation.cpp

Audit Directions

  • Weakly-typed index/count/size
    search other Dawn resolve/copy paths where a uint32_t still doubles as index, count, offset, and byte size, and confirm each role now carries a TypedInteger.
  • Returning raw availability state
    audit getters that expose backing vectors directly instead of a bounds-checked per-index query, since these hide out-of-range accesses.
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, &params, 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(),
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cpp
  • third_party/dawn/src/dawn/native/CommandEncoder.cpp
  • third_party/dawn/src/dawn/native/QuerySet.cpp
  • third_party/dawn/src/dawn/native/RenderPassEncoder.cpp
  • third_party/dawn/src/dawn/native/d3d12/QuerySetD3D12.cpp
  • third_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

  1. Use WebGPU to create a GPUQuerySet (e.g., occlusion, 1024 queries) and a mappable destination GPUBuffer.
  2. Create a “Phantom” GPUCommandEncoder. Begin a render pass and call beginOcclusionQuery(0) and endOcclusionQuery().
  3. Discard the Phantom encoder (do not call finish() or submit it). The global QuerySet now falsely marks query 0 as available.
  4. Create a “Resolving” GPUCommandEncoder. Call resolveQuerySet(queryset, 0, 1, destinationBuffer, 0).
  5. finish() and submit the Resolving encoder to the GPUQueue.
  6. Use copyBufferToBuffer and mapAsync(READ) to extract the contents of destinationBuffer. It will contain uninitialized GPU memory from the native query heap allocation, which may belong to other processes.

Query availability state should not be permanently committed to the QuerySetBase during command encoding.

  1. Track Per-Pass/Encoder: CommandEncoder should track query availability changes locally in its CommandBufferResourceUsage state.
  2. Commit on Submit: Only update the global QuerySetBase availability state when the command buffer is successfully submitted to the GPUQueue (e.g., in Queue::Submit).
  3. Thread Safety: Ensure that concurrent writes and reads to the QuerySet state during execution are properly synchronized or transitioned away from std::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.

View on issue tracker