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
Tracker523266585
Fix commit630cd843c4d3 (angle/angle) +32/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Files Changed

  • src/compiler/translator/hlsl/UtilsHLSL.cpp
  • src/tests/gl_tests/GLSLTest.cpp
From 630cd843c4d352b46775f51d42f8da48e178c046 Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <[email protected]>
Date: Mon, 06 Jul 2026 15:48:32 -0400
Subject: [PATCH] HLSL: Fix name collision between globals and locals

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

diff --git a/src/compiler/translator/hlsl/UtilsHLSL.cpp b/src/compiler/translator/hlsl/UtilsHLSL.cpp
index 53912e5..96ca73f 100644
--- a/src/compiler/translator/hlsl/UtilsHLSL.cpp
+++ b/src/compiler/translator/hlsl/UtilsHLSL.cpp
@@ -841,6 +841,7 @@
 
 TString DecorateVariableIfNeeded(const TVariable &variable)
 {
+    const TQualifier qualifier = variable.getType().getQualifier();
     if (variable.symbolType() == SymbolType::AngleInternal ||
         variable.symbolType() == SymbolType::BuiltIn || variable.symbolType() == SymbolType::Empty)
     {
@@ -854,7 +855,7 @@
     // 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 &&
-             variable.getType().getQualifier() == EvqTemporary)
+             (qualifier == EvqTemporary || qualifier == EvqGlobal || qualifier == EvqConst))
     {
         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 f80bd3b..ec1978a 100644
--- a/src/tests/gl_tests/GLSLTest.cpp
+++ b/src/tests/gl_tests/GLSLTest.cpp
@@ -24506,6 +24506,36 @@
     EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(51, 153, 0, 255), 1);
     ASSERT_GL_NO_ERROR();
 }
+
+// Regression test for a bug in the HLSL generator where the global variable names could collide
+// with local variable names.  In particular, the local variables were suffixed with the symbol id,
+// starting from 3000 (kFirstUserDefinedSymbolId) but the global variables weren't.
+TEST_P(GLSLTest_ES3, HLSLGlobalNameCollisionWithLocalVar)
+{
+    // 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 _a)" << id
+           << R"( = 0.5;
+void main()
+{
+  float a = 0.2;
+  a = _a)" << id
+           << 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);
+        ASSERT_GL_NO_ERROR();
+    }
+}
 }  // anonymous namespace
 
 ANGLE_INSTANTIATE_TEST_ES2_AND_ES3_AND_ES31_AND_ES32(
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 f80bd3b..ec1978a 100644
--- a/src/tests/gl_tests/GLSLTest.cpp
+++ b/src/tests/gl_tests/GLSLTest.cpp
@@ -24506,6 +24506,36 @@
     EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(51, 153, 0, 255), 1);
     ASSERT_GL_NO_ERROR();
 }
+
+// Regression test for a bug in the HLSL generator where the global variable names could collide
+// with local variable names.  In particular, the local variables were suffixed with the symbol id,
+// starting from 3000 (kFirstUserDefinedSymbolId) but the global variables weren't.
+TEST_P(GLSLTest_ES3, HLSLGlobalNameCollisionWithLocalVar)
+{
+    // 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 _a)" << id
+           << R"( = 0.5;
+void main()
+{
+  float a = 0.2;
+  a = _a)" << id
+           << 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);
+        ASSERT_GL_NO_ERROR();
+    }
+}
 }  // anonymous namespace
 
 ANGLE_INSTANTIATE_TEST_ES2_AND_ES3_AND_ES31_AND_ES32(
Loading diff…

Original Bug Report

reported by [email protected]

Potential OOB access in WebGL via HLSL local variable shadowing

Flapjack, 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 deterministic identifier collision in ANGLE’s HLSL translator allows local variables to shadow global variables in the generated HLSL code. This shadowing causes AST-based array bounds checks to be applied to the wrong variable, potentially leading to out-of-bounds reads and writes on the GPU stack.

Affected files:

  • third_party/angle/src/compiler/translator/hlsl/UtilsHLSL.cpp

Estimated timestamp from git blame: 2019-02-04

Background

When ANGLE translates GLSL to HLSL for the Direct3D backend, it decorates identifiers to avoid name collisions. This logic is implemented in DecorateVariableIfNeeded within third_party/angle/src/compiler/translator/hlsl/UtilsHLSL.cpp.

Global variables are decorated by prepending an underscore _. For example, a global variable named a3005 becomes _a3005.

Local variables (which receive the EvqTemporary qualifier) are decorated by prepending _ and appending the variable’s internal sequential uniqueId. For example, a local variable named a that is assigned a uniqueId of 3005 also becomes _a3005.

Vulnerability Details

Because TSymbolTable resets its mUniqueIdCounter to kFirstUserDefinedSymbolId (3000) at the start of every shader compilation, unique IDs are assigned deterministically. An attacker can control a local variable’s ID by declaring a specific number of dummy variables prior to it.

This predictable assignment allows a local variable to intentionally collide with the decorated name of a global variable. When the HLSL is generated, standard scoping rules apply, and the local declaration within the function shadows the global declaration.

This shadowing bypasses ANGLE’s array bounds checking pass, ClampIndirectIndices. The pass inserts a clamp() operation based on the size of the array in the Abstract Syntax Tree (AST). If the AST correctly points to a large global array, a large clamp bound is generated. However, the underlying HLSL compiler will execute this clamped access against the shadowed, smaller local array, resulting in out-of-bounds memory access.

Potential Attacker Steps

An attacker could potentially trigger this by supplying the following WebGL shader:

  1. Declare a large global array: float a3005[100];.
  2. Declare dummy variables to increment the internal symbol ID counter to exactly 3004.
  3. Inside a function, declare a small local array: float a[1];. This variable receives uniqueId 3005.
  4. Access the global array using an attacker-controlled index: a3005[index];.

During compilation:

  • ClampIndirectIndices analyzes the AST, sees the access to the global a3005 of size 100, and emits: clamp(index, 0, 99).
  • OutputHLSL generates the global variable: static float _a3005[100];.
  • OutputHLSL generates the local variable, creating a name collision: float _a3005[1];.
  • OutputHLSL generates the array access: _a3005[clamp(index, 0, 99)].

When the HLSL executes, _a3005 resolves to the local array of size 1. An attacker supplying an index of 50 will read/write memory far past the end of the local array on the GPU’s stack or registers.

(Note: These are potential steps based on code analysis; our tooling agent does not yet have the ability to run code to confirm a live exploit.)

Impact

Out-of-bounds reads and writes on the GPU can lead to arbitrary code execution within the GPU process. On platforms where HLSL is used (primarily Windows), this constitutes a sandbox escape from the highly restricted WebGL renderer process into the GPU process.

Suggested Fix

The decoration scheme for temporary variables should use a separator that cannot be part of a user-defined GLSL identifier. Since WebGL shaders are restricted from using double underscores __ or starting identifiers with certain characters, using a safer decoration pattern would prevent these collisions.

For example, in third_party/angle/src/compiler/translator/hlsl/UtilsHLSL.cpp:

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

Evaluated with Chrome root at commit: 2155cb00003ec35716a76ed3246eae995f87b7ff


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