CVE-2026-9910
Overview
Files Changed
src/compiler/translator/ParseContext.cppsrc/tests/gl_tests/GLSLValidationTest.cpp
Patch
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)
Regression Test / PoC
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)
Original Bug Report
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.cppthird_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:
- A user variable named
X(exactly 1022 characters long) has_uprepended to it, yielding a 1024-character identifier_uX. - 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.
- 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; } - 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;. - Achieve OOB Access: The native driver compiles the shader, linking the access to the 1-element array but allowing indices up to 16383.
- Exploit GPU State: The attacker sets the
idxuniform 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:
- Treat it as a compilation error during the semantic analysis phase (e.g., rejecting identifiers longer than
1024 - kPrefixLength). - 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.