CVE-2026-14061
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/dawn/native/CommandEncoder.cpp |
modified |
Files Changed
src/dawn/native/CommandEncoder.cppsrc/dawn/native/Device.cppsrc/dawn/native/Device.hsrc/dawn/native/Toggles.cppsrc/dawn/native/Toggles.hsrc/dawn/native/webgpu/DeviceWGPU.cppsrc/dawn/native/webgpu/DeviceWGPU.hsrc/dawn/tests/end2end/BufferZeroInitTests.cpp
Patch
From 5326a2acd1a0bc04637e3618b2095894ba97f3bd Mon Sep 17 00:00:00 2001 From: Corentin Wallez <[email protected]> Date: Wed, 06 May 2026 10:39:40 -0700 Subject: [PATCH] [dawn][native] Quantize timestamp even on 1ns timestamp hardware The timestamp conversion compute shader is also used for quantization so run it if either quantization or conversion is needed. This adds a new capability query on backend devices to let the WebGPU backend tell the frontend that quantization is not needed since it's already taken care of by the inner device. Bug: 502434484 Change-Id: I070f5ed1427395a66e48d2a5d4fc360feedd0f0b Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/305815 Commit-Queue: Kai Ninomiya <[email protected]> Reviewed-by: Kai Ninomiya <[email protected]> --- diff --git a/src/dawn/native/CommandEncoder.cpp b/src/dawn/native/CommandEncoder.cpp index 3f653c1..c388682 100644 --- a/src/dawn/native/CommandEncoder.cpp +++ b/src/dawn/native/CommandEncoder.cpp @@ -1072,13 +1072,13 @@ uint64_t destinationOffset) { DeviceBase* device = encoder->GetDevice(); - const uint32_t quantization_mask = (device->IsToggleEnabled(Toggle::TimestampQuantization)) - ? kTimestampQuantizationMask - : 0xFFFFFFFF; + const uint32_t quantizationMask = device->IsToggleEnabled(Toggle::TimestampQuantization) + ? kTimestampQuantizationMask + : 0xFFFFFFFF; // Timestamp params uniform buffer TimestampParams params(uint32_t{queryCount}, static_cast<uint32_t>(destinationOffset), - quantization_mask, device->GetTimestampPeriodInNS()); + quantizationMask, device->GetTimestampPeriodInNS()); BufferDescriptor parmsDesc = {}; parmsDesc.usage = wgpu::BufferUsage::Uniform | wgpu::BufferUsage::CopyDst; @@ -2194,21 +2194,23 @@ cmd->destination = destination; cmd->destinationOffset = destinationOffset; - // Encode internal compute pipeline for timestamp query - if (querySet->GetQueryType() == wgpu::QueryType::Timestamp && - !GetDevice()->IsToggleEnabled(Toggle::DisableTimestampQueryConversion) && - (GetDevice()->GetTimestampPeriodInNS() != 1.0f || - GetDevice()->IsToggleEnabled(Toggle::TimestampQueryConversionEvenIf1NS))) { - // The below function might create new resources. Need to lock the Device. - // TODO(crbug.com/dawn/1618): In future, all temp resources should be created at - // Command Submit time, so the locking would be removed from here at that point. - auto deviceGuard = GetDevice()->GetGuard(); - - DAWN_TRY(EncodeTimestampsToNanosecondsConversion( - this, querySet, firstQuery, queryCount, destination, destinationOffset)); + if (querySet->GetQueryType() != wgpu::QueryType::Timestamp) { + return {}; } - return {}; + const bool needsConversion = + GetDevice()->GetTimestampPeriodInNS() != 1.0f && + !GetDevice()->IsToggleEnabled(Toggle::DisableTimestampQueryConversion); + const bool needsQuantization = + GetDevice()->IsToggleEnabled(Toggle::TimestampQuantization) && + !GetDevice()->AreTimestampsQuantized(); + if (!needsConversion && !needsQuantization) { + return {}; + } + + auto deviceGuard = GetDevice()->GetGuard(); + return EncodeTimestampsToNanosecondsConversion(this, querySet, firstQuery, queryCount, + destination, destinationOffset); }, "encoding %s.ResolveQuerySet(%s, %u, %u, %s, %u).", this, querySet, firstQuery, queryCount, destination, destinationOffset); diff --git a/src/dawn/native/Device.cpp b/src/dawn/native/Device.cpp index ed01a6c..d32fb4d 100644 --- a/src/dawn/native/Device.cpp +++ b/src/dawn/native/Device.cpp @@ -2560,6 +2560,10 @@ return 4u; } +bool DeviceBase::AreTimestampsQuantized() const { + return false; +} + MaybeError DeviceBase::CopyFromStagingToTexture(BufferBase* source, const TexelCopyBufferLayout& src, const TextureCopy& dst, diff --git a/src/dawn/native/Device.h b/src/dawn/native/Device.h index b0bbef4..9f15642 100644 --- a/src/dawn/native/Device.h +++ b/src/dawn/native/Device.h @@ -392,6 +392,7 @@ virtual uint64_t GetBufferCopyOffsetAlignmentForDepthStencil() const; virtual float GetTimestampPeriodInNS() const = 0; + virtual bool AreTimestampsQuantized() const; virtual bool ShouldDuplicateNumWorkgroupsForDispatchIndirect( ComputePipelineBase* computePipeline) const; diff --git a/src/dawn/native/Toggles.cpp b/src/dawn/native/Toggles.cpp index eba0b47..3b2f648 100644 --- a/src/dawn/native/Toggles.cpp +++ b/src/dawn/native/Toggles.cpp @@ -270,11 +270,6 @@ {"disable_timestamp_query_conversion", "Resolve timestamp queries into ticks instead of nanoseconds.", "https://crbug.com/dawn/1305", ToggleStage::Device}}, - {Toggle::TimestampQueryConversionEvenIf1NS, - {"timestamp_query_conversion_even_if_1ns", - "Force timestamp query conversion to run even if it isn't needed (unless " - "disable_timestamp_query_conversion).", - "https://crbug.com/499211666", ToggleStage::Device}}, {Toggle::TimestampQuantization, {"timestamp_quantization", "Enable timestamp queries quantization to reduce the precision of timers that can be created " diff --git a/src/dawn/native/Toggles.h b/src/dawn/native/Toggles.h index eae81a0..b88b45b 100644 --- a/src/dawn/native/Toggles.h +++ b/src/dawn/native/Toggles.h @@ -85,7 +85,6 @@ FxcOptimizations, RecordDetailedTimingInTraceEvents, DisableTimestampQueryConversion, - TimestampQueryConversionEvenIf1NS, TimestampQuantization, ClearBufferBeforeResolveQueries, VulkanUseZeroInitializeWorkgroupMemoryExtension, diff --git a/src/dawn/native/webgpu/DeviceWGPU.cpp b/src/dawn/native/webgpu/DeviceWGPU.cpp index 7e0fe84..32c04db 100644 --- a/src/dawn/native/webgpu/DeviceWGPU.cpp +++ b/src/dawn/native/webgpu/DeviceWGPU.cpp @@ -448,6 +448,10 @@ return 1.0f; } +bool Device::AreTimestampsQuantized() const { + return true; +} + bool Device::CanResolveSubRect() const { // Related code in src/dawn/native/RenderPassWorkaroundsHelper.cpp // WebGPU backend will pass down cmd->resolveRect to the inner layer backend to handle it diff --git a/src/dawn/native/webgpu/DeviceWGPU.h b/src/dawn/native/webgpu/DeviceWGPU.h index 06f4a0b..116eabb 100644 --- a/src/dawn/native/webgpu/DeviceWGPU.h +++ b/src/dawn/native/webgpu/DeviceWGPU.h @@ -60,6 +60,7 @@ bool CanResolveSubRect() const override; float GetTimestampPeriodInNS() const override; + bool AreTimestampsQuantized() const override; bool NeedsIndirectGPUValidation() const override; diff --git a/src/dawn/tests/end2end/BufferZeroInitTests.cpp b/src/dawn/tests/end2end/BufferZeroInitTests.cpp index f66eda2..6d86a10 100644 --- a/src/dawn/tests/end2end/BufferZeroInitTests.cpp +++ b/src/dawn/tests/end2end/BufferZeroInitTests.cpp @@ -1401,26 +1401,17 @@ } DAWN_INSTANTIATE_TEST(BufferZeroInitTest, - D3D11Backend({"nonzero_clear_resources_on_creation_for_testing", - "timestamp_query_conversion_even_if_1ns"}), + D3D11Backend({"nonzero_clear_resources_on_creation_for_testing"}), D3D11Backend({"auto_map_backend_buffer", "d3d11_disable_cpu_buffers", - "nonzero_clear_resources_on_creation_for_testing", - "timestamp_query_conversion_even_if_1ns"}), - D3D12Backend({"nonzero_clear_resources_on_creation_for_testing", - "timestamp_query_conversion_even_if_1ns"}), - D3D12Backend({"nonzero_clear_resources_on_creation_for_testing", - "timestamp_query_conversion_even_if_1ns"}, + "nonzero_clear_resources_on_creation_for_testing"}), + D3D12Backend({"nonzero_clear_resources_on_creation_for_testing"}), + D3D12Backend({"nonzero_clear_resources_on_creation_for_testing"}, {"d3d12_create_not_zeroed_heap"}), - MetalBackend({"nonzero_clear_resources_on_creation_for_testing", - "timestamp_query_conversion_even_if_1ns"}), - OpenGLBackend({"nonzero_clear_resources_on_creation_for_testing", - "timestamp_query_conversion_even_if_1ns"}), - OpenGLESBackend({"nonzero_clear_resources_on_creation_for_testing", - "timestamp_query_conversion_even_if_1ns"}), - VulkanBackend({"nonzero_clear_resources_on_creation_for_testing", - "timestamp_query_conversion_even_if_1ns"}), - WebGPUBackend({"nonzero_clear_resources_on_creation_for_testing", - "timestamp_query_conversion_even_if_1ns"})); + MetalBackend({"nonzero_clear_resources_on_creation_for_testing"}), + OpenGLBackend({"nonzero_clear_resources_on_creation_for_testing"}), + OpenGLESBackend({"nonzero_clear_resources_on_creation_for_testing"}), + VulkanBackend({"nonzero_clear_resources_on_creation_for_testing"}), + WebGPUBackend({"nonzero_clear_resources_on_creation_for_testing"})); } // anonymous namespace } // namespace dawn
Regression Test / PoC
diff --git a/src/dawn/tests/end2end/BufferZeroInitTests.cpp b/src/dawn/tests/end2end/BufferZeroInitTests.cpp
index f66eda2..6d86a10 100644
--- a/src/dawn/tests/end2end/BufferZeroInitTests.cpp
+++ b/src/dawn/tests/end2end/BufferZeroInitTests.cpp
@@ -1401,26 +1401,17 @@
}
DAWN_INSTANTIATE_TEST(BufferZeroInitTest,
- D3D11Backend({"nonzero_clear_resources_on_creation_for_testing",
- "timestamp_query_conversion_even_if_1ns"}),
+ D3D11Backend({"nonzero_clear_resources_on_creation_for_testing"}),
D3D11Backend({"auto_map_backend_buffer", "d3d11_disable_cpu_buffers",
- "nonzero_clear_resources_on_creation_for_testing",
- "timestamp_query_conversion_even_if_1ns"}),
- D3D12Backend({"nonzero_clear_resources_on_creation_for_testing",
- "timestamp_query_conversion_even_if_1ns"}),
- D3D12Backend({"nonzero_clear_resources_on_creation_for_testing",
- "timestamp_query_conversion_even_if_1ns"},
+ "nonzero_clear_resources_on_creation_for_testing"}),
+ D3D12Backend({"nonzero_clear_resources_on_creation_for_testing"}),
+ D3D12Backend({"nonzero_clear_resources_on_creation_for_testing"},
{"d3d12_create_not_zeroed_heap"}),
- MetalBackend({"nonzero_clear_resources_on_creation_for_testing",
- "timestamp_query_conversion_even_if_1ns"}),
- OpenGLBackend({"nonzero_clear_resources_on_creation_for_testing",
- "timestamp_query_conversion_even_if_1ns"}),
- OpenGLESBackend({"nonzero_clear_resources_on_creation_for_testing",
- "timestamp_query_conversion_even_if_1ns"}),
- VulkanBackend({"nonzero_clear_resources_on_creation_for_testing",
- "timestamp_query_conversion_even_if_1ns"}),
- WebGPUBackend({"nonzero_clear_resources_on_creation_for_testing",
- "timestamp_query_conversion_even_if_1ns"}));
+ MetalBackend({"nonzero_clear_resources_on_creation_for_testing"}),
+ OpenGLBackend({"nonzero_clear_resources_on_creation_for_testing"}),
+ OpenGLESBackend({"nonzero_clear_resources_on_creation_for_testing"}),
+ VulkanBackend({"nonzero_clear_resources_on_creation_for_testing"}),
+ WebGPUBackend({"nonzero_clear_resources_on_creation_for_testing"}));
} // anonymous namespace
} // namespace dawn
diff --git a/src/dawn/tests/white_box/GPUTimestampCalibrationTests.cpp b/src/dawn/tests/white_box/GPUTimestampCalibrationTests.cpp
index 8678862..439e81e 100644
--- a/src/dawn/tests/white_box/GPUTimestampCalibrationTests.cpp
+++ b/src/dawn/tests/white_box/GPUTimestampCalibrationTests.cpp
@@ -284,7 +284,11 @@
queue.Submit(1, &resolveCommands);
float errorToleranceRatio = 0.0f;
- if (!HasToggleEnabled("disable_timestamp_query_conversion")) {
+
+ // The internal shader runs if either timestamp conversion or quantization is enabled.
+ bool needsConversion = !HasToggleEnabled("disable_timestamp_query_conversion");
+ bool needsQuantization = HasToggleEnabled("timestamp_quantization");
+ if (needsConversion || needsQuantization) {
float period = mBackend->GetTimestampPeriod();
gpuTimestamp0 = static_cast<uint64_t>(static_cast<double>(gpuTimestamp0 * period));
gpuTimestamp1 = static_cast<uint64_t>(static_cast<double>(gpuTimestamp1 * period));
@@ -312,11 +316,12 @@
DAWN_INSTANTIATE_TEST_P(
GPUTimestampCalibrationTests,
- // Test with the disable_timestamp_query_conversion toggle forced on and off.
- {D3D12Backend({"disable_timestamp_query_conversion"}, {}),
- D3D12Backend({}, {"disable_timestamp_query_conversion"}),
- MetalBackend({"disable_timestamp_query_conversion"}, {}),
- MetalBackend({}, {"disable_timestamp_query_conversion"})},
+ // Test both with the timestamp quantization/conversion shader running and without. The shader
+ // runs if either timestamp conversion or quantization is enabled.
+ {D3D12Backend(),
+ D3D12Backend({"disable_timestamp_query_conversion"}, {"timestamp_quantization"}),
+ MetalBackend(),
+ MetalBackend({"disable_timestamp_query_conversion"}, {"timestamp_quantization"})},
{wgpu::FeatureName::TimestampQuery,
wgpu::FeatureName::ChromiumExperimentalTimestampQueryInsidePasses},
{EncoderType::NonPass, EncoderType::ComputePass, EncoderType::RenderPass});
Original Bug Report
WebGPU timestamp quantization bypass on hardware with 1ns timestamp period
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 Chrome Security team.
Overview: A logic error in Dawn’s command encoder bypasses the WebGPU timestamp quantization mitigation when the GPU hardware reports a timestamp period of exactly 1.0 nanosecond. An optimization check skips the internal compute pass responsible for applying the quantization mask, allowing malicious web content to read raw, unquantized high-resolution GPU ticks. This provides a precise timer primitive that can facilitate microarchitectural timing side-channel attacks.
Affected files:
third_party/dawn/src/dawn/native/CommandEncoder.cppthird_party/dawn/src/dawn/native/metal/DeviceMTL.mmthird_party/dawn/src/dawn/native/vulkan/DeviceVk.cpp
Estimated timestamp from git blame: 2026-01-26
Description
To mitigate high-resolution timing side-channel attacks, Dawn implements a TimestampQuantization toggle (enabled by default) that coarsens WebGPU timestamp queries. This is achieved by applying a quantization mask (kTimestampQuantizationMask = 0xFFFF0000) to the raw GPU ticks, reducing the timer resolution to approximately 65.5µs.
However, there is a potential bypass in CommandEncoder::APIResolveQuerySet (third_party/dawn/src/dawn/native/CommandEncoder.cpp). When resolving a timestamp query set, Dawn encodes an internal compute pipeline pass (EncodeTimestampsToNanosecondsConversion) to scale the raw hardware ticks into nanoseconds and apply the quantization mask.
To avoid an unnecessary compute pass when scaling is not mathematically required, the code includes the following optimization check:
// third_party/dawn/src/dawn/native/CommandEncoder.cpp
if (querySet->GetQueryType() == wgpu::QueryType::Timestamp &&
!GetDevice()->IsToggleEnabled(Toggle::DisableTimestampQueryConversion) &&
GetDevice()->GetTimestampPeriodInNS() != 1.0f) {
// ...
DAWN_TRY(EncodeTimestampsToNanosecondsConversion(...));
}
If GetTimestampPeriodInNS() == 1.0f, this condition fails and EncodeTimestampsToNanosecondsConversion is completely skipped. Because the quantization mask is only applied within the compute shader generated by this skipped pass, the backend executes a raw buffer copy (e.g., vkCmdCopyQueryPoolResults in Vulkan). The user’s buffer is populated with raw, unquantized GPU ticks.
This affects systems where the driver natively reports a timestamp period of exactly 1.0, most notably NVIDIA GPUs running on the Vulkan backend (e.g., on Linux, ChromeOS, and Android). It also temporarily affects the Metal backend, which initializes its timestamp period to 1.0f before a Kalman filter refines it.
Impact
An attacker can obtain ~1ns resolution GPU timers, completely defeating the intended ~65.5µs coarsening mitigation. This high-precision timer primitive can be used to mount GPU microarchitectural side-channel attacks, cache-timing attacks, and fine-grained device fingerprinting.
Suggested Reproduction Steps
Note: Our tooling agent does not have the ability to run code, so these are suggested steps to trigger the potential vulnerability.
- On a system with an NVIDIA GPU using the Vulkan backend, navigate to a page executing malicious JavaScript.
- The script requests a WebGPU device with the required feature enabled:
const device = await adapter.requestDevice({ requiredFeatures: ['timestamp-query'] }); - Create a
GPUQuerySetof type"timestamp"and aGPUBufferwithQUERY_RESOLVEandMAP_READusages. - Encode a compute or render pass and provide a
timestampWritesarray in the descriptor (e.g., recording timestamps atbeginningOfPassandendOfPass). - Resolve the queries by calling
commandEncoder.resolveQuerySet(querySet, 0, queryCount, resolveBuffer, 0);and submit the command buffer. - Map the resolve buffer to the CPU:
await resolveBuffer.mapAsync(GPUMapMode.READ); - Read the 64-bit integer values. Observe that the lower 16 bits are non-zero, proving that the
0xFFFF0000quantization mask was not applied.
Suggested Fix
The optimization check in CommandEncoder::APIResolveQuerySet should be modified so that it does not skip the compute pass if timestamp quantization is mandated, regardless of the timestamp period.
For example, the condition should be updated to execute the compute pass if either scaling is needed or quantization is enabled:
const bool needsScaling = GetDevice()->GetTimestampPeriodInNS() != 1.0f;
const bool needsQuantization = GetDevice()->IsToggleEnabled(Toggle::TimestampQuantization);
if (querySet->GetQueryType() == wgpu::QueryType::Timestamp &&
!GetDevice()->IsToggleEnabled(Toggle::DisableTimestampQueryConversion) &&
(needsScaling || needsQuantization)) {
// ... execute EncodeTimestampsToNanosecondsConversion
}
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
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.