Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in ANGLE
DescriptionOut of bounds write in ANGLE
ComponentANGLE
Bug ClassOOB
Tracker536532605
Fix commitd3a7ea3c7fe2 (angle/angle) +44/-13
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
GLSLTestPassthrough
src/tests/gl_tests/GLSLTest.cpp
modified

Files Changed

  • src/compiler/translator/hlsl/OutputHLSL.cpp
  • src/compiler/translator/hlsl/OutputHLSL.h
  • src/compiler/translator/hlsl/UtilsHLSL.cpp
  • src/tests/gl_tests/GLSLTest.cpp
From d3a7ea3c7fe2f189ed2c6dab1302dc1c24fa2a16 Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <[email protected]>
Date: Mon, 20 Jul 2026 16:10:49 -0400
Subject: [PATCH] HLSL: Fix name collision between parameters and locals

Bug: chromium:536532605
Change-Id: I8975daed303ab62745f7bfaf3da7667a87211749
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8127455
Reviewed-by: Geoff Lang <[email protected]>
Commit-Queue: Shahbaz Youssefi <[email protected]>
---

diff --git a/src/compiler/translator/hlsl/OutputHLSL.cpp b/src/compiler/translator/hlsl/OutputHLSL.cpp
index 0021413..272f342 100644
--- a/src/compiler/translator/hlsl/OutputHLSL.cpp
+++ b/src/compiler/translator/hlsl/OutputHLSL.cpp
@@ -1929,12 +1929,12 @@
     return true;
 }
 
-ImmutableString OutputHLSL::samplerNamePrefixFromStruct(TIntermTyped *node)
+TString OutputHLSL::samplerNamePrefixFromStruct(TIntermTyped *node)
 {
     if (node->getAsSymbolNode())
     {
         ASSERT(node->getAsSymbolNode()->variable().symbolType() != SymbolType::Empty);
-        return node->getAsSymbolNode()->getName();
+        return DecorateVariableIfNeeded(node->getAsSymbolNode()->variable());
     }
     TIntermBinary *nodeBinary = node->getAsBinaryNode();
     switch (nodeBinary->getOp())
@@ -1943,9 +1943,9 @@
         {
             int index = nodeBinary->getRight()->getAsConstantUnion()->getIConst(0);
 
-            std::stringstream prefixSink = sh::InitializeStream<std::stringstream>();
+            TStringStream prefixSink = sh::InitializeStream<TStringStream>();
             prefixSink << samplerNamePrefixFromStruct(nodeBinary->getLeft()) << "_" << index;
-            return ImmutableString(prefixSink.str());
+            return prefixSink.str();
         }
         case EOpIndexDirectStruct:
         {
@@ -1953,14 +1953,14 @@
             int index           = nodeBinary->getRight()->getAsConstantUnion()->getIConst(0);
             const TField *field = s->fields()[index];
 
-            std::stringstream prefixSink = sh::InitializeStream<std::stringstream>();
+            TStringStream prefixSink = sh::InitializeStream<TStringStream>();
             prefixSink << samplerNamePrefixFromStruct(nodeBinary->getLeft()) << "_"
                        << field->name();
-            return ImmutableString(prefixSink.str());
+            return prefixSink.str();
         }
         default:
             UNREACHABLE();
-            return kEmptyImmutableString;
+            return "";
     }
 }
 
@@ -2283,8 +2283,8 @@
                 {
                     const TType &argType = typedArg->getType();
                     TVector<const TVariable *> samplerSymbols;
-                    ImmutableString structName = samplerNamePrefixFromStruct(typedArg);
-                    std::string namePrefix     = "angle_";
+                    TString structName     = samplerNamePrefixFromStruct(typedArg);
+                    std::string namePrefix = "angle";
                     namePrefix += structName.data();
                     argType.createSamplerSymbols(ImmutableString(namePrefix), "", &samplerSymbols,
                                                  nullptr, mSymbolTable);
diff --git a/src/compiler/translator/hlsl/OutputHLSL.h b/src/compiler/translator/hlsl/OutputHLSL.h
index cdf3c0d..dd592e8 100644
--- a/src/compiler/translator/hlsl/OutputHLSL.h
+++ b/src/compiler/translator/hlsl/OutputHLSL.h
@@ -280,7 +280,7 @@
 
   private:
     TString generateStructMapping(const std::vector<MappedStruct> &std140Structs) const;
-    ImmutableString samplerNamePrefixFromStruct(TIntermTyped *node);
+    TString samplerNamePrefixFromStruct(TIntermTyped *node);
     bool ancestorEvaluatesToSamplerInStruct();
     // We need to do struct mapping when pass the struct to a function or copy the struct via
     // assignment.
diff --git a/src/compiler/translator/hlsl/UtilsHLSL.cpp b/src/compiler/translator/hlsl/UtilsHLSL.cpp
index 6ed1eba..ae8eae4 100644
--- a/src/compiler/translator/hlsl/UtilsHLSL.cpp
+++ b/src/compiler/translator/hlsl/UtilsHLSL.cpp
@@ -854,7 +854,9 @@
     // For user defined variables, combine variable name with unique id
     // so variables of the same name in different scopes do not get overwritten.
     else if (variable.symbolType() == SymbolType::UserDefined &&
-             (qualifier == EvqTemporary || qualifier == EvqGlobal || qualifier == EvqConst))
+             (qualifier == EvqTemporary || qualifier == EvqGlobal || qualifier == EvqConst ||
+              qualifier == EvqParamIn || qualifier == EvqParamOut || qualifier == EvqParamInOut ||
+              qualifier == EvqParamConst))
     {
         return Decorate(variable.name()) + str(variable.uniqueId().get());
     }
diff --git a/src/tests/gl_tests/GLSLTest.cpp b/src/tests/gl_tests/GLSLTest.cpp
index 77f340f..6cad6e2 100644
--- a/src/tests/gl_tests/GLSLTest.cpp
+++ b/src/tests/gl_tests/GLSLTest.cpp
@@ -24827,8 +24827,6 @@
            << R"(;
   gl_FragColor = vec4(a);
 })";
-        std::cout << fs.str() << "\n";
-
         ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), fs.str().c_str());
         drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f);
         EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(127, 127, 127, 127), 1);
@@ -24836,6 +24834,37 @@
     }
 }
 
+// Regression test for a bug in the HLSL generator where the function parameter names could collide
+// with local variable names.  In particular, the local variables were suffixed with the symbol id,
+// starting from 3000 (kFirstUserDefinedSymbolId) but the function parameters weren't.
+TEST_P(GLSLTest_ES3, HLSLParameterNameCollisionWithLocalVar)
+{
+    // At the time this regression test was written, the ID of the local variable was 3003.  Try a
+    // few IDs starting at kFirstUserDefinedSymbolId so the test is not sensitive to small
+    // variations in the ID.
+    for (uint32_t id = 3000; id < 3010; ++id)
+    {
+        std::ostringstream fs;
+        fs << R"(precision highp float;
+float f(float _a)"
+           << id << R"()
+{
+    float a;
+    a = _a)"
+           << id << R"( + 0.5;
+    return a;
+}
+void main()
+{
+  gl_FragColor = vec4(f(0.2));
+})";
+        ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), fs.str().c_str());
+        drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f);
+        EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(178, 178, 178, 178), 1);
+        ASSERT_GL_NO_ERROR();
+    }
+}
+
 class GLSLTestPassthrough : public GLSLTest
 {};
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/GLSLTest.cpp b/src/tests/gl_tests/GLSLTest.cpp
index 77f340f..6cad6e2 100644
--- a/src/tests/gl_tests/GLSLTest.cpp
+++ b/src/tests/gl_tests/GLSLTest.cpp
@@ -24827,8 +24827,6 @@
            << R"(;
   gl_FragColor = vec4(a);
 })";
-        std::cout << fs.str() << "\n";
-
         ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), fs.str().c_str());
         drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f);
         EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(127, 127, 127, 127), 1);
@@ -24836,6 +24834,37 @@
     }
 }
 
+// Regression test for a bug in the HLSL generator where the function parameter names could collide
+// with local variable names.  In particular, the local variables were suffixed with the symbol id,
+// starting from 3000 (kFirstUserDefinedSymbolId) but the function parameters weren't.
+TEST_P(GLSLTest_ES3, HLSLParameterNameCollisionWithLocalVar)
+{
+    // At the time this regression test was written, the ID of the local variable was 3003.  Try a
+    // few IDs starting at kFirstUserDefinedSymbolId so the test is not sensitive to small
+    // variations in the ID.
+    for (uint32_t id = 3000; id < 3010; ++id)
+    {
+        std::ostringstream fs;
+        fs << R"(precision highp float;
+float f(float _a)"
+           << id << R"()
+{
+    float a;
+    a = _a)"
+           << id << R"( + 0.5;
+    return a;
+}
+void main()
+{
+  gl_FragColor = vec4(f(0.2));
+})";
+        ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), fs.str().c_str());
+        drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f);
+        EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(178, 178, 178, 178), 1);
+        ASSERT_GL_NO_ERROR();
+    }
+}
+
 class GLSLTestPassthrough : public GLSLTest
 {};
Loading diff…

Original Bug Report

reported by [email protected]

Potential ANGLE HLSL Identifier Collision via Missing Delimiter Causes GPU OOB Write

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: A missing delimiter in ANGLE’s HLSL translator when decorating local variables allows an attacker to cause an identifier collision between a local variable and a function parameter. This collision exploits HLSL scoping rules to bypass the ClampIndirectIndices mitigation, leading to an out-of-bounds write on a 1-element local array. The vulnerability can be reached via WebGL from untrusted web content, resulting in potential memory corruption in the sandboxed GPU process on Windows.

Affected files:

  • third_party/angle/src/compiler/translator/hlsl/UtilsHLSL.cpp
  • third_party/angle/src/compiler/translator/hlsl/OutputHLSL.cpp
  • third_party/angle/src/compiler/translator/tree_ops/ClampIndirectIndices.cpp

Estimated timestamp from git blame: 2019-02-09

1. Summary of the Issue (Meant for Human Triage)

A potential high-severity vulnerability exists in ANGLE’s D3D11/HLSL translator where user-defined local variables can lexically shadow parameters of different scopes due to an identifier collision. In third_party/angle/src/compiler/translator/hlsl/UtilsHLSL.cpp, local variables (EvqTemporary) are decorated with their unique ID to prevent collisions, but no delimiter is used between the decorated name and the ID string. In contrast, function parameters (EvqParamIn/Out) omit the unique ID entirely.

An attacker providing a malicious GLSL shader via WebGL can craft a parameter name (e.g., p3007) that perfectly collides with the emitted HLSL name of an inner local variable (e.g., p with unique ID 3007). By generating a dynamic array access, ANGLE’s ClampIndirectIndices pass applies bounds-checking based on the large parameter array size. However, standard C++/HLSL scoping causes the Microsoft HLSL compiler (fxc or D3DCompile) to resolve the identifier to the smaller inner local array. This bypasses ANGLE’s out-of-bounds mitigations, potentially resulting in a GPU-SHADER out-of-bounds write into thread-private registers or scratch memory on D3D11 backends.

Because the D3D11 backend is Windows-only (compile-time excluded on Android) and the GPU process is sandboxed on Windows, the issue is capped at High severity (S1).

(Note: These are suggested steps based on source-code analysis; tooling does not yet have the capability to execute a live Proof-of-Concept.)


2. Proof-of-Concept & Detailed Execution Flow

Vulnerability Mechanics & Step-by-Step Execution

  1. Unique ID Determinism: ANGLE assigns deterministic unique IDs to user-defined symbols. Per third_party/angle/src/compiler/translator/SymbolTable.cpp:421, mUniqueIdCounter resets to kFirstUserDefinedSymbolId (3000) per compilation. An attacker can reliably predict that a specific local variable will receive a target ID (e.g., 3007) by padding declarations.
  2. Shader Construction & Parser Phase: The attacker supplies a GLSL payload via gl.shaderSource. They declare a function taking an array parameter sized at 256: out float p3007[256]. Inside this function, they declare a nested local array: float p[1];, positioned so it receives unique ID 3007.
  3. Bypassing Optimization: To ensure the local variable p[1] survives the RemoveUnreferencedVariables pass (tree_ops/RemoveUnreferencedVariables.cpp:252), the attacker includes a dummy reference like p[0] = 0.0;.
  4. Target Assignment: The attacker adds a dynamic assignment targeting the parameter: p3007[idx] = 1.0;, where idx is an attacker-controlled uniform.
  5. Clamping Pass Bound Logic: On D3D WebGL contexts, compileOptions.clampIndirectArrayBounds triggers ClampIndirectIndices (Compiler.cpp:1039). In tree_ops/ClampIndirectIndices.cpp:72-82, the pass reads the AST type of p3007 (the 256-element array) and rewrites the index expression to enforce a maximum bound of 255: clamp(idx, 0, 255).
  6. Parameter Emission without ID: During HLSL emission, OutputHLSL::writeParameter evaluates the parameter (qualifier EvqParamOut). In UtilsHLSL.cpp:841 (DecorateVariableIfNeeded), this qualifier falls to the else branch (line 861), outputting Decorate(variable.name()) without a unique ID suffix. Decorate prepends _, yielding _p3007.
  7. Local Emission with ID (The Flaw): OutputHLSL visits the nested block and emits the local variable p (qualifier EvqTemporary). DecorateVariableIfNeeded enters the else if branch (line 856):
    return Decorate(variable.name()) + str(variable.uniqueId().get());
    
    There is no delimiter. Decorate("p") (_p) plus str(3007) (3007) yields the exact same HLSL identifier: _p3007.
  8. Lexical Shadowing & OOB Write: The emitted HLSL contains an inner block overriding the parameter scope:
    void f(out float _p3007[256], in int _idx) {
        {
            float _p3007[1] = {0.0}; 
            _p3007[0] = 0.0;
            _p3007[int(clamp(float(_idx), 0.0, 255.0))] = 1.0;
        }
    }
    
    Microsoft’s fxc/D3DCompile applies C++ scoping rules. The identifier _p3007 dynamically binds to the innermost declaration (the 1-element dcl_indexableTemp array).
  9. Driver Execution: The attacker supplies idx = 200. The shader attempts to write to index 200 of a size-1 array. D3D11 robust buffer access mitigations do not apply to thread-private indexable-temp arrays, leading to a driver-level OOB memory write across GPU-resident thread-local storage.

Suggested Fix

Introduce a delimiter (such as an underscore) in third_party/angle/src/compiler/translator/hlsl/UtilsHLSL.cpp:859 when combining the variable name and its unique ID:

return Decorate(variable.name()) + "_" + str(variable.uniqueId().get());

3. Technical Verification Details (Automated Audit Logs - Reviewers may skip this section)

> The vulnerability is valid. The missing delimiter in DecorateVariableIfNeeded when appending uniqueId allows HLSL identifier collisions. Although the reporter’s primary EvqGlobal PoC is flawed in current ANGLE (as EvqGlobal now also gets its uniqueId appended), the parameter variant (EvqParamIn/Out/InOut) and two-locals variant are fully sound. Parameters fall to the else branch and omit the uniqueId, so an out float pN[256] parameter emits as _pN. This collides with a nested local float p[1] (assigned uniqueId N), which emits as _p + N = _pN. ClampIndirectIndices uses the AST type to bound the index to [0, 255], but D3DCompile lexically binds the emitted _pN to the size-1 array. This results in an OOB dcl_indexableTemp write on the D3D11 driver (GPU-SHADER). Reachable by A-SERVER via WebGL. Because D3D11 is Windows-only ([no-Android]), the GPU process is sandboxed, capping the severity at High (S1) per the gating table.

Static Analysis and Code Reachability Proofs

  • Missing Delimiter: UtilsHLSL.cpp:856-859 verified: else if (variable.symbolType() == SymbolType::UserDefined && (qualifier == EvqTemporary || qualifier == EvqGlobal || qualifier == EvqConst)) { return Decorate(variable.name()) + str(variable.uniqueId().get()); }
  • Parameter Qualifier Fall-Through: Parameters use EvqParamIn, EvqParamOut, etc. The codebase_investigator trace on UtilsHLSL.cpp confirms they fall into the else block (line 861), which returns Decorate(variable.name()) without uniqueId.
  • Clamp Bounds Sourced from AST: tree_ops/ClampIndirectIndices.cpp:72-82 relies purely on leftType.getOutermostArraySize() to compute createClampValue(arraySize - 1, ...), extracting the 256 size from the AST node before output emission.
  • Optimization Bypass (RemoveUnreferencedVariables): A dummy access (p[0] = 0.0;) is required in the PoC. tree_ops/RemoveUnreferencedVariables.cpp:243-266 prunes EvqTemporary nodes if their reference count is 1. The modified dummy-reference PoC forces a refcount >= 2, effectively surviving this pass and reaching HLSL emission.

Environmental Assumptions & Validation Flags

  • fxc Shadowing Behavior: Standard HLSL (like C++) evaluates identifier shadowing by binding inner scopes over outer scopes. This scoping fidelity is explicitly preserved by OutputHLSL::visitBlock (lines 2026-2088) wrapping body contents in {...}.
  • D3D11 dcl_indexableTemp Robustness: The D3D11.3 specification does not require bounds checking for dynamic indirect accesses into thread-private dcl_indexableTemp storage. Any OOB write generates driver-undefined behavior, traditionally resulting in memory corruption of adjacent thread-private state.
  • Target Backend: The issue specifically relies on ClampIndirectIndices which is forced active for D3D backends via libANGLE/renderer/d3d/ShaderD3D.cpp:292 (options->clampIndirectArrayBounds = true;).

Evaluated with Chrome root at commit: b96d2ec58f4f5f92b540a723966b199d6e9951b4


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