High watch CVSS 8.1 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Tint
DescriptionInappropriate implementation in Tint
ComponentTint
Sub-componentTint
Bug ClassLogic Error
Action tierwatch — dos-class only — no demonstrated corruption path
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker502206907
Fix commit4bb845fa4082 (dawn) +1195/-699
CWECWE-119 (Buffer bounds error)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-04-13 (per vendor advisory)
Fix landed2026-04-15 (public git)
Disclosed2026-04-28
Patch gap13 days — fix public in source before the advisory

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-if that compares the loop index against a bound.
`is_capable_binary_for_loop_exit`
the predicate that returns true when a comparison binary (<, >, <=, >=) between the index and a bound is a valid finiteness witness; true means “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).

Key insight
The core mistake was that the < 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

  1. Author a malicious WGSL loop An attacker-controlled WebGPU page compiles a shader containing a loop whose break-if uses < or > comparing the index to a limit constant (e.g. index > INT32_MAX, always false) or to a let/param bound.
  2. Analysis misclassifies as finite Pre-patch, is_capable_binary_for_loop_exit returns true for that comparison via is_immutable_before_body, so IsBreakIfOnIndex reports the loop finite.
  3. Mitigation skipped Tint omits the tint_loop_idx bound for that loop because it believes it provably terminates.
  4. Emit non-terminating shader The generated backend shader contains a loop that never satisfies its exit condition at runtime.
  5. 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

A web page using WebGPU can craft a WGSL shader whose loop analysis is fooled into skipping infinite-loop mitigation, resulting in an un-terminating GPU loop and a denial of service (device hang / timeout) affecting the GPU process and potentially the system’s graphics stack. The advisory classifies this as high severity “inappropriate implementation” and does not report in-the-wild exploitation. The precondition is WebGPU access; no memory-corruption or escape primitive is demonstrated by the patch, so any such escalation would be (inferred).

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

Audit Directions

  • Asymmetric handling of comparison operators
    Review 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 reasoning
    Audit 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" bounds
    Flag 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.
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 @@
             }
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 [email protected]

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