CVE-2026-7983
Overview
Background
- Dawn / Tint
- Dawn is Chromium’s WebGPU implementation; Tint is its WGSL shader compiler, which includes an IR robustness transform that inserts bounds clamps into shaders.
- Robustness transform (`robustness.cc`)
- a Tint IR pass that clamps out-of-range indices (coordinates, mip levels, array indices) on texture operations so GPU accesses stay in bounds.
- `textureLoad` on multisampled textures
- a WGSL builtin that reads a texel from a
texture_multisampled_2d/texture_depth_multisampled_2dusing coordinates plus a sample index selecting one of the texture’s MSAA samples. - `textureNumSamples`
- the WGSL builtin returning a multisampled texture’s sample count; the inspector reports which texture bindings need this value so the backend can supply it.
Root Cause Analysis
For textureLoad on multisampled and depth-multisampled textures, the robustness transform clamped the coordinate argument (via clamp_coords) but never clamped the sample index argument, so an attacker-controlled out-of-range sample index reached the texture load unclamped. As the commit notes, an out-of-bounds sample index “returns an undefined value in some APIs, which could potentially produce uninitialized values,” i.e. an out-of-bounds/undefined read.
The fix adds a clamp_sample_index lambda that computes textureNumSamples(args[0]) - 1 as the limit and rewrites the sample-index operand to min(CastToU32(args[idx]), limit), and invokes it in the kTextureLoad handling when the texture IsAnyOf<type::MultisampledTexture, type::DepthMultisampledTexture>(). The regression IR in robustness_test.cc confirms the new lowering now emits textureNumSamples/sub/min for the sample index before textureLoad. Because clamping the sample index requires the shader to query textureNumSamples, the inspector (inspector.cc) is updated to mark such texture bindings in metadata.textures_with_num_samples (via a new uses_num_samples flag), so the GLSL backend binds the sample count.
textureLoad on multisampled textures able to read an out-of-bounds/undefined sample; the fix adds clamp_sample_index to min() it against textureNumSamples - 1.Attack Path
- Author a malicious WGSL shader
An attacker supplies a WebGPU shader that calls
textureLoadon atexture_multisampled_2d(or depth-multisampled) texture with an attacker-chosen sample index. - Compile through Tint
The pre-patch robustness transform clamps the coordinates but leaves the sample index unclamped, so the shader’s
textureLoadcarries an unbounded sample index into the generated backend shader. - Dispatch with an out-of-range sample index At draw/dispatch time the shader passes a sample index >= the texture’s sample count.
- Out-of-bounds / undefined read The GPU texture load returns an undefined value, which per the commit may be uninitialized memory, surfacing out-of-bounds data to the shader (the concrete backend read sink is not shown in this diff).
Impact Assessment
textureLoad, potentially exposing uninitialized GPU memory to the shader (and thus to the renderer) across the WebGPU attack surface. The precondition is only the ability to submit a WGSL shader and a multisampled texture binding. The vendor advisory rates this medium severity (“Out of bounds read in Dawn”) and does not report in-the-wild exploitation; any use of the leaked values beyond information disclosure is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
forsrc/tint/lang/wgsl/inspector/inspector.cc |
modified | |
ifsrc/tint/lang/wgsl/inspector/inspector.cc |
modified | |
TEST_Fsrc/tint/lang/wgsl/inspector/inspector_test.cc |
modified |
Files Changed
src/tint/lang/core/ir/transform/robustness.ccsrc/tint/lang/core/ir/transform/robustness_test.ccsrc/tint/lang/wgsl/inspector/inspector.ccsrc/tint/lang/wgsl/inspector/inspector_test.cc
Audit Directions
- Per-argument clamp coverageIn robustness/bounds-insertion passes, enumerate every index-like argument of each texture/buffer builtin (coords, level, array layer, sample index) and verify each has a clamp; omissions of newer argument kinds are the recurring bug.
- Overload-specific index argumentsAudit builtins whose argument set varies by texture type (multisampled vs sampled vs depth), since clamps written for the common overload often miss arguments that exist only on specialized overloads.
- Metadata driving a security clampWhere a bounds clamp depends on a runtime value (like
textureNumSamples), confirm the inspector/metadata path actually reports and binds that value for every relevant texture type, or the inserted clamp cannot function.
Patch
From 53ab80b614475f245e7dc6dbf882ce7b24247c98 Mon Sep 17 00:00:00 2001
From: James Price <jrprice@google.com>
Date: Thu, 02 Apr 2026 16:22:10 -0700
Subject: [PATCH] [tint] Clamp sample index for textureLoad on multisampled textures
An out-of-bounds sample index returns an undefined value in some APIs,
which could potentially produce uninitialized values.
Update the inspector to reflect the new usage of textureNumSamples to
Dawn for the GLSL backend.
Fixed: 497975608
Change-Id: I6f3dfc073d317516e6cdc94918182e1fd7a6cab9
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/300995
Reviewed-by: dan sinclair <dsinclair@chromium.org>
Commit-Queue: James Price <jrprice@google.com>
---
diff --git a/src/tint/lang/core/ir/transform/robustness.cc b/src/tint/lang/core/ir/transform/robustness.cc
index 82cc820..545fe2d 100644
--- a/src/tint/lang/core/ir/transform/robustness.cc
+++ b/src/tint/lang/core/ir/transform/robustness.cc
@@ -377,6 +377,14 @@
b.Min(CastToU32(args[idx]), limit)->Result());
};
+ // Helper for clamping the sample index.
+ auto clamp_sample_index = [&](uint32_t idx) {
+ auto* num_samples = b.Call(ty.u32(), core::BuiltinFn::kTextureNumSamples, args[0]);
+ auto* limit = b.Subtract(num_samples, 1_u);
+ call->SetOperand(CoreBuiltinCall::kArgsOperandOffset + idx,
+ b.Min(CastToU32(args[idx]), limit)->Result());
+ };
+
// Select which arguments to clamp based on the function overload.
switch (call->Func()) {
case core::BuiltinFn::kTextureDimensions: {
@@ -393,6 +401,9 @@
if (texture->IsAnyOf<type::SampledTexture, type::DepthTexture>()) {
clamp_level(next_arg++);
}
+ if (texture->IsAnyOf<type::MultisampledTexture, type::DepthMultisampledTexture>()) {
+ clamp_sample_index(next_arg++);
+ }
clamp_coords(1u); // Must run after clamp_level
break;
}
diff --git a/src/tint/lang/core/ir/transform/robustness_test.cc b/src/tint/lang/core/ir/transform/robustness_test.cc
index 4af13d0..01f13ed 100644
--- a/src/tint/lang/core/ir/transform/robustness_test.cc
+++ b/src/tint/lang/core/ir/transform/robustness_test.cc
@@ -2339,22 +2339,29 @@
%load_signed = func(%coords:vec2<i32>, %level:i32):vec4<f32> {
$B2: {
%5:texture_multisampled_2d<f32> = load %texture
- %6:vec2<u32> = textureDimensions %5
- %7:vec2<u32> = sub %6, vec2<u32>(1u)
- %8:vec2<u32> = convert %coords
- %9:vec2<u32> = min %8, %7
- %10:vec4<f32> = textureLoad %5, %9, %level
- ret %10
+ %6:u32 = textureNumSamples %5
+ %7:u32 = sub %6, 1u
+ %8:u32 = convert %level
+ %9:u32 = min %8, %7
+ %10:vec2<u32> = textureDimensions %5
+ %11:vec2<u32> = sub %10, vec2<u32>(1u)
+ %12:vec2<u32> = convert %coords
+ %13:vec2<u32> = min %12, %11
+ %14:vec4<f32> = textureLoad %5, %13, %9
+ ret %14
}
}
%load_unsigned = func(%coords_1:vec2<u32>, %level_1:u32):vec4<f32> { # %coords_1: 'coords', %level_1: 'level'
$B3: {
- %14:texture_multisampled_2d<f32> = load %texture
- %15:vec2<u32> = textureDimensions %14
- %16:vec2<u32> = sub %15, vec2<u32>(1u)
- %17:vec2<u32> = min %coords_1, %16
- %18:vec4<f32> = textureLoad %14, %17, %level_1
- ret %18
+ %18:texture_multisampled_2d<f32> = load %texture
+ %19:u32 = textureNumSamples %18
+ %20:u32 = sub %19, 1u
+ %21:u32 = min %level_1, %20
+ %22:vec2<u32> = textureDimensions %18
+ %23:vec2<u32> = sub %22, vec2<u32>(1u)
+ %24:vec2<u32> = min %coords_1, %23
+ %25:vec4<f32> = textureLoad %18, %24, %21
+ ret %25
}
}
)";
@@ -2626,22 +2633,29 @@
%load_signed = func(%coords:vec2<i32>, %index:i32):f32 {
$B2: {
%5:texture_depth_multisampled_2d = load %texture
- %6:vec2<u32> = textureDimensions %5
- %7:vec2<u32> = sub %6, vec2<u32>(1u)
- %8:vec2<u32> = convert %coords
- %9:vec2<u32> = min %8, %7
- %10:f32 = textureLoad %5, %9, %index
- ret %10
+ %6:u32 = textureNumSamples %5
+ %7:u32 = sub %6, 1u
+ %8:u32 = convert %index
+ %9:u32 = min %8, %7
+ %10:vec2<u32> = textureDimensions %5
+ %11:vec2<u32> = sub %10, vec2<u32>(1u)
+ %12:vec2<u32> = convert %coords
+ %13:vec2<u32> = min %12, %11
+ %14:f32 = textureLoad %5, %13, %9
+ ret %14
}
}
%load_unsigned = func(%coords_1:vec2<u32>, %index_1:u32):f32 { # %coords_1: 'coords', %index_1: 'index'
$B3: {
- %14:texture_depth_multisampled_2d = load %texture
- %15:vec2<u32> = textureDimensions %14
- %16:vec2<u32> = sub %15, vec2<u32>(1u)
- %17:vec2<u32> = min %coords_1, %16
- %18:f32 = textureLoad %14, %17, %index_1
- ret %18
+ %18:texture_depth_multisampled_2d = load %texture
+ %19:u32 = textureNumSamples %18
+ %20:u32 = sub %19, 1u
+ %21:u32 = min %index_1, %20
+ %22:vec2<u32> = textureDimensions %18
+ %23:vec2<u32> = sub %22, vec2<u32>(1u)
+ %24:vec2<u32> = min %coords_1, %23
+ %25:f32 = textureLoad %18, %24, %21
+ ret %25
}
}
)";
diff --git a/src/tint/lang/wgsl/inspector/inspector.cc b/src/tint/lang/wgsl/inspector/inspector.cc
index b8b0480..f9ecbad 100644
--- a/src/tint/lang/wgsl/inspector/inspector.cc
+++ b/src/tint/lang/wgsl/inspector/inspector.cc
@@ -639,6 +639,7 @@
}
bool uses_num_levels = false;
+ bool uses_num_samples = false;
switch (builtin->Fn()) {
case wgsl::BuiltinFn::kTextureNumLevels:
uses_num_levels = true;
@@ -656,6 +657,8 @@
uses_num_levels = !texture_type->IsAnyOf<core::type::MultisampledTexture,
core::type::DepthMultisampledTexture,
core::type::ExternalTexture>();
+ uses_num_samples = texture_type->IsAnyOf<core::type::MultisampledTexture,
+ core::type::DepthMultisampledTexture>();
metadata.has_texture_load_with_depth_texture |=
texture_type
->IsAnyOf<core::type::DepthTexture, core::type::DepthMultisampledTexture>();
@@ -670,10 +673,7 @@
break;
case wgsl::BuiltinFn::kTextureNumSamples:
- for (const auto* texture : textures) {
- auto texture_binding_point = texture->Attributes().binding_point.value();
- metadata.textures_with_num_samples.insert(texture_binding_point);
- }
+ uses_num_samples = true;
break;
default:
@@ -686,6 +686,12 @@
metadata.textures_with_num_levels.insert(texture_binding_point);
}
}
+ if (uses_num_samples) {
+ for (const auto* texture : textures) {
+ auto texture_binding_point = texture->Attributes().binding_point.value();
+ metadata.textures_with_num_samples.insert(texture_binding_point);
+ }
+ }
};
// Iterate the call graph in reverse topological order such that function callers come before
diff --git a/src/tint/lang/wgsl/inspector/inspector_test.cc b/src/tint/lang/wgsl/inspector/inspector_test.cc
index 09215dd..6c97476 100644
--- a/src/tint/lang/wgsl/inspector/inspector_test.cc
+++ b/src/tint/lang/wgsl/inspector/inspector_test.cc
@@ -4285,7 +4285,11 @@
Inspector& inspector = Initialize(shader);
auto info = inspector.GetTextureQueries("main");
- ASSERT_EQ(0u, info.size());
+ ASSERT_EQ(1u, info.size());
+
+ EXPECT_EQ(Inspector::TextureQueryType::kTextureNumSamples, info[0].type);
+ EXPECT_EQ(2u, info[0].group);
+ EXPECT_EQ(3u, info[0].binding);
}
TEST_F(InspectorTextureTest, TextureLoadMultipleInEP) {
@@ -4306,7 +4310,7 @@
Inspector& inspector = Initialize(shader);
Regression Test / PoC
diff --git a/src/tint/lang/core/ir/transform/robustness_test.cc b/src/tint/lang/core/ir/transform/robustness_test.cc
index 4af13d0..01f13ed 100644
--- a/src/tint/lang/core/ir/transform/robustness_test.cc
+++ b/src/tint/lang/core/ir/transform/robustness_test.cc
@@ -2339,22 +2339,29 @@
%load_signed = func(%coords:vec2<i32>, %level:i32):vec4<f32> {
$B2: {
%5:texture_multisampled_2d<f32> = load %texture
- %6:vec2<u32> = textureDimensions %5
- %7:vec2<u32> = sub %6, vec2<u32>(1u)
- %8:vec2<u32> = convert %coords
- %9:vec2<u32> = min %8, %7
- %10:vec4<f32> = textureLoad %5, %9, %level
- ret %10
+ %6:u32 = textureNumSamples %5
+ %7:u32 = sub %6, 1u
+ %8:u32 = convert %level
+ %9:u32 = min %8, %7
+ %10:vec2<u32> = textureDimensions %5
+ %11:vec2<u32> = sub %10, vec2<u32>(1u)
+ %12:vec2<u32> = convert %coords
+ %13:vec2<u32> = min %12, %11
+ %14:vec4<f32> = textureLoad %5, %13, %9
+ ret %14
}
}
%load_unsigned = func(%coords_1:vec2<u32>, %level_1:u32):vec4<f32> { # %coords_1: 'coords', %level_1: 'level'
$B3: {
- %14:texture_multisampled_2d<f32> = load %texture
- %15:vec2<u32> = textureDimensions %14
- %16:vec2<u32> = sub %15, vec2<u32>(1u)
- %17:vec2<u32> = min %coords_1, %16
- %18:vec4<f32> = textureLoad %14, %17, %level_1
- ret %18
+ %18:texture_multisampled_2d<f32> = load %texture
+ %19:u32 = textureNumSamples %18
+ %20:u32 = sub %19, 1u
+ %21:u32 = min %level_1, %20
+ %22:vec2<u32> = textureDimensions %18
+ %23:vec2<u32> = sub %22, vec2<u32>(1u)
+ %24:vec2<u32> = min %coords_1, %23
+ %25:vec4<f32> = textureLoad %18, %24, %21
+ ret %25
}
}
)";
@@ -2626,22 +2633,29 @@
%load_signed = func(%coords:vec2<i32>, %index:i32):f32 {
$B2: {
%5:texture_depth_multisampled_2d = load %texture
- %6:vec2<u32> = textureDimensions %5
- %7:vec2<u32> = sub %6, vec2<u32>(1u)
- %8:vec2<u32> = convert %coords
- %9:vec2<u32> = min %8, %7
- %10:f32 = textureLoad %5, %9, %index
- ret %10
+ %6:u32 = textureNumSamples %5
+ %7:u32 = sub %6, 1u
+ %8:u32 = convert %index
+ %9:u32 = min %8, %7
+ %10:vec2<u32> = textureDimensions %5
+ %11:vec2<u32> = sub %10, vec2<u32>(1u)
+ %12:vec2<u32> = convert %coords
+ %13:vec2<u32> = min %12, %11
+ %14:f32 = textureLoad %5, %13, %9
+ ret %14
}
}
%load_unsigned = func(%coords_1:vec2<u32>, %index_1:u32):f32 { # %coords_1: 'coords', %index_1: 'index'
$B3: {
- %14:texture_depth_multisampled_2d = load %texture
- %15:vec2<u32> = textureDimensions %14
- %16:vec2<u32> = sub %15, vec2<u32>(1u)
- %17:vec2<u32> = min %coords_1, %16
- %18:f32 = textureLoad %14, %17, %index_1
- ret %18
+ %18:texture_depth_multisampled_2d = load %texture
+ %19:u32 = textureNumSamples %18
+ %20:u32 = sub %19, 1u
+ %21:u32 = min %index_1, %20
+ %22:vec2<u32> = textureDimensions %18
+ %23:vec2<u32> = sub %22, vec2<u32>(1u)
+ %24:vec2<u32> = min %coords_1, %23
+ %25:f32 = textureLoad %18, %24, %21
+ ret %25
}
}
)";
diff --git a/src/tint/lang/wgsl/inspector/inspector_test.cc b/src/tint/lang/wgsl/inspector/inspector_test.cc
index 09215dd..6c97476 100644
--- a/src/tint/lang/wgsl/inspector/inspector_test.cc
+++ b/src/tint/lang/wgsl/inspector/inspector_test.cc
@@ -4285,7 +4285,11 @@
Inspector& inspector = Initialize(shader);
auto info = inspector.GetTextureQueries("main");
- ASSERT_EQ(0u, info.size());
+ ASSERT_EQ(1u, info.size());
+
+ EXPECT_EQ(Inspector::TextureQueryType::kTextureNumSamples, info[0].type);
+ EXPECT_EQ(2u, info[0].group);
+ EXPECT_EQ(3u, info[0].binding);
}
TEST_F(InspectorTextureTest, TextureLoadMultipleInEP) {
@@ -4306,7 +4310,7 @@
Inspector& inspector = Initialize(shader);
auto info = inspector.GetTextureQueries("main");
- ASSERT_EQ(2u, info.size());
+ ASSERT_EQ(3u, info.size());
Inspector::LevelSampleInfo info1 = {
/*type */ Inspector::TextureQueryType::kTextureNumLevels,
@@ -4318,7 +4322,12 @@
/*group*/ 2,
/*binding*/ 3,
};
- EXPECT_THAT(info, testing::UnorderedElementsAre(info1, info2));
+ Inspector::LevelSampleInfo info3 = {
+ /*type */ Inspector::TextureQueryType::kTextureNumSamples,
+ /*group*/ 1,
+ /*binding*/ 4,
+ };
+ EXPECT_THAT(info, testing::UnorderedElementsAre(info1, info2, info3));
}
TEST_F(InspectorTextureTest, TextureInSubfunction) {
Original Bug Report
OOB Read via Unclamped sample_index in Multisampled textureLoad
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: Tint’s Robustness transform fails to clamp the sample_index argument for textureLoad operations on multisampled textures. This allows an attacker to pass an out-of-bounds sample index to the Metal and OpenGL backends, potentially leading to an out-of-bounds read of adjacent or stale GPU memory and a cross-origin information leak.
Affected files:
third_party/dawn/src/tint/lang/core/ir/transform/robustness.cc
Estimated timestamp from git blame: 2024-11-12
Summary
Tint’s IR Robustness transform (third_party/dawn/src/tint/lang/core/ir/transform/robustness.cc) incorrectly skips clamping the sample_index argument for textureLoad operations on multisampled textures (specifically texture_multisampled_2d<T> and texture_depth_multisampled_2d). This omission results in a potential out-of-bounds (OOB) read vulnerability on the GPU when using WebGPU on platforms where Tint is the primary robustness mechanism, such as macOS/iOS (Metal) and OpenGL.
Technical Details
In third_party/dawn/src/tint/lang/core/ir/transform/robustness.cc, the function ClampTextureCallArgs contains logic for handling textureLoad built-ins. For multisampled textures, the sample_index is the third argument (args[2]). The current clamping logic is as follows:
case core::BuiltinFn::kTextureLoad: {
uint32_t next_arg = 2u;
if (type::IsTextureArray(texture->Dim())) {
clamp_array_index(next_arg++);
}
if (texture->IsAnyOf<type::SampledTexture, type::DepthTexture>()) {
clamp_level(next_arg++);
}
clamp_coords(1u); // Must run after clamp_level
break;
}
The texture->IsAnyOf<type::SampledTexture, type::DepthTexture>() check incorrectly excludes multisampled texture types. In the Tint type system, MultisampledTexture and DepthMultisampledTexture are final classes that derive directly from the base Texture class and are not instances of SampledTexture or DepthTexture.
Consequently, for multisampled textures, the transform evaluates this condition as false and entirely skips clamping the argument at next_arg (index 2).
Since no clamp_sample_index helper is implemented in the transform and kTextureNumSamples is never queried to determine the valid range, the sample_index value remains completely unmodified in the generated Tint IR. This behavior is explicitly reflected in Tint’s golden tests (e.g., TextureLoad_Multisampled2D in robustness_test.cc), which show coordinates being safely clamped while the sample index remains raw.
Impact
When lowered to Metal Shading Language (MSL) or GLSL, the unclamped sample_index is passed directly to the underlying driver functions (e.g., texture.read(coords, sample_index) in MSL or texelFetch() in GLSL). Both the MSL and GLSL specifications define out-of-bounds sample indices as undefined behavior.
On many GPUs, particularly those with Tile Based Deferred Rendering (TBDR) architectures like Apple Silicon, an out-of-bounds sample read causes the hardware sampling unit to calculate an offset far outside the memory region allocated for that pixel or tile. This returns stale data from adjacent tile memory or other GPU device memory allocations. This allows a malicious WebGPU-enabled website to perform an information leak of highly sensitive, cross-origin texture data or other GPU-resident information without triggering crashes or driver errors.
Potential Reproduction Steps
(Note: These are potential steps as our tooling agent cannot execute code to verify the exploit end-to-end.)
- On a system using Metal (macOS) or OpenGL, open a browser with WebGPU enabled.
- Create a multisampled texture (
texture_multisampled_2d<f32>). - Create a compute shader that calls
textureLoad(tex, vec2<i32>(0,0), sample_index). Source thesample_indexfrom an attacker-controlled storage buffer. - Populate the storage buffer with a massively out-of-bounds value (e.g.,
0x7FFFFFFF). - Execute the compute pass and write the result of the
textureLoadcall to an output storage buffer. - Map the output buffer and read it back to the CPU via JavaScript. Observe that the returned values contain undefined data from adjacent GPU memory, violating the WebGPU specification’s out-of-bounds safety guarantees.
Suggested Fix
Update ClampTextureCallArgs in third_party/dawn/src/tint/lang/core/ir/transform/robustness.cc to explicitly handle multisampled textures.
- Implement a
clamp_sample_indexhelper lambda that queriescore::BuiltinFn::kTextureNumSamplesto determine the maximum valid sample index for the given texture (num_samples - 1). - Add a check
if (texture->IsAnyOf<type::MultisampledTexture, type::DepthMultisampledTexture>())within thekTextureLoadcase block to call this newclamp_sample_indexhelper on the appropriate argument index (next_arg).
Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0
Results from 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.