CVE-2026-7983
Overview
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
Patch
From 53ab80b614475f245e7dc6dbf882ce7b24247c98 Mon Sep 17 00:00:00 2001 From: James Price <[email protected]> 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 <[email protected]> Commit-Queue: James Price <[email protected]> --- 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.