CVE-2026-79240
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
GLSLTestPassthroughsrc/tests/gl_tests/GLSLTest.cpp |
modified |
Files Changed
src/compiler/translator/hlsl/OutputHLSL.cppsrc/compiler/translator/hlsl/OutputHLSL.hsrc/compiler/translator/hlsl/UtilsHLSL.cppsrc/tests/gl_tests/GLSLTest.cpp
Patch
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 {};
Regression Test / PoC
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
{};
Original Bug Report
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.cppthird_party/angle/src/compiler/translator/hlsl/OutputHLSL.cppthird_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
- Unique ID Determinism: ANGLE assigns deterministic unique IDs to user-defined symbols. Per
third_party/angle/src/compiler/translator/SymbolTable.cpp:421,mUniqueIdCounterresets tokFirstUserDefinedSymbolId(3000) per compilation. An attacker can reliably predict that a specific local variable will receive a target ID (e.g.,3007) by padding declarations. - 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 ID3007. - Bypassing Optimization: To ensure the local variable
p[1]survives theRemoveUnreferencedVariablespass (tree_ops/RemoveUnreferencedVariables.cpp:252), the attacker includes a dummy reference likep[0] = 0.0;. - Target Assignment: The attacker adds a dynamic assignment targeting the parameter:
p3007[idx] = 1.0;, whereidxis an attacker-controlled uniform. - Clamping Pass Bound Logic: On D3D WebGL contexts,
compileOptions.clampIndirectArrayBoundstriggersClampIndirectIndices(Compiler.cpp:1039). Intree_ops/ClampIndirectIndices.cpp:72-82, the pass reads the AST type ofp3007(the 256-element array) and rewrites the index expression to enforce a maximum bound of 255:clamp(idx, 0, 255). - Parameter Emission without ID: During HLSL emission,
OutputHLSL::writeParameterevaluates the parameter (qualifierEvqParamOut). InUtilsHLSL.cpp:841(DecorateVariableIfNeeded), this qualifier falls to theelsebranch (line 861), outputtingDecorate(variable.name())without a unique ID suffix.Decorateprepends_, yielding_p3007. - Local Emission with ID (The Flaw):
OutputHLSLvisits the nested block and emits the local variablep(qualifierEvqTemporary).DecorateVariableIfNeededenters theelse ifbranch (line 856):There is no delimiter.return Decorate(variable.name()) + str(variable.uniqueId().get());Decorate("p")(_p) plusstr(3007)(3007) yields the exact same HLSL identifier:_p3007. - Lexical Shadowing & OOB Write: The emitted HLSL contains an inner block overriding the parameter scope:
Microsoft’s
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; } }fxc/D3DCompileapplies C++ scoping rules. The identifier_p3007dynamically binds to the innermost declaration (the 1-elementdcl_indexableTemparray). - 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-859verified: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. Thecodebase_investigatortrace onUtilsHLSL.cppconfirms they fall into theelseblock (line 861), which returnsDecorate(variable.name())withoutuniqueId. - Clamp Bounds Sourced from AST:
tree_ops/ClampIndirectIndices.cpp:72-82relies purely onleftType.getOutermostArraySize()to computecreateClampValue(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-266prunesEvqTemporarynodes 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
fxcShadowing Behavior: Standard HLSL (like C++) evaluates identifier shadowing by binding inner scopes over outer scopes. This scoping fidelity is explicitly preserved byOutputHLSL::visitBlock(lines 2026-2088) wrapping body contents in{...}.- D3D11
dcl_indexableTempRobustness: The D3D11.3 specification does not require bounds checking for dynamic indirect accesses into thread-privatedcl_indexableTempstorage. Any OOB write generates driver-undefined behavior, traditionally resulting in memory corruption of adjacent thread-private state. - Target Backend: The issue specifically relies on
ClampIndirectIndiceswhich is forced active for D3D backends vialibANGLE/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.