High watch CVSS 9.6 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in ANGLE
DescriptionOut of bounds write in ANGLE
ComponentANGLE
Sub-componentHLSL
Bug ClassOOB
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker523266585
Fix commit630cd843c4d3 (angle/angle) +32/-1
CWECWE-787 (Out-of-bounds write)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-06-12 (per vendor advisory)
Fix landed2026-07-09 (public git)
Disclosed2026-08-25
Patch gap47 days — fix public in source before the advisory

Background

ANGLE HLSL translator
The ANGLE backend that translates validated GLSL ES shaders into HLSL for the Direct3D driver, generating identifier names for every shader variable.
`DecorateVariableIfNeeded`
The utility that produces the emitted HLSL name for a shader variable, deciding whether to append the variable’s unique symbol id to disambiguate names.
`TQualifier` (`EvqTemporary` / `EvqGlobal` / `EvqConst`)
Storage/qualifier classes for shader variables: local/temporary, global (file-scope), and constant respectively.
Unique symbol id / `kFirstUserDefinedSymbolId`
A per-variable id (user-defined ids start at 3000) that ANGLE appends to local names so same-named variables in different scopes do not alias.

Root Cause Analysis

In src/compiler/translator/hlsl/UtilsHLSL.cpp, DecorateVariableIfNeeded appended the unique symbol id (str(variable.uniqueId().get())) only when a user-defined variable had qualifier EvqTemporary. Global (EvqGlobal) and constant (EvqConst) user-defined variables were emitted with an undecorated name, so a global variable declared with a name like _a3003 could collide in the generated HLSL with a local variable a that had been decorated to the suffixed name a3003. This violated the invariant that every distinct source variable maps to a distinct HLSL identifier, causing name aliasing between a global and a local in the translated program.

The fix broadens the suffixing condition to (qualifier == EvqTemporary || qualifier == EvqGlobal || qualifier == EvqConst), so globals and constants are also decorated with their unique id and can no longer collide with locals. The regression test constructs shaders whose global is named _a<id> for ids 3000–3009 and confirms the rendered result is correct after the fix.

Key insight
The core mistake was decorating only EvqTemporary user-defined variables with a unique id while leaving globals and constants undecorated, allowing an attacker-chosen global name to alias a local’s generated HLSL name; the fix decorates EvqGlobal and EvqConst as well so all user-defined names stay distinct.

Attack Path

  1. Craft a colliding shader An attacker supplies a GLSL ES shader with a global variable named to match the decorated form of a local (e.g. global _a3003 alongside a local a that receives suffix 3003).
  2. Translate to HLSL DecorateVariableIfNeeded decorates the local but not the global, so both map to the same HLSL identifier, producing a program where the two variables alias.
  3. Corrupt generated code The aliasing yields incorrect or malformed HLSL where distinct storage is conflated, which the advisory identifies as leading to an out-of-bounds write in ANGLE (inferred mechanism from the collision; the specific OOB sink is not shown in the diff).
  4. Out-of-bounds write (inferred) Per the advisory the resulting condition is an out-of-bounds write in the GPU process; the diff itself demonstrates only the name collision, so the write primitive and its location are (inferred).

Impact Assessment

An attacker able to submit a shader (e.g. via WebGL from a renderer) can force a global/local HLSL name collision that the advisory characterizes as an out-of-bounds write in ANGLE, occurring in the GPU process where the HLSL translator and D3D driver run. The precondition is the ability to compile an attacker-controlled shader through the HLSL backend. The advisory reports the class as an out-of-bounds write with no exploited-in-the-wild status stated; memory-corruption escalation beyond the name collision the patch fixes is (inferred).

Files Changed

  • src/compiler/translator/hlsl/UtilsHLSL.cpp
  • src/tests/gl_tests/GLSLTest.cpp

Audit Directions

  • Incomplete qualifier enumeration
    Look for name-generation or decoration logic in ANGLE translators that special-cases only a subset of TQualifier values, where an omitted qualifier can produce colliding or undecorated identifiers.
  • Uniqueness assumptions in codegen
    Audit any backend (HLSL, MSL, GLSL output) that relies on unique-id suffixing for disambiguation to confirm the suffix is applied to every user-defined symbol class, not just temporaries.
  • Attacker-controllable identifier names
    Review paths where user shader source can influence emitted identifier strings, since a chosen name matching an internal decoration scheme can force aliasing.
From 630cd843c4d352b46775f51d42f8da48e178c046 Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
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 <syoussefi@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
---
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 rj...@google.com

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