Medium watch CVSS 8.8 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in ANGLE
DescriptionImproper input validation in ANGLE
ComponentANGLE
Sub-componentMSL
Bug ClassInput Validation
Action tierwatch — ub-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.
Tracker520504922
Fix commitbc129145afe5 (angle/angle) +10/-1
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-06-05 (per vendor advisory)
Fix landed2026-07-20 (public git)
Disclosed2026-08-25
Patch gap36 days — fix public in source before the advisory

Background

MSL (Metal Shading Language) backend
ANGLE’s translator target for Apple’s Metal, which requires loops to be transformed to guarantee forward progress.
`EnsureLoopForwardProgress` transform
a pass that recognizes a canonical loop shape (variable op constant condition with a simple increment expression) and rewrites it to satisfy Metal’s forward-progress rule.
Loop variable
the induction variable extracted from the loop condition/update, which the transform assumes is a simple scalar integer amenable to its rewrite.
`Type::Scalar(BasicType::Int/Uint)` / `isScalar()`
the type predicates the fix uses to confirm the candidate loop variable is a scalar integer rather than a vector/aggregate.

Root Cause Analysis

The loop-forward-progress transform identified a “loop variable” from the loop condition and update and rewrote the loop assuming that variable was a scalar integer. Its validation only required the basic type to be an integer (IsInteger(...)) with an EvqTemporary qualifier, so a non-scalar integer such as ivec2 used as the loop variable (e.g. for (ivec2 i = ivec2(0); i != ivec2(10, 20); i++)) was accepted and fed into logic that presumes a scalar. Applying scalar-oriented rewrite logic to a vector loop variable is improper input validation: the transform manipulates a type it was never designed to handle, producing incorrect or undefined transformation of attacker-supplied shader input.

The fix adds a scalar check in both the Rust IR implementation (rejecting unless Type::Scalar(BasicType::Int)/Uint, else return None) and the C++ tree-ops implementation (!variable->getType().isScalar() added to the reject condition returning nullptr), so any non-scalar candidate is declined and the transform leaves such loops alone.

Key insight
The transform checked only that the loop variable was an integer, not that it was a scalar integer, so vector induction variables slipped into scalar-only rewrite logic; the fix additionally requires the loop variable to be a scalar integer and bails out otherwise.

Attack Path

  1. Author a vector-induction loop Write a shader whose loop uses a non-scalar integer induction variable, e.g. for (ivec2 i = ivec2(0); i != ivec2(10, 20); i++) { }, matching the recognized variable op constant shape but with a vector variable.
  2. Reach the MSL forward-progress pass Have the shader translated for the Metal backend so EnsureLoopForwardProgress runs and, pre-patch, accepts the ivec2 as the loop variable because only IsInteger/is_variable was checked.
  3. Feed a non-scalar into scalar logic The transform proceeds to apply its scalar-oriented rewrite to the vector variable, an unvalidated/unsupported input that leads to incorrect transformation or undefined behavior (inferred: specific memory-safety consequence not shown in the diff).

Impact Assessment

An attacker able to supply a shader for MSL translation (e.g., via WebGL on Apple platforms reaching the GPU process) could feed a non-scalar loop variable through the forward-progress transform, an improper-input-validation condition. The advisory classifies this as improper input validation in ANGLE; no exploited-in-the-wild status is stated. Any escalation to memory corruption or further compromise beyond the mis-transformation the patch prevents is (inferred).

Files Changed

  • src/tests/gl_tests/GLSLOutputTest.cpp
  • src/compiler/translator/ir/src/transform/msl/ensure_loop_forward_progress.rs
  • src/compiler/translator/tree_ops/msl/EnsureLoopForwardProgress.cpp

Audit Directions

  • Integer without scalar check
    Audit transforms/validators that gate on IsInteger/basic-type predicates but omit an isScalar() (or component-count) check, allowing vectors/matrices into scalar-only logic.
  • Loop/induction-variable assumptions
    Review other loop-canonicalization or unrolling passes for implicit assumptions that the induction variable and its update are scalar and single-component.
  • Parallel Rust/C++ implementations
    Where a pass exists in both the new Rust IR and the legacy C++ tree-ops (as here), check that validation is kept in sync so a bypass isn’t reachable through one path.
From bc129145afe520b62f11dae1a80d821ebfd6f273 Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <[email protected]>
Date: Thu, 16 Jul 2026 14:43:21 -0400
Subject: [PATCH] MSL: Don't take non-scalars as loop vars in loop-forward-progress

Bug: chromium:520504922
Change-Id: I0974c02eab4aacacd01013ea5bff0d7b0b6d4153
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8108624
Reviewed-by: Geoff Lang <[email protected]>
Commit-Queue: Shahbaz Youssefi <[email protected]>
---
diff --git a/src/compiler/translator/ir/src/transform/msl/ensure_loop_forward_progress.rs b/src/compiler/translator/ir/src/transform/msl/ensure_loop_forward_progress.rs
index 4bbf37c..c4eb076 100644
--- a/src/compiler/translator/ir/src/transform/msl/ensure_loop_forward_progress.rs
+++ b/src/compiler/translator/ir/src/transform/msl/ensure_loop_forward_progress.rs
@@ -91,6 +91,7 @@
 //
 // Where:
 //
+// * `variable` can only be a scalar integer
 // * `op` can only be one of > >= < <= == or !=
 // * `constant` can be a non-zero constant, or a read-only value such as shader inputs and uniforms.
 // * `expression` can only be one of:
@@ -159,6 +160,12 @@
         ir_meta.get_instruction(loop_variable_value).op
         && loop_variable.id.is_variable()
     {
+        // Loop variable must be scalar integer
+        let type_info = ir_meta.get_type(ir_meta.get_pointee_type(loop_variable.type_id));
+        if !matches!(type_info, Type::Scalar(BasicType::Int) | Type::Scalar(BasicType::Uint)) {
+            return None;
+        }
+
         loop_variable.id.get_variable()
     } else {
         return None;
diff --git a/src/compiler/translator/tree_ops/msl/EnsureLoopForwardProgress.cpp b/src/compiler/translator/tree_ops/msl/EnsureLoopForwardProgress.cpp
index 93d603f..55df115 100644
--- a/src/compiler/translator/tree_ops/msl/EnsureLoopForwardProgress.cpp
+++ b/src/compiler/translator/tree_ops/msl/EnsureLoopForwardProgress.cpp
@@ -81,7 +81,7 @@
     {
         return nullptr;
     }
-    if (!IsInteger(variable->getType().getBasicType()) ||
+    if (!IsInteger(variable->getType().getBasicType()) || !variable->getType().isScalar() ||
         variable->getType().getQualifier() != EvqTemporary)
     {
         return nullptr;
diff --git a/src/tests/gl_tests/GLSLOutputTest.cpp b/src/tests/gl_tests/GLSLOutputTest.cpp
index 3cb431a..08f1705 100644
--- a/src/tests/gl_tests/GLSLOutputTest.cpp
+++ b/src/tests/gl_tests/GLSLOutputTest.cpp
@@ -620,6 +620,8 @@
         "for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; i--, j++) { } }",
         "for (int i = 0; i < 10; f()) { }",
         "for (int i = 0; i < 10; a == 0 ? i++ : i = 0) { }",
+        "for (ivec2 i = ivec2(0); i != ivec2(10, 20); i++) { }",
+        "for (ivec2 i = ivec2(0); i != ivec2(10, 10); i += ivec2(1, 2)) { }",
     };
 
     for (const char *test : kTests)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/GLSLOutputTest.cpp b/src/tests/gl_tests/GLSLOutputTest.cpp
index 3cb431a..08f1705 100644
--- a/src/tests/gl_tests/GLSLOutputTest.cpp
+++ b/src/tests/gl_tests/GLSLOutputTest.cpp
@@ -620,6 +620,8 @@
         "for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; i--, j++) { } }",
         "for (int i = 0; i < 10; f()) { }",
         "for (int i = 0; i < 10; a == 0 ? i++ : i = 0) { }",
+        "for (ivec2 i = ivec2(0); i != ivec2(10, 20); i++) { }",
+        "for (ivec2 i = ivec2(0); i != ivec2(10, 10); i += ivec2(1, 2)) { }",
     };
 
     for (const char *test : kTests)
Loading diff…

Original Bug Report

reported by [email protected]

Potential loop forward progress bypass in ANGLE MSL translator via vector loop variables

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: In ANGLE’s MSL translator, the computeFiniteLoopVariable analysis incorrectly classifies loops with vector-typed index variables (such as uvec2 or ivec2) as finite. This occurs because the logic checks only if the variable’s basic type is integer, failing to verify that the variable is a scalar. Consequently, side-effect-free infinite loops can bypass the forward-progress injection mitigation, potentially allowing the Metal compiler optimizer to delete the loop and execute downstream code with undefined state.

Affected files:

  • third_party/angle/src/compiler/translator/tree_ops/msl/EnsureLoopForwardProgress.cpp

Estimated timestamp from git blame: 2025-05-22

Root Cause

In third_party/angle/src/compiler/translator/tree_ops/msl/EnsureLoopForwardProgress.cpp, computeFiniteLoopVariable() is responsible for identifying loops that are guaranteed to terminate (i.e. finite). If a loop is determined to be finite, the EnsureLoopForwardProgress AST pass skips injecting the ANGLE_loopForwardProgress() protection marker.

The function identifies loop variables using the following type validation:

    if (!IsInteger(variable->getType().getBasicType()) ||
        variable->getType().getQualifier() != EvqTemporary)
    {
        return nullptr;
    }

IsInteger() is defined in BaseTypes.h as:

inline bool IsInteger(TBasicType type)
{
    return type == EbtInt || type == EbtUInt;
}

This check only evaluates the variable’s basic element type (e.g., EbtUInt or EbtInt), but fails to verify if the variable is a scalar. As a result, vector loop variables such as uvec2 or ivec2 are incorrectly accepted as finite.

Impact and Potential Exploitation Scenario

By bypassing this check, an attacker could write a WebGL2 shader with a side-effect-free infinite loop on a vector index that is classified as finite. The compiler then skips inserting the forward-progress marker. When compiled by Apple’s Metal compiler (which assumes side-effect-free loops always terminate under standard C++ forward-progress guarantees), the optimizer may delete the loop entirely. This potentially results in the execution of downstream code with undefined or poisoned GPU state.

Scenario 1: Unsigned Wrap-Around Bypass

A WebGL2 shader could contain:

for (uvec2 i = uvec2(0u); i != C; i++) {}

If C is set to uvec2(3u, 7u), the vector index i increments as (0,0), (1,1), (2,2), etc., and never matches (3,7) even with unsigned integer wrap-around. The loop is infinite but is incorrectly classified as finite.

Scenario 2: Component-wise Multi-dimensional Stride Bypass

In EnsureLoopForwardProgress.cpp:123, binary update expressions are evaluated:

        const TConstantUnion *value = binExpr->getRight()->getConstantValue();
        ...
        switch (value->getType())
        {
            case EbtInt:
                if (value->getIConst() == -1 || value->getIConst() == 1)

For a loop like for(ivec2 i=ivec2(0); i!=ivec2(10,10); i+=ivec2(1,-1)), getConstantValue() returns a pointer to the element array. The code checks value->getIConst(), which only reads the first element (1), completely ignoring the second component (-1). This loop is also incorrectly classified as finite.

Suggested Potential Steps to Reproduce

(Please note these are suggested/potential steps; our tooling agent does not yet have the ability to execute code and verify runtime behavior.)

  1. On macOS Chrome (which uses the default ANGLE Metal backend), initialize a WebGL2 context.
  2. Compile and link a fragment shader containing a loop with a vector-typed loop variable:
    #version 300 es
    precision highp int; precision highp float;
    uniform uvec2 C; out vec4 c;
    void main(){ for(uvec2 i=uvec2(0u); i!=C; i++){} c=vec4(1.0); }
    
  3. Set the uniform C to uvec2(3u, 7u) and execute a draw call.
  4. Verify that ANGLE’s EnsureLoopForwardProgress pass skips the forward-progress marker insertion.

Suggested Fix

Reject any non-scalar variables in computeFiniteLoopVariable by enforcing that the variable must be a scalar:

    if (!IsInteger(variable->getType().getBasicType()) ||
        !variable->getType().isScalar() ||
        variable->getType().getQualifier() != EvqTemporary)
    {
        return nullptr;
    }

Evaluated with Chrome root at commit: e9507a33bb4148ee071aaaf8a7e9ad68770359bf


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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