Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds memory access in ANGLE
DescriptionOut of bounds memory access in ANGLE
ComponentANGLE
Bug ClassOOB
Tracker499176133
Fix commit9c75a2920292 (angle/angle) +80/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Files Changed

  • src/compiler/translator/ParseContext.cpp
  • src/tests/gl_tests/GLSLValidationTest.cpp
From 9c75a29202926d5d584e40a28a6bf5c5fd1a2852 Mon Sep 17 00:00:00 2001
From: dan sinclair <[email protected]>
Date: Mon, 04 May 2026 14:06:58 -0400
Subject: [PATCH] Handle `_u` prefix on long identifiers.

If an identifier is >= 1022 characters and begins with `_u` this will
cause issues for the hashing done on identifier names. Turn it into a
parse error if such an identifier is encountered.

Fixed: chromium:499176133
Change-Id: I18c993335cb819b687c0af7fc6898d8b4a8baba8
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7808385
Reviewed-by: Geoff Lang <[email protected]>
Auto-Submit: dan sinclair <[email protected]>
Commit-Queue: dan sinclair <[email protected]>
---

diff --git a/src/compiler/translator/ParseContext.cpp b/src/compiler/translator/ParseContext.cpp
index 43a7bf5..b3c959f 100644
--- a/src/compiler/translator/ParseContext.cpp
+++ b/src/compiler/translator/ParseContext.cpp
@@ -54,6 +54,9 @@
 constexpr size_t kWebGLMaxPrivateVariableSizeInBytes = static_cast<size_t>(64) * 1024;
 constexpr size_t kWebGLMaxTotalPrivateVariableSizeInBytes = static_cast<size_t>(16) * 1024 * 1024;
 
+// The 1024 character identifier limit, `-2` for the `_u`
+constexpr size_t kMaxAvailableIdentifierLength = 1022;
+
 bool ShouldEnforceESSL100LoopAndIndexingLimitations(ShShaderSpec spec,
                                                     int shaderVersion,
                                                     const ShCompileOptions &compileOptions)
@@ -1231,6 +1234,18 @@
                     identifier.data());
         }
     }
+
+    // Validate that identifier names won't conflict with the name hashing done later.
+    // See https://crbug.com/499176133
+    if ((identifier.length() >= kMaxAvailableIdentifierLength) &&
+        mResources.UserVariableNamePrefix != '\0' && identifier[0] == '_' &&
+        identifier[1] == mResources.UserVariableNamePrefix)
+    {
+        std::string err = "identifiers beginning with `_u` must be < " +
+                          std::to_string(kMaxAvailableIdentifierLength) + " characters";
+        error(line, err.c_str(), identifier);
+        return false;
+    }
     return true;
 }
 
diff --git a/src/tests/gl_tests/GLSLValidationTest.cpp b/src/tests/gl_tests/GLSLValidationTest.cpp
index 5908233..2568a07 100644
--- a/src/tests/gl_tests/GLSLValidationTest.cpp
+++ b/src/tests/gl_tests/GLSLValidationTest.cpp
@@ -257,6 +257,71 @@
                   "'==' : undefined operation for structs containing samplers");
 }
 
+// https://crbug.com/499176133
+TEST_P(GLSLValidationTest, LongIdentifierAtLimit_1024)
+{
+    std::string longName = "_u";
+    longName.append(1024 - 2, 'a');
+    std::string shader = R"(
+void main() {
+  precision mediump float;
+  float )" + longName + R"( = 1.0;
+})";
+
+    std::string result =
+        std::string("'") + longName +
+        std::string("' : identifiers beginning with `_u` must be < 1022 characters");
+
+    validateError(GL_FRAGMENT_SHADER, shader.c_str(), result.c_str());
+}
+// https://crbug.com/499176133
+TEST_P(GLSLValidationTest, LongIdentifierAtLimit_1023)
+{
+    std::string longName = "_u";
+    longName.append(1023 - 2, 'a');
+    std::string shader = R"(
+void main() {
+  precision mediump float;
+  float )" + longName + R"( = 1.0;
+})";
+
+    std::string result =
+        std::string("'") + longName +
+        std::string("' : identifiers beginning with `_u` must be < 1022 characters");
+
+    validateError(GL_FRAGMENT_SHADER, shader.c_str(), result.c_str());
+}
+// https://crbug.com/499176133
+TEST_P(GLSLValidationTest, LongIdentifierAtLimit_1022)
+{
+    std::string longName = "_u";
+    longName.append(1022 - 2, 'a');
+    std::string shader = R"(
+void main() {
+  precision mediump float;
+  float )" + longName + R"( = 1.0;
+})";
+
+    std::string result =
+        std::string("'") + longName +
+        std::string("' : identifiers beginning with `_u` must be < 1022 characters");
+
+    validateError(GL_FRAGMENT_SHADER, shader.c_str(), result.c_str());
+}
+// https://crbug.com/499176133
+TEST_P(GLSLValidationTest, LongIdentifierAtLimit_1021)
+{
+    std::string longName = "_u";
+    longName.append(1021 - 2, 'a');
+    std::string shader = R"(
+void main() {
+  precision mediump float;
+  float )" + longName + R"( = 1.0;
+})";
+
+    validateSuccess(GL_FRAGMENT_SHADER, shader.c_str());
+}
+
 // Samplers are not allowed as l-values (ESSL 3.00 section 4.1.7), our interpretation is that this
 // extends to structs containing samplers. ESSL 1.00 spec is clearer about this.
 TEST_P(GLSLValidationTest_ES3, AssignStructsContainingSamplers)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/GLSLValidationTest.cpp b/src/tests/gl_tests/GLSLValidationTest.cpp
index 5908233..2568a07 100644
--- a/src/tests/gl_tests/GLSLValidationTest.cpp
+++ b/src/tests/gl_tests/GLSLValidationTest.cpp
@@ -257,6 +257,71 @@
                   "'==' : undefined operation for structs containing samplers");
 }
 
+// https://crbug.com/499176133
+TEST_P(GLSLValidationTest, LongIdentifierAtLimit_1024)
+{
+    std::string longName = "_u";
+    longName.append(1024 - 2, 'a');
+    std::string shader = R"(
+void main() {
+  precision mediump float;
+  float )" + longName + R"( = 1.0;
+})";
+
+    std::string result =
+        std::string("'") + longName +
+        std::string("' : identifiers beginning with `_u` must be < 1022 characters");
+
+    validateError(GL_FRAGMENT_SHADER, shader.c_str(), result.c_str());
+}
+// https://crbug.com/499176133
+TEST_P(GLSLValidationTest, LongIdentifierAtLimit_1023)
+{
+    std::string longName = "_u";
+    longName.append(1023 - 2, 'a');
+    std::string shader = R"(
+void main() {
+  precision mediump float;
+  float )" + longName + R"( = 1.0;
+})";
+
+    std::string result =
+        std::string("'") + longName +
+        std::string("' : identifiers beginning with `_u` must be < 1022 characters");
+
+    validateError(GL_FRAGMENT_SHADER, shader.c_str(), result.c_str());
+}
+// https://crbug.com/499176133
+TEST_P(GLSLValidationTest, LongIdentifierAtLimit_1022)
+{
+    std::string longName = "_u";
+    longName.append(1022 - 2, 'a');
+    std::string shader = R"(
+void main() {
+  precision mediump float;
+  float )" + longName + R"( = 1.0;
+})";
+
+    std::string result =
+        std::string("'") + longName +
+        std::string("' : identifiers beginning with `_u` must be < 1022 characters");
+
+    validateError(GL_FRAGMENT_SHADER, shader.c_str(), result.c_str());
+}
+// https://crbug.com/499176133
+TEST_P(GLSLValidationTest, LongIdentifierAtLimit_1021)
+{
+    std::string longName = "_u";
+    longName.append(1021 - 2, 'a');
+    std::string shader = R"(
+void main() {
+  precision mediump float;
+  float )" + longName + R"( = 1.0;
+})";
+
+    validateSuccess(GL_FRAGMENT_SHADER, shader.c_str());
+}
+
 // Samplers are not allowed as l-values (ESSL 3.00 section 4.1.7), our interpretation is that this
 // extends to structs containing samplers. ESSL 1.00 spec is clearer about this.
 TEST_P(GLSLValidationTest_ES3, AssignStructsContainingSamplers)
Loading diff…

Original Bug Report

reported by [email protected]

OOB Access in WebGL via ANGLE Identifier Collision and Clamp Bypass

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: A logic flaw in ANGLE’s identifier renaming allows attackers to force name collisions in the generated GLSL by exploiting the 1024-character length limit. This collision subverts lexical scoping rules after ANGLE’s AST-based bounds clamping has occurred. Consequently, the native OpenGL driver applies a large valid clamp range to a small inner array, allowing potential out-of-bounds memory access in GPU shader-local storage.

Affected files:

  • third_party/angle/src/compiler/translator/HashNames.cpp
  • third_party/angle/src/compiler/translator/tree_ops/ClampIndirectIndices.cpp

Estimated timestamp from git blame: 2025-07-24

Root Cause Analysis

In ANGLE’s third_party/angle/src/compiler/translator/HashNames.cpp, the HashName function is responsible for prefixing user-defined variables (typically with _u) to prevent collisions with built-in GLSL variables. However, if the HashFunction is nullptr (which is the default for the passthrough command decoder on the OpenGL backend), the function uses a length check to avoid generating identifiers longer than the GLSL spec limit of 1024 characters:

size_t kPrefixLength = 2;
if (!prefix || name.length() + kPrefixLength > kESSLMaxIdentifierLength)
{
    return name; // Returns unmodified if prefixing exceeds 1024 characters
}
// Otherwise prepends '_' + prefix (e.g., '_u')
ImmutableString res = BuildConcatenatedImmutableString('_', prefix, name);

Because the preprocessor allows user tokens up to 1024 characters (kESSLMaxIdentifierLength), a collision can be engineered:

  1. A user variable named X (exactly 1022 characters long) has _u prepended to it, yielding a 1024-character identifier _uX.
  2. A second user variable explicitly named _uX (exactly 1024 characters long) triggers the length limit and is returned unmodified as _uX.

Exploit Mechanism (Clamp Bypass)

This collision becomes a security vulnerability when combined with ANGLE’s ClampIndirectIndices pass, which is enabled on the OpenGL backend. The clamping pass operates on the Abstract Syntax Tree (AST), where the two variables are still distinct symbols.

If an attacker declares the 1022-character variable as a large array in an outer scope, and the 1024-character variable as a small array in an inner scope, ANGLE’s AST will correctly resolve an array access in the inner scope to the outer array. The compiler will insert a clamp() matching the outer array’s large bounds.

However, when TOutputGLSLBase generates the final GLSL text, both variables share the exact same string name _uX. When the native OpenGL driver parses this emitted GLSL, standard lexical scoping causes the identifier to resolve to the inner (small) array, but it still retains the large clamp() range inserted by ANGLE.

Potential Attacker Steps

Note: These are suggested steps based on source code analysis, as our tooling agent cannot execute code to verify a live PoC.

  1. Craft the Shader: An attacker provides a WebGL2 fragment shader containing the following structure:
    // 'X' is 1022 chars long
    float X[16384]; 
    {
      // '_uX' is 1024 chars long, exactly matching the prefixed outer name
      float _uX[1]; 
      // ANGLE clamps this based on the outer array: clamp(idx, 0, 16383)
      X[idx] = 1.0; 
    }
    
  2. Trigger the Collision: The passthrough command decoder sends this to ANGLE. ANGLE emits GLSL where both arrays are named _uX (1024 chars long). The emitted assignment becomes _uX[clamp(idx, 0, 16383)] = 1.0;.
  3. Achieve OOB Access: The native driver compiles the shader, linking the access to the 1-element array but allowing indices up to 16383.
  4. Exploit GPU State: The attacker sets the idx uniform to a large value, achieving out-of-bounds reads/writes in the GPU’s shader-local storage (scratch memory/registers). This can potentially be leveraged to leak cross-origin data from other contexts sharing the GPU or corrupt the GPU process.

Suggested Fix

The HashName logic should not silently return the original string if appending the prefix exceeds the maximum identifier length, as this breaks the guarantee that all user variables are safely prefixed.

If a user-defined identifier is so long that adding the _u prefix would exceed 1024 characters, the compiler should either:

  1. Treat it as a compilation error during the semantic analysis phase (e.g., rejecting identifiers longer than 1024 - kPrefixLength).
  2. Use a guaranteed-safe fallback hashing mechanism that always generates a short, unique name regardless of the input length.

Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33


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