High CVSS 8.8 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
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
CWECWE-125, CWE-787 (Out-of-bounds read, Out-of-bounds write)
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-04-03 (per vendor advisory)
Fix landed2026-05-04 (public git)
Disclosed2026-05-27
Patch gap23 days — fix public in source before the advisory

Background

ANGLE shader translator
Compiles WebGL GLSL/ESSL into backend GLSL (for the OpenGL/passthrough backend), renaming user identifiers to avoid collisions with built-ins.
Identifier hashing / `_u` prefix
When no HashFunction is configured (the default for the passthrough decoder), HashName prefixes user identifiers with _ + UserVariableNamePrefix (u), unless prefixing would exceed kESSLMaxIdentifierLength (1024), in which case the name is returned unchanged.
`ClampIndirectIndices`
An AST pass (enabled on the OpenGL backend) that inserts a clamp(idx, 0, N-1) around indirect array accesses to enforce bounds, computed from the resolved array’s declared size.
`ParseContext`
The parser/semantic-analysis stage where identifier validity is enforced; mResources.UserVariableNamePrefix holds the prefix character.
`kMaxAvailableIdentifierLength`
New constant 1022 = the 1024 limit minus the two _u prefix characters.

Root Cause Analysis

The underlying flaw (described by the advisory, in HashNames.cpp, which is not in this diff) is that HashName silently returns a user identifier unmodified whenever adding the _u prefix would exceed 1024 characters. That breaks the invariant that every user identifier is uniquely prefixed: a 1022-char identifier X becomes the 1024-char _uX, while a distinct user identifier literally named _uX at 1024 chars is left unprefixed and therefore collides with the hashed form of X. Two AST symbols that are distinct during ClampIndirectIndices (so the clamp is sized to the larger/outer array) emit the identical string name in the final GLSL, where the native driver’s lexical scoping binds the access to the smaller inner array while retaining the oversized clamp range — defeating the bounds mitigation.

Rather than change the hashing, the patch forecloses the collision at parse time in src/compiler/translator/ParseContext.cpp. It adds constexpr size_t kMaxAvailableIdentifierLength = 1022; and, in the identifier-validation routine, rejects any identifier that is >= kMaxAvailableIdentifierLength long and begins with _ followed by mResources.UserVariableNamePrefix: it raises a compile error(...) (“identifiers beginning with _u must be < 1022 characters”) and returns false. Because a user identifier can no longer both start with _u and be long enough to equal a prefixed-and-truncated name, the hashed form of one variable can never coincide with the raw form of another. The accompanying GLSLValidationTest cases confirm the boundary: _u-prefixed identifiers of length 1022/1023/1024 are rejected, while 1021 is accepted. This matches the advisory’s eventual OOB consequence, though the memory-unsafe access itself occurs in the native driver and is not present in this diff.

Key insight
ANGLE’s bounds clamping relies on distinct AST symbols mapping to distinct emitted names, but the _u prefixing silently truncates/aliases over-long identifiers, so an inner small array and an outer large array could emit the same name _uX — the driver then applied the outer array’s large clamp to the inner array. The fix rejects the collision-enabling identifiers during parsing.

Attack Path

  1. Craft shader Attacker writes a WebGL2 shader declaring a large outer array under a 1022-char name X and, in an inner scope, a 1-element array explicitly named _uX (1024 chars) that hashes to the same emitted string.
  2. Submit to ANGLE The passthrough command decoder feeds the shader to ANGLE; ClampIndirectIndices sizes the clamp to the outer array because the AST symbols are still distinct.
  3. Emit colliding GLSL TOutputGLSLBase writes both arrays as _uX, producing e.g. _uX[clamp(idx, 0, 16383)] = 1.0; bound (by driver scoping) to the 1-element inner array.
  4. Compile natively The native OpenGL driver compiles the emitted GLSL, applying the large clamp range to the tiny array (inferred; driver-side).
  5. Out-of-bounds access Setting idx large drives reads/writes beyond the inner array into GPU shader-local storage — the achieved primitive is a GPU OOB access, potentially leaking cross-context data or corrupting GPU state (inferred, not in diff).

Impact Assessment

Reachable from an unprivileged renderer via a WebGL2 shader; the collision and clamp mis-sizing are substantiated by the fix, but the actual out-of-bounds memory access happens inside the native OpenGL driver’s compiled shader and is therefore (inferred) rather than shown by the diff. The advisory classes this CWE-125/CWE-787 (OOB read/write); the diff itself is an input-validation hardening that eliminates the identifier collision — the memory-safety sink is downstream in the driver. Strength is accordingly an inferred GPU-process OOB read/write (potential cross-origin leak or state corruption), gated on a driver that honors the colliding scoping as described.

Files Changed

  • src/compiler/translator/ParseContext.cpp
  • src/tests/gl_tests/GLSLValidationTest.cpp

Audit Directions

  • Name-hashing invariants
    Audit HashNames.cpp/HashName for other paths that silently return an unmodified name (null HashFunction, length caps) and could reintroduce collisions despite the parse-time guard.
  • Mitigations relying on name uniqueness
    Review other translator passes (not just ClampIndirectIndices) whose correctness assumes distinct AST symbols produce distinct emitted identifiers.
  • Prefix/limit edge cases
    Check UserVariableNamePrefix handling and kESSLMaxIdentifierLength arithmetic across backends/specs for off-by-one or multi-char-prefix assumptions.
From 9c75a29202926d5d584e40a28a6bf5c5fd1a2852 Mon Sep 17 00:00:00 2001
From: dan sinclair <dsinclair@chromium.org>
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 <geofflang@chromium.org>
Auto-Submit: dan sinclair <dsinclair@chromium.org>
Commit-Queue: dan sinclair <dsinclair@chromium.org>
---

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 vm...@google.com

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