CVE-2026-9999
Overview
Files Changed
src/compiler/translator/tree_ops/msl/EnsureLoopForwardProgress.cppsrc/tests/gl_tests/GLSLOutputTest.cpp
Patch
From 5d101383183884b5e98107c0fcd7c004ae2b66a4 Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Fri, 15 May 2026 09:15:30 -0400
Subject: [PATCH] MSL: Don't take globals as loop vars in loop-forward-progress
This was already correct in the IR transformation.
Bug: chromium:513364480
Change-Id: I6721ba24825628abf29c515d89307dd5c0a93e37
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7852695
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
---
diff --git a/src/compiler/translator/tree_ops/msl/EnsureLoopForwardProgress.cpp b/src/compiler/translator/tree_ops/msl/EnsureLoopForwardProgress.cpp
index 3d81eed..7997879 100644
--- a/src/compiler/translator/tree_ops/msl/EnsureLoopForwardProgress.cpp
+++ b/src/compiler/translator/tree_ops/msl/EnsureLoopForwardProgress.cpp
@@ -81,7 +81,8 @@
{
return nullptr;
}
- if (!IsInteger(variable->getType().getBasicType()))
+ if (!IsInteger(variable->getType().getBasicType()) ||
+ variable->getType().getQualifier() != EvqTemporary)
{
return nullptr;
}
diff --git a/src/tests/gl_tests/GLSLOutputTest.cpp b/src/tests/gl_tests/GLSLOutputTest.cpp
index 4df385b..c961069 100644
--- a/src/tests/gl_tests/GLSLOutputTest.cpp
+++ b/src/tests/gl_tests/GLSLOutputTest.cpp
@@ -534,6 +534,30 @@
verifyCountInTranslation(GL_FRAGMENT_SHADER, "loopForwardProgress", 2 + 1);
}
+// Test that loopForwardProgress() is inserted when the loop variable is global.
+TEST_P(GLSLOutputMSLTest_EnsureLoopForwardProgress, InfiniteForWithGlobal)
+{
+ constexpr char kFS[] = R"(#version 300 es
+highp int i;
+void f()
+{
+ i = 0;
+}
+void main() {
+ for (i = 0; i < 100; i++)
+ {
+ i = 0;
+ }
+ for (i = 0; i < 100; i++)
+ {
+ f();
+ }
+})";
+ compileShader(GL_FRAGMENT_SHADER, kFS);
+ // One occurrence for defining |loopForwardProgress()|, and one call in each loop.
+ verifyCountInTranslation(GL_FRAGMENT_SHADER, "loopForwardProgress", 2 + 1);
+}
+
// Test that loopForwardProgress() is not inserted when the for loop is not an infinite loop,
// testing various tricky loops.
TEST_P(GLSLOutputMSLTest_EnsureLoopForwardProgress, FiniteFors)
Regression Test / PoC
diff --git a/src/tests/gl_tests/GLSLOutputTest.cpp b/src/tests/gl_tests/GLSLOutputTest.cpp
index 4df385b..c961069 100644
--- a/src/tests/gl_tests/GLSLOutputTest.cpp
+++ b/src/tests/gl_tests/GLSLOutputTest.cpp
@@ -534,6 +534,30 @@
verifyCountInTranslation(GL_FRAGMENT_SHADER, "loopForwardProgress", 2 + 1);
}
+// Test that loopForwardProgress() is inserted when the loop variable is global.
+TEST_P(GLSLOutputMSLTest_EnsureLoopForwardProgress, InfiniteForWithGlobal)
+{
+ constexpr char kFS[] = R"(#version 300 es
+highp int i;
+void f()
+{
+ i = 0;
+}
+void main() {
+ for (i = 0; i < 100; i++)
+ {
+ i = 0;
+ }
+ for (i = 0; i < 100; i++)
+ {
+ f();
+ }
+})";
+ compileShader(GL_FRAGMENT_SHADER, kFS);
+ // One occurrence for defining |loopForwardProgress()|, and one call in each loop.
+ verifyCountInTranslation(GL_FRAGMENT_SHADER, "loopForwardProgress", 2 + 1);
+}
+
// Test that loopForwardProgress() is not inserted when the for loop is not an infinite loop,
// testing various tricky loops.
TEST_P(GLSLOutputMSLTest_EnsureLoopForwardProgress, FiniteFors)
Original Bug Report
Potential interprocedural bypass in ANGLE Metal EnsureLoopForwardProgress pass
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: The EnsureLoopForwardProgress AST pass in ANGLE’s Metal backend fails to detect mutations of global loop variables when they occur inside called functions. This allows crafted shaders to bypass infinite-loop mitigations, potentially leading to GPU process crashes or undefined behavior due to downstream compiler optimizations.
Affected files:
third_party/angle/src/compiler/translator/tree_ops/msl/EnsureLoopForwardProgress.cppthird_party/angle/src/compiler/translator/tree_util/IntermTraverse.cpp
Estimated timestamp from git blame: 2025-05-22
Background
ANGLE’s Metal backend implements the EnsureLoopForwardProgress AST pass to mitigate issues where the Metal compiler (based on LLVM) optimizes away loops it perceives as infinite and side-effect-free. This optimization can lead to GPU process instability, hangs, or execution with undefined values (e.g., crbug.com/41486305). The pass inserts a volatile-write marker (ANGLE_loopForwardProgress()) into loops it cannot prove are finite.
Root Cause
The vulnerability is a potential interprocedural blind spot in the analysis performed within third_party/angle/src/compiler/translator/tree_ops/msl/EnsureLoopForwardProgress.cpp. The analysis incorrectly assumes a loop is finite if its counter variable is only modified by the loop’s own increment expression. This logic fails due to two factors:
- Global Variable Acceptance: The
computeFiniteLoopVariable()function identifies loop variables without verifying their storage qualifier. Consequently, file-scope global variables (EvqGlobal) are treated as if they were loop-local variables. - Intraprocedural Analysis Limitations: The
EnsureLoopForwardProgressTraverser(inheriting fromTLValueTrackingTraverser) only inspects the loop body intraprocedurally. When it encounters a function call (TIntermAggregate), it iterates over the arguments but does not inspect the callee’s definition for potential mutations of global variables.
Because the analysis does not account for global variables mutated inside called functions, it may incorrectly identify an infinite loop as finite and skip the insertion of the necessary ANGLE_loopForwardProgress() mitigation.
Potential Impact
An attacker could potentially craft a WebGL2 shader that implements an infinite loop using a global counter variable reset within a function call. If the EnsureLoopForwardProgress pass is bypassed, the resulting Metal Shading Language (MSL) code will lack the mitigation. The Metal compiler may then apply forward-progress assumptions, leading to undefined behavior or GPU process instability on macOS where ANGLE Metal is the default backend.
Suggested Proof of Concept
The following GLSL ES 3.00 shader illustrates the potential bypass. The analysis sees g++, but fails to see the mutation of g inside clobber():
#version 300 es
precision highp int;
int g;
void clobber() { g = 0; }
void main() {
// The analysis may incorrectly conclude this loop is finite.
for (g = 0; g != 10; g++) {
clobber();
}
}
Suggested Fix
Consider the following mitigations:
- Modify
computeFiniteLoopVariable()to only accept variables with theEvqTemporaryqualifier, ensuring the loop variable is strictly local to the function. - Alternatively, update the traverser to conservatively mark the current loop as non-finite whenever a user-defined function call is encountered within the loop body.
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
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.