CVE-2026-9913
Overview
Files Changed
src/compiler/translator/tree_ops/msl/EnsureLoopForwardProgress.cppsrc/tests/gl_tests/GLSLOutputTest.cpp
Patch
From 64b421a9a1ef9a726e9a8bcca11c2b17c4bbfb29 Mon Sep 17 00:00:00 2001 From: Shahbaz Youssefi <[email protected]> Date: Thu, 07 May 2026 10:56:33 -0400 Subject: [PATCH] MSL: Inspect loop init/cond/expr in loop-forward-progress The loop variable in an enclosing loop might be modified inside the init, condition and expression of the for loop too, not just the body. Bug: chromium:500046096 Change-Id: I880be859bb57122a0549f60401763cc8122a7aff Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7828698 Reviewed-by: Geoff Lang <[email protected]> Commit-Queue: Shahbaz Youssefi <[email protected]> --- diff --git a/src/compiler/translator/tree_ops/msl/EnsureLoopForwardProgress.cpp b/src/compiler/translator/tree_ops/msl/EnsureLoopForwardProgress.cpp index 98a4c8c..3d81eed 100644 --- a/src/compiler/translator/tree_ops/msl/EnsureLoopForwardProgress.cpp +++ b/src/compiler/translator/tree_ops/msl/EnsureLoopForwardProgress.cpp @@ -209,10 +209,23 @@ void EnsureLoopForwardProgressTraverser::traverseLoop(TIntermLoop *node) { + ScopedNodeInTraversalPath addToPath(this, node); + if (node->getInit()) + { + node->getInit()->traverse(this); + } + if (node->getCondition()) + { + node->getCondition()->traverse(this); + } + if (node->getExpression()) + { + node->getExpression()->traverse(this); + } + LoopInfoStack loopInfo{node, mLoopInfoStack}; mLoopInfoStack = &loopInfo; - ScopedNodeInTraversalPath addToPath(this, node); node->getBody()->traverse(this); if (!loopInfo.isFinite()) diff --git a/src/tests/gl_tests/GLSLOutputTest.cpp b/src/tests/gl_tests/GLSLOutputTest.cpp index 0539f10..4df385b 100644 --- a/src/tests/gl_tests/GLSLOutputTest.cpp +++ b/src/tests/gl_tests/GLSLOutputTest.cpp @@ -580,18 +580,24 @@ )"; const char kShaderSuffix[] = "}\n"; - const char *kTests[]{"for (;;) { }", - "for (bool b = true; b; b = false) { }", - "for (int i = 0; i < 10;) { }", - "int i = 101; for (; i < 10; i+=2) { }", - "int i = 101; for (; i < 10; i-=2) { }", - "int z = 7; for (int i = 0; i < z; i++) { }", - "for (int i = 0; i < 10; i++) { i++; }", - "for (int i = 0; i < 10;) { i++; }", - "for (int i = 0; i < a/2; i++) { }", - "for (int i = 0; float(i) < 10e10; ++i) { }", - "for (int i = 0; i < 10; i++) { for (int j = 0; j < 1000; ++i) { }}", - "for (int i = 0; i != 1; i+=2) { }"}; + const char *kTests[]{ + "for (;;) { }", + "for (bool b = true; b; b = false) { }", + "for (int i = 0; i < 10;) { }", + "int i = 101; for (; i < 10; i+=2) { }", + "int i = 101; for (; i < 10; i-=2) { }", + "int z = 7; for (int i = 0; i < z; i++) { }", + "for (int i = 0; i < 10; i++) { i++; }", + "for (int i = 0; i < 10;) { i++; }", + "for (int i = 0; i < a/2; i++) { }", + "for (int i = 0; float(i) < 10e10; ++i) { }", + "for (int i = 0; i < 10; i++) { for (int j = 0; j < 1000; ++i) { }}", + "for (int i = 0; i != 1; i+=2) { }", + "for (int i = 0; i < 10; i++) { int j; for (j = 0, i = 0; j < 10; j++) { } }", + "for (int i = 0; i < 10; i++) { for (int j = 0; i = 0, j < 10; j++) { } }", + "for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; i = 0, j++) { } }", + "for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; i--, j++) { } }", + }; for (const char *test : kTests) {
Regression Test / PoC
diff --git a/src/tests/gl_tests/GLSLOutputTest.cpp b/src/tests/gl_tests/GLSLOutputTest.cpp
index 0539f10..4df385b 100644
--- a/src/tests/gl_tests/GLSLOutputTest.cpp
+++ b/src/tests/gl_tests/GLSLOutputTest.cpp
@@ -580,18 +580,24 @@
)";
const char kShaderSuffix[] = "}\n";
- const char *kTests[]{"for (;;) { }",
- "for (bool b = true; b; b = false) { }",
- "for (int i = 0; i < 10;) { }",
- "int i = 101; for (; i < 10; i+=2) { }",
- "int i = 101; for (; i < 10; i-=2) { }",
- "int z = 7; for (int i = 0; i < z; i++) { }",
- "for (int i = 0; i < 10; i++) { i++; }",
- "for (int i = 0; i < 10;) { i++; }",
- "for (int i = 0; i < a/2; i++) { }",
- "for (int i = 0; float(i) < 10e10; ++i) { }",
- "for (int i = 0; i < 10; i++) { for (int j = 0; j < 1000; ++i) { }}",
- "for (int i = 0; i != 1; i+=2) { }"};
+ const char *kTests[]{
+ "for (;;) { }",
+ "for (bool b = true; b; b = false) { }",
+ "for (int i = 0; i < 10;) { }",
+ "int i = 101; for (; i < 10; i+=2) { }",
+ "int i = 101; for (; i < 10; i-=2) { }",
+ "int z = 7; for (int i = 0; i < z; i++) { }",
+ "for (int i = 0; i < 10; i++) { i++; }",
+ "for (int i = 0; i < 10;) { i++; }",
+ "for (int i = 0; i < a/2; i++) { }",
+ "for (int i = 0; float(i) < 10e10; ++i) { }",
+ "for (int i = 0; i < 10; i++) { for (int j = 0; j < 1000; ++i) { }}",
+ "for (int i = 0; i != 1; i+=2) { }",
+ "for (int i = 0; i < 10; i++) { int j; for (j = 0, i = 0; j < 10; j++) { } }",
+ "for (int i = 0; i < 10; i++) { for (int j = 0; i = 0, j < 10; j++) { } }",
+ "for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; i = 0, j++) { } }",
+ "for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; i--, j++) { } }",
+ };
for (const char *test : kTests)
{
Original Bug Report
Potential bypass of ANGLE Metal forward-progress mitigation via incomplete AST traversal
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 security team.
Overview: The EnsureLoopForwardProgress pass in ANGLE fails to traverse loop initialization, condition, and expression subtrees. This allows WebGL shaders to hide induction variable writes inside nested loops, bypassing infinite loop detection and potentially leading to out-of-bounds memory access in the GPU process.
Affected files:
third_party/angle/src/compiler/translator/tree_ops/msl/EnsureLoopForwardProgress.cpp
Estimated timestamp from git blame: 2025-06-16
Background
ANGLE includes a security mitigation (EnsureLoopForwardProgress) for the macOS/Metal backend to prevent the Apple Metal compiler (which is LLVM-based) from aggressively optimizing away side-effect-free infinite loops. Under LLVM semantics, an infinite loop without side effects is undefined behavior. If a shader contains such a loop, the compiler may assume the code following the loop is unreachable or elide bounds checks that ANGLE inserts for safety. This optimization bug has historically led to out-of-bounds (OOB) memory access and crashes in the GPU process (e.g., crbug.com/41486305). To prevent this, ANGLE injects a volatile variable access (ANGLE_loopForwardProgress()) into loops it cannot prove are finite.
Description of the Vulnerability
The vulnerability lies in how EnsureLoopForwardProgressTraverser::traverseLoop traverses the Abstract Syntax Tree (AST). The pass is designed to monitor loop induction variables to ensure they are not modified outside of the expected increment/decrement step. If a modification is detected via visitSymbol, the loop is marked as non-finite.
However, the custom traverseLoop implementation in third_party/angle/src/compiler/translator/tree_ops/msl/EnsureLoopForwardProgress.cpp is flawed:
void EnsureLoopForwardProgressTraverser::traverseLoop(TIntermLoop *node)
{
// ... setup ...
node->getBody()->traverse(this); // <-- ONLY traverses the body
// ... mitigation injection ...
}
Unlike the default TIntermTraverser::traverseLoop, which traverses all children of a loop (init, cond, expr, and body), this custom implementation explicitly skips traversing the initialization (init), condition (cond), and expression (expr) subtrees.
Additionally, the helper function computeFiniteLoopVariable (used to initially identify simple finite loops) only inspects the cond and expr nodes, completely ignoring the init node.
Potential Attack Steps
An attacker can construct a WebGL shader to hide a write to an outer loop’s induction variable inside the initialization of a nested inner loop:
for (int i = 0; i != 10; i++) { // Outer loop
for (int j = (i = 0); j != 5; j++) { // Inner loop resets 'i'
// Inner loop body
}
}
- The traverser evaluates the outer loop and marks it as finite based on
i != 10andi++. - The traverser traverses the outer loop’s body and encounters the inner loop.
- The traverser evaluates the inner loop and marks it as finite based on
j != 5andj++. It ignores thej = (i = 0)initialization. - The traverser traverses the inner loop’s body. Crucially, it skips traversing the inner loop’s initialization.
- Because the
initblock is never traversed, theTLValueTrackingTraversernever sees thei = 0assignment.visitSymbolis bypassed for this modification. - Both loops are incorrectly finalized as finite, and the
ANGLE_loopForwardProgress()mitigation is not injected into either loop. - The resulting Metal Shading Language (MSL) code contains a side-effect-free infinite loop. The Apple Metal compiler optimizes this loop, potentially eliding subsequent bounds checks and leading to out-of-bounds memory access in the GPU process.
Note: These are suggested steps based on static analysis. Our tooling agent does not currently have the ability to run code to produce a working proof of concept.
Proposed Fix
Modify EnsureLoopForwardProgressTraverser::traverseLoop to ensure that all subtrees of the loop are traversed, not just the body. It should call traverse(node) (which relies on the base class’s generic traversal logic that visits all children) instead of node->getBody()->traverse(this).
Care must be taken to ensure that the traversal of the loop’s own expr block (e.g., i++) does not incorrectly flag the loop as non-finite. The traverser might need to temporarily suppress setNotFinite for the current loop’s expected induction variable when traversing its own expr node.
Evaluated with Chrome root at commit: f200f57a19490707ff8bc7aa5de3cbc443a3afad
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.