CVE-2026-7346
Overview
Background
- Tint
- the WebGPU shader compiler in Dawn that translates WGSL into backend shading languages (HLSL/MSL/SPIR-V) and applies safety transforms.
- Infinite-loop mitigation
- a Tint transform that injects a bounded counter (e.g.
tint_loop_idx) into loops it cannot prove terminate, forcing termination to prevent GPU hangs; skipping it on a truly non-terminating loop is the hazard. - `loop_analysis` / `IsBreakIfOnIndex`
- the analysis that decides whether a loop’s exit condition provably makes the loop finite by matching a
break-ifthat compares the loop index against a bound. - `is_capable_binary_for_loop_exit`
- the predicate that returns
truewhen a comparison binary (<,>,<=,>=) between the index and a bound is a valid finiteness witness;truemeans “no mitigation needed.”
Root Cause Analysis
is_capable_binary_for_loop_exit already guarded the <=/>= cases by requiring a constant bound (is_constant_i32_or_u32) and rejecting constants at the type limit, where the comparison would be always-true/always-false and the loop would never exit. The < and > cases did not: they used is_immutable_before_body, which returns true for any constant, function parameter, or pre-loop let, so a </> comparison against such a value was accepted as a finiteness witness without checking for a limit value. Consequently a loop like index < INT32_MIN (always false, never breaks) or one whose bound is a let/param that could hold the limit value was treated as finite, and the infinite-loop mitigation was omitted — leaving an unbounded loop in the emitted shader.
The fix removes is_immutable_before_body and rewrites </> to mirror <=/>=: accept only constant bounds, and return false when the constant sits at the limit (i32::kLowestValue/kHighestValue, u32::kLowestValue/kHighestValue) that would make the predicate always-false. It also conservatively rejects non-constant immutable bounds (lets/params), which is why the E2E tests now show added tint_loop_idx mitigations (the acknowledged false positives).
< and > branches trusted any immutable value (including limit-valued constants and lets) as proof a loop exits, unlike the already-guarded <=/>= branches; the fix restricts those branches to non-limit constants so a never-exiting loop is no longer misclassified as finite and correctly receives mitigation.Attack Path
- Author a malicious WGSL loop
An attacker-controlled WebGPU page compiles a shader containing a loop whose
break-ifuses<or>comparing the index to a limit constant (e.g.index > INT32_MAX, always false) or to alet/param bound. - Analysis misclassifies as finite
Pre-patch,
is_capable_binary_for_loop_exitreturnstruefor that comparison viais_immutable_before_body, soIsBreakIfOnIndexreports the loop finite. - Mitigation skipped
Tint omits the
tint_loop_idxbound for that loop because it believes it provably terminates. - Emit non-terminating shader The generated backend shader contains a loop that never satisfies its exit condition at runtime.
- GPU denial of service
Executing the shader runs an unbounded loop on the GPU, causing a hang/TDR; any effect beyond denial of service is
(inferred)and not shown by the diff.
Impact Assessment
(inferred).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
Audit Directions
- Asymmetric handling of comparison operatorsReview analyses that special-case
<=/>=but treat</>(or signed vs unsigned) differently; limit/edge-value handling must be symmetric across all operators. - Limit-value / overflow edge cases in range reasoningAudit any “provably finite/terminating” analysis for always-true/always-false predicates at
INT32_MIN/INT32_MAX/0/UINT32_MAX, where a comparison can never flip and defeats the exit assumption. - Trusting non-constant "immutable" boundsFlag analyses that accept
let/function-parameter values as compile-time-known bounds; an immutable binding is not a known constant and may carry a hostile runtime value, so it should force the conservative (mitigated) path.
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.