Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Tint
DescriptionInappropriate implementation in Tint
ComponentTint
Bug ClassLogic Error
Tracker502206907
Fix commit4bb845fa4082 (dawn) +1195/-699
CISA KEVNot listed
CreditedGoogle
Disclosed2026-04-28

Changed Functions

FunctionChangeNotes
if
src/tint/lang/core/ir/analysis/loop_analysis.cc
modified
while
test/tint/bug/tint/1121.wgsl.expected.dxc.hlsl
modified

Files Changed

  • src/tint/lang/core/ir/analysis/loop_analysis.cc
  • src/tint/lang/core/ir/analysis/loop_analysis_test.cc
  • test/tint/bug/tint/1121.wgsl.expected.dxc.hlsl
From 4bb845fa408205b58a060f2b474cecfdb3d53fe0 Mon Sep 17 00:00:00 2001
From: James Price <jrprice@google.com>
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 <dsinclair@chromium.org>
Commit-Queue: James Price <jrprice@google.com>
---

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 @@
             }
Loading diff…

Regression Test / PoC

shipped with the fix
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),
Loading diff…

Original Bug Report

reported by vm...@google.com

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:

  1. Missing Degenerate-Bound Checks for < and > (lines 243-247): The is_capable_binary_for_loop_exit function accepts kLessThan and kGreaterThan operators 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 < 0u is always false for a u32 index).

  2. Uncorrelated Exit Branch Polarity (line 313): The logic at line 313 considers a loop finite if either the True or False branch of an if instruction leads to an ExitLoop: return is_simple_loop_exit(i->True()) || is_simple_loop_exit(i->False()); This does not account for the condition’s polarity. Thus, an if (idx < 0u) { break; } statement is treated as a valid termination even though the break (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:

  1. 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;
    
  2. Robustness Transform: Tint’s Robustness transform executes and replaces my_array[my_idx] with clamped indexing: my_array[min(my_idx, array_size - 1)].
  3. PreventInfiniteLoops Bypass: LoopAnalysis misclassifies the inner loop as finite. Consequently, the PreventInfiniteLoops transform is bypassed, and no iteration cap is injected into the loop.
  4. 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.
  5. 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 check min(my_idx, array_size - 1), it determines the min operation is redundant and optimizes it away to just my_idx.
  6. OOB Execution: The compiler removes the unreachable infinite loop. At runtime, if the attacker provides a my_idx greater than or equal to array_size, execution falls through to the array access. Because the min clamping 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.

View on issue tracker