CVE-2026-7346
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/tint/lang/core/ir/analysis/loop_analysis.cc |
modified | |
whiletest/tint/bug/tint/1121.wgsl.expected.dxc.hlsl |
modified |
Files Changed
src/tint/lang/core/ir/analysis/loop_analysis.ccsrc/tint/lang/core/ir/analysis/loop_analysis_test.cctest/tint/bug/tint/1121.wgsl.expected.dxc.hlsl
Patch
From 4bb845fa408205b58a060f2b474cecfdb3d53fe0 Mon Sep 17 00:00:00 2001 From: James Price <[email protected]> Date: Wed, 15 Apr 2026 12:43:37 -0700 Subject: [PATCH] [tint] Fix loop analysis for limit cases We handled limit cases for >= and <=, but not for > and <. This causes us to add infinite loop mitigations for loops that store their bounds in a `let` (see the modified E2E tests), since we conservatively assume that the let may contain the problematic limit value. With some extra work we should be able to make the analysis avoid these false positives for common cases, but that will be left to a future CL. Fixed: 502206907 Change-Id: I30224cdc47125e7603d3dc07daf0619c886f92e4 Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/302776 Reviewed-by: dan sinclair <[email protected]> Commit-Queue: James Price <[email protected]> --- diff --git a/src/tint/lang/core/ir/analysis/loop_analysis.cc b/src/tint/lang/core/ir/analysis/loop_analysis.cc index 96b663a..6a3b48f 100644 --- a/src/tint/lang/core/ir/analysis/loop_analysis.cc +++ b/src/tint/lang/core/ir/analysis/loop_analysis.cc @@ -195,7 +195,7 @@ [&](CoreBuiltinCall* c) { return c->Func() == core::BuiltinFn::kBitcast; }, // [&](Binary*) { return true; }, // [&](If* i) { - if (IsBreakIfOnIndex(loop, i, index)) { + if (IsBreakIfOnIndex(i, index)) { // The loop is finite. has_break_if = true; } @@ -211,7 +211,7 @@ } /// @returns `true` if @p is a break-if construct that exits the loop based on @p index. - bool IsBreakIfOnIndex(const Loop& loop, If* i, Var& index) { + bool IsBreakIfOnIndex(If* i, Var& index) { // Returns `true` if the given value is a load of the index variable. auto is_index = [&index](Value* v) { if (auto* load = As<Load>(UnwrapBitcast(v))) { @@ -219,18 +219,6 @@ } return false; }; - // Returns `true` if the given value an immutable value declared before the loop body. - auto is_immutable_before_body = [&loop](Value* v) { - return tint::Switch( - UnwrapBitcast(v), // - [](ir::Constant*) { return true; }, // - [](ir::FunctionParam*) { return true; }, // - [&](ir::InstructionResult* r) { - auto* let = r->Instruction()->As<Let>(); - return let && let->Block() != loop.Body(); - } // - ); - }; auto is_constant_i32_or_u32 = [](Value* v) { auto* constant_value = v->As<Constant>(); if (!constant_value) { @@ -240,10 +228,53 @@ }; auto is_capable_binary_for_loop_exit = [&](Binary* binary) { switch (binary->Op()) { - case BinaryOp::kLessThan: + case BinaryOp::kLessThan: { + if (is_index(binary->LHS()) && is_constant_i32_or_u32(binary->RHS())) { + // index < kConstantValue + // If `kConstantValue` is the lowest possible value then the expression is + // always false. + auto* constant_value = binary->RHS()->As<Constant>()->Value(); + if (constant_value->Type()->Is<type::I32>()) { + return constant_value->ValueAs<int32_t>() > i32::kLowestValue; + } + TINT_ASSERT(constant_value->Type()->Is<type::U32>()); + return constant_value->ValueAs<uint32_t>() > u32::kLowestValue; + } else if (is_index(binary->RHS()) && is_constant_i32_or_u32(binary->LHS())) { + // kConstantValue < index + // If `kConstantValue` is the highest possible value then the expression is + // always false. + auto* constant_value = binary->LHS()->As<Constant>()->Value(); + if (constant_value->Type()->Is<type::I32>()) { + return constant_value->ValueAs<int32_t>() < i32::kHighestValue; + } + TINT_ASSERT(constant_value->Type()->Is<type::U32>()); + return constant_value->ValueAs<uint32_t>() < u32::kHighestValue; + } + return false; + } case BinaryOp::kGreaterThan: { - return (is_index(binary->LHS()) && is_immutable_before_body(binary->RHS())) || - (is_index(binary->RHS()) && is_immutable_before_body(binary->LHS())); + if (is_index(binary->LHS()) && is_constant_i32_or_u32(binary->RHS())) { + // index > kConstantValue + // If `kConstantValue` is the highest possible value then the expression is + // always false. + auto* constant_value = binary->RHS()->As<Constant>()->Value(); + if (constant_value->Type()->Is<type::I32>()) { + return constant_value->ValueAs<int32_t>() < i32::kHighestValue; + } + TINT_ASSERT(constant_value->Type()->Is<type::U32>()); + return constant_value->ValueAs<uint32_t>() < u32::kHighestValue; + } else if (is_index(binary->RHS()) && is_constant_i32_or_u32(binary->LHS())) { + // kConstantValue > index + // If `kConstantValue` is the lowest possible value then the expression is + // always false. + auto* constant_value = binary->LHS()->As<Constant>()->Value(); + if (constant_value->Type()->Is<type::I32>()) { + return constant_value->ValueAs<int32_t>() > i32::kLowestValue; + } + TINT_ASSERT(constant_value->Type()->Is<type::U32>()); + return constant_value->ValueAs<uint32_t>() > u32::kLowestValue; + } + return false; } case BinaryOp::kLessThanEqual: { if (is_index(binary->LHS()) && is_constant_i32_or_u32(binary->RHS())) { diff --git a/src/tint/lang/core/ir/analysis/loop_analysis_test.cc b/src/tint/lang/core/ir/analysis/loop_analysis_test.cc index 88362d0..5923aa7 100644 --- a/src/tint/lang/core/ir/analysis/loop_analysis_test.cc +++ b/src/tint/lang/core/ir/analysis/loop_analysis_test.cc @@ -262,6 +262,16 @@ // Comparing the index to a constant that is at a limit can result in // an always-true or always-false result, which is not OK. + Bound<i32, Index, kLessThan, INT32_MIN>(false), + Bound<u32, Index, kLessThan, 0>(false), + Bound<i32, INT32_MAX, kLessThan, Index>(false), + Bound<u32, UINT32_MAX, kLessThan, Index>(false), + + Bound<i32, Index, kGreaterThan, INT32_MAX>(false), + Bound<u32, Index, kGreaterThan, UINT32_MAX>(false), + Bound<i32, INT32_MIN, kGreaterThan, Index>(false), + Bound<u32, 0, kGreaterThan, Index>(false), + Bound<i32, Index, kLessThanEqual, INT32_MAX>(false), Bound<u32, Index, kLessThanEqual, UINT32_MAX>(false), Bound<i32, INT32_MIN, kLessThanEqual, Index>(false), @@ -272,9 +282,26 @@ Bound<i32, INT32_MAX, kGreaterThanEqual, Index>(false), Bound<u32, UINT32_MAX, kGreaterThanEqual, Index>(false), - // Using other immutable values for the bound is not OK for some - // comparison operators, since that value could result in an - // always-true or always-false outcome (as above). + // Using other immutable values for the bound is not OK since that + // value could result in an always-true or always-false outcome. + Bound<i32, Index, kLessThan, kFunctionParam>(false), + Bound<i32, Index, kLessThan, kLet>(false), + Bound<i32, kFunctionParam, kLessThan, Index>(false), + Bound<i32, kLet, kLessThan, Index>(false), + Bound<u32, Index, kLessThan, kFunctionParam>(false), + Bound<u32, Index, kLessThan, kLet>(false), + Bound<u32, kFunctionParam, kLessThan, Index>(false), + Bound<u32, kLet, kLessThan, Index>(false), + + Bound<i32, Index, kGreaterThan, kFunctionParam>(false), + Bound<i32, Index, kGreaterThan, kLet>(false), + Bound<i32, kFunctionParam, kGreaterThan, Index>(false), + Bound<i32, kLet, kGreaterThan, Index>(false), + Bound<u32, Index, kGreaterThan, kFunctionParam>(false), + Bound<u32, Index, kGreaterThan, kLet>(false), + Bound<u32, kFunctionParam, kGreaterThan, Index>(false), + Bound<u32, kLet, kGreaterThan, Index>(false), + Bound<i32, Index, kLessThanEqual, kFunctionParam>(false), Bound<i32, Index, kLessThanEqual, kLet>(false), Bound<i32, kFunctionParam, kLessThanEqual, Index>(false), diff --git a/test/tint/bug/tint/1121.wgsl.expected.dxc.hlsl b/test/tint/bug/tint/1121.wgsl.expected.dxc.hlsl index 9fde89f..b31444f 100644 --- a/test/tint/bug/tint/1121.wgsl.expected.dxc.hlsl +++ b/test/tint/bug/tint/1121.wgsl.expected.dxc.hlsl @@ -58,15 +58,23 @@ int TILE_COUNT_X = int(2); int TILE_COUNT_Y = int(2); { + uint2 tint_loop_idx = (4294967295u).xx; int y = int(0); while(true) { + if (all((tint_loop_idx == (0u).xx))) { + break; + } if ((y < TILE_COUNT_Y)) { } else { break; } { + uint2 tint_loop_idx_1 = (4294967295u).xx; int x = int(0); while(true) { + if (all((tint_loop_idx_1 == (0u).xx))) { + break; + } if ((x < TILE_COUNT_X)) { } else { break; @@ -130,6 +138,10 @@ }
Regression Test / PoC
diff --git a/src/tint/lang/core/ir/analysis/loop_analysis_test.cc b/src/tint/lang/core/ir/analysis/loop_analysis_test.cc
index 88362d0..5923aa7 100644
--- a/src/tint/lang/core/ir/analysis/loop_analysis_test.cc
+++ b/src/tint/lang/core/ir/analysis/loop_analysis_test.cc
@@ -262,6 +262,16 @@
// Comparing the index to a constant that is at a limit can result in
// an always-true or always-false result, which is not OK.
+ Bound<i32, Index, kLessThan, INT32_MIN>(false),
+ Bound<u32, Index, kLessThan, 0>(false),
+ Bound<i32, INT32_MAX, kLessThan, Index>(false),
+ Bound<u32, UINT32_MAX, kLessThan, Index>(false),
+
+ Bound<i32, Index, kGreaterThan, INT32_MAX>(false),
+ Bound<u32, Index, kGreaterThan, UINT32_MAX>(false),
+ Bound<i32, INT32_MIN, kGreaterThan, Index>(false),
+ Bound<u32, 0, kGreaterThan, Index>(false),
+
Bound<i32, Index, kLessThanEqual, INT32_MAX>(false),
Bound<u32, Index, kLessThanEqual, UINT32_MAX>(false),
Bound<i32, INT32_MIN, kLessThanEqual, Index>(false),
@@ -272,9 +282,26 @@
Bound<i32, INT32_MAX, kGreaterThanEqual, Index>(false),
Bound<u32, UINT32_MAX, kGreaterThanEqual, Index>(false),
- // Using other immutable values for the bound is not OK for some
- // comparison operators, since that value could result in an
- // always-true or always-false outcome (as above).
+ // Using other immutable values for the bound is not OK since that
+ // value could result in an always-true or always-false outcome.
+ Bound<i32, Index, kLessThan, kFunctionParam>(false),
+ Bound<i32, Index, kLessThan, kLet>(false),
+ Bound<i32, kFunctionParam, kLessThan, Index>(false),
+ Bound<i32, kLet, kLessThan, Index>(false),
+ Bound<u32, Index, kLessThan, kFunctionParam>(false),
+ Bound<u32, Index, kLessThan, kLet>(false),
+ Bound<u32, kFunctionParam, kLessThan, Index>(false),
+ Bound<u32, kLet, kLessThan, Index>(false),
+
+ Bound<i32, Index, kGreaterThan, kFunctionParam>(false),
+ Bound<i32, Index, kGreaterThan, kLet>(false),
+ Bound<i32, kFunctionParam, kGreaterThan, Index>(false),
+ Bound<i32, kLet, kGreaterThan, Index>(false),
+ Bound<u32, Index, kGreaterThan, kFunctionParam>(false),
+ Bound<u32, Index, kGreaterThan, kLet>(false),
+ Bound<u32, kFunctionParam, kGreaterThan, Index>(false),
+ Bound<u32, kLet, kGreaterThan, Index>(false),
+
Bound<i32, Index, kLessThanEqual, kFunctionParam>(false),
Bound<i32, Index, kLessThanEqual, kLet>(false),
Bound<i32, kFunctionParam, kLessThanEqual, Index>(false),
Original Bug Report
Bypass of PreventInfiniteLoops via flawed LoopAnalysis leads to OOB GPU memory access
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: Tint’s LoopAnalysis misidentifies certain dynamically infinite loops as finite when they use degenerate bounds (e.g., i < 0u). This bypasses the PreventInfiniteLoops transform, allowing an attacker to trigger Undefined Behavior optimizations in downstream driver compilers that strip security-critical memory bounds checks.
Affected files:
third_party/dawn/src/tint/lang/core/ir/analysis/loop_analysis.cc
Estimated timestamp from git blame: 2025-07-21
Summary
Tint’s LoopAnalysis, which determines if a loop is finite for the PreventInfiniteLoops security transform, contains a logic flaw that misclassifies certain infinite loops as finite. This occurs when a loop exit condition uses a strict inequality comparison (< or >) with a degenerate bound (e.g., idx < 0u for an unsigned index) and fails to verify if the exit branch is actually reachable. This flaw allows an attacker to bypass the mandatory 2^64-iteration cap injection, which in turn allows them to leverage Undefined Behavior (UB) optimizations in downstream driver compilers to bypass Robustness bounds checks.
Root Cause
The vulnerability arises from two interacting gaps in third_party/dawn/src/tint/lang/core/ir/analysis/loop_analysis.cc:
-
Missing Degenerate-Bound Checks for
<and>(lines 243-247): Theis_capable_binary_for_loop_exitfunction acceptskLessThanandkGreaterThanoperators if one operand is the loop index and the other is an immutable value. Unlike the implementation for<=and>=, it does not check if the bound value makes the comparison always false (e.g.,index < 0uis always false for au32index). -
Uncorrelated Exit Branch Polarity (line 313): The logic at line 313 considers a loop finite if either the True or False branch of an
ifinstruction leads to anExitLoop:return is_simple_loop_exit(i->True()) || is_simple_loop_exit(i->False());This does not account for the condition’s polarity. Thus, anif (idx < 0u) { break; }statement is treated as a valid termination even though thebreak(the True branch) is never reachable.
Potential Attack Vector
An attacker can construct a WGSL shader that exploits this flaw to achieve out-of-bounds (OOB) memory access on the GPU. The following steps outline the theoretical attack mechanism:
- Attacker Input: The attacker provides a WGSL shader similar to the following:
var my_idx = user_provided_index; if (my_idx >= array_size) { // Syntactically "finite" but dynamically infinite loop for (var i: u32 = 1u; ; i = i + 1u) { if (i < 0u) { break; } } } // Access array my_array[my_idx] = 1; - Robustness Transform: Tint’s
Robustnesstransform executes and replacesmy_array[my_idx]with clamped indexing:my_array[min(my_idx, array_size - 1)]. - PreventInfiniteLoops Bypass:
LoopAnalysismisclassifies the inner loop as finite. Consequently, thePreventInfiniteLoopstransform is bypassed, and no iteration cap is injected into the loop. - Driver Compiler UB Optimization: The emitted shader is handed to the downstream driver compiler (e.g., an LLVM-based compiler like Apple’s Metal compiler or DXC). The driver compiler statically determines that the inner loop is infinite and has no side effects.
- Bounds Check Stripping: Under C++/LLVM semantics, an infinite loop without side effects is Undefined Behavior. The compiler assumes the program will never enter this UB state, marking the
if (my_idx >= array_size)branch as unreachable. It then propagates the inverse constraint (my_idx < array_size) globally. When evaluating the robustness checkmin(my_idx, array_size - 1), it determines theminoperation is redundant and optimizes it away to justmy_idx. - OOB Execution: The compiler removes the unreachable infinite loop. At runtime, if the attacker provides a
my_idxgreater than or equal toarray_size, execution falls through to the array access. Because theminclamping was stripped, the array access is performed out-of-bounds.
Proposed Fix
Update is_capable_binary_for_loop_exit in loop_analysis.cc to correctly reject degenerate bounds for kLessThan and kGreaterThan operations, similar to the existing logic for kLessThanEqual and kGreaterThanEqual. Additionally, IsBreakIfOnIndex should verify that the ExitLoop resides in the branch that corresponds to the index actually reaching the bound.
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.