Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read and write in ANGLE
DescriptionOut of bounds read and write in ANGLE
ComponentANGLE
Bug ClassOOB
Tracker523435970
Fix commitab15aec81e49 (angle/angle) +149/-7
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-21

Changed Functions

FunctionChangeNotes
NameMap
src/compiler/translator/HashNames.h
modified
ImmutableString
src/compiler/translator/HashNames.h
modified
TSymbol
src/compiler/translator/HashNames.h
modified
HashNamesTest
src/tests/compiler_tests/HashNames_test.cpp
modified

Files Changed

  • src/compiler/translator/HashNames.cpp
  • src/compiler/translator/HashNames.h
  • src/compiler/translator/ShaderLang.cpp
  • src/tests/angle_unittests.gni
  • src/tests/compiler_tests/HashNames_test.cpp
From ab15aec81e4976bef62ed9d16a19a3d59a15f297 Mon Sep 17 00:00:00 2001
From: Ken Russell <kbr@chromium.org>
Date: Mon, 13 Jul 2026 11:06:16 -0700
Subject: [PATCH] Translator: avoid collisions in hashed names.

When using shader name hashing (mainly used in WebGL implementations),
guard against collisions in the hashed names. Add a unit test for the
new logic.

Co-authored with Gemini.

Fixed: chromium:523435970
Change-Id: I6747f55f5f9fb5128965e05b9aed17de91dbc3a3
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8084755
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Reviewed-by: Amirali Abdolrashidi <abdolrashidi@google.com>
Commit-Queue: Kenneth Russell <kbr@chromium.org>
---

diff --git a/src/compiler/translator/HashNames.cpp b/src/compiler/translator/HashNames.cpp
index 62b4f6d..f94d9da 100644
--- a/src/compiler/translator/HashNames.cpp
+++ b/src/compiler/translator/HashNames.cpp
@@ -45,10 +45,9 @@
         NameMap::const_iterator it = nameMap->find(name.data());
         if (it != nameMap->end())
         {
-            // (How bout returning?)
             return;
         }
-        (*nameMap)[name.data()] = hashedName.data();
+        nameMap->insert(name.data(), hashedName.data());
     }
 }
 
@@ -74,9 +73,35 @@
         return res;
     }
 
-    // Has a hash function
-    ImmutableString hashedName = HashName(name, hashFunction);
-    AddToNameMapIfNotMapped(name, hashedName, nameMap);
+    // Has a hash function. If this name has already been mapped, reuse the
+    // existing entry so the same source identifier always maps to the same
+    // output identifier.
+    if (nameMap)
+    {
+        NameMap::const_iterator it = nameMap->find(name.data());
+        if (it != nameMap->end())
+        {
+            return ImmutableString(it->second);
+        }
+    }
+
+    const ImmutableString baseHashedName = HashName(name, hashFunction);
+    if (!nameMap)
+    {
+        return baseHashedName;
+    }
+
+    // Ensure the chosen output identifier is not already assigned to a
+    // different source identifier. If it is, append a numeric suffix until a
+    // unique name is found.
+    ImmutableString hashedName = baseHashedName;
+    unsigned int suffix        = 0u;
+    while (nameMap->containsHashedName(hashedName.data()))
+    {
+        ++suffix;
+        hashedName = BuildConcatenatedImmutableString(baseHashedName, '_', suffix);
+    }
+    nameMap->insert(name.data(), hashedName.data());
     return hashedName;
 }
 
diff --git a/src/compiler/translator/HashNames.h b/src/compiler/translator/HashNames.h
index 791fdf2..1f8c9ab 100644
--- a/src/compiler/translator/HashNames.h
+++ b/src/compiler/translator/HashNames.h
@@ -8,6 +8,7 @@
 #define COMPILER_TRANSLATOR_HASHNAMES_H_
 
 #include <map>
+#include <unordered_set>
 
 #include "GLSLANG/ShaderLang.h"
 #include "compiler/translator/Common.h"
@@ -15,7 +16,38 @@
 namespace sh
 {
 
-typedef std::map<TPersistString, TPersistString> NameMap;
+class NameMap
+{
+  public:
+    void insert(const TPersistString &name, const TPersistString &hashedName)
+    {
+        mNames[name] = hashedName;
+        mHashedNames.insert(hashedName);
+    }
+
+    void clear()
+    {
+        mNames.clear();
+        mHashedNames.clear();
+    }
+
+    bool containsHashedName(const TPersistString &hashedName) const
+    {
+        return mHashedNames.find(hashedName) != mHashedNames.end();
+    }
+
+    const std::map<TPersistString, TPersistString> &getInternalMap() const { return mNames; }
+
+    using const_iterator = std::map<TPersistString, TPersistString>::const_iterator;
+
+    const_iterator find(const TPersistString &name) const { return mNames.find(name); }
+    const_iterator end() const { return mNames.end(); }
+    const_iterator begin() const { return mNames.begin(); }
+
+  private:
+    std::map<TPersistString, TPersistString> mNames;
+    std::unordered_set<TPersistString> mHashedNames;
+};
 
 class ImmutableString;
 class TSymbol;
diff --git a/src/compiler/translator/ShaderLang.cpp b/src/compiler/translator/ShaderLang.cpp
index a4740a1..327e7a5 100644
--- a/src/compiler/translator/ShaderLang.cpp
+++ b/src/compiler/translator/ShaderLang.cpp
@@ -512,7 +512,7 @@
 {
     TCompiler *compiler = GetCompilerFromHandle(handle);
     ASSERT(compiler);
-    return &(compiler->getNameMap());
+    return &(compiler->getNameMap().getInternalMap());
 }
 
 const std::vector<ShaderVariable> *GetUniforms(const ShHandle handle)
diff --git a/src/tests/angle_unittests.gni b/src/tests/angle_unittests.gni
index 124657c..628f197 100644
--- a/src/tests/angle_unittests.gni
+++ b/src/tests/angle_unittests.gni
@@ -95,6 +95,7 @@
   "compiler_tests/FloatLex_test.cpp",
   "compiler_tests/GeometryShader_test.cpp",
   "compiler_tests/GlFragDataNotModified_test.cpp",
+  "compiler_tests/HashNames_test.cpp",
   "compiler_tests/ImmutableString_test.cpp",
   "compiler_tests/IntermNode_test.cpp",
   "compiler_tests/NV_draw_buffers_test.cpp",
diff --git a/src/tests/compiler_tests/HashNames_test.cpp b/src/tests/compiler_tests/HashNames_test.cpp
new file mode 100644
index 0000000..505896b
--- /dev/null
+++ b/src/tests/compiler_tests/HashNames_test.cpp
@@ -0,0 +1,84 @@
+//
+// Copyright 2026 The ANGLE Project Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// HashNames_test.cpp:
+//   Tests for hashed identifier names in the translated GLSL output.
+//
+
+#include "GLSLANG/ShaderLang.h"
+#include "angle_gl.h"
+#include "gtest/gtest.h"
+#include "tests/test_utils/compiler_test.h"
+
+using namespace sh;
+
+namespace
+{
+
+// A hash that depends only on the first character so that distinct identifiers
+// beginning with the same letter map to the same hashed name.
+khronos_uint64_t FirstCharHash(const char *str, size_t len)
+{
+    return len > 0 ? static_cast<khronos_uint64_t>(str[0]) : 0u;
+}
+
+class HashNamesTest : public MatchOutputCodeTest
+{
+  public:
+    HashNamesTest() : MatchOutputCodeTest(GL_FRAGMENT_SHADER, SH_ESSL_OUTPUT)
+    {
+        getResources()->HashFunction = FirstCharHash;
+    }
+};
+
+// When two distinct source identifiers map to the same hash value the
+// translator must still emit distinct names so the inner declaration does not
+// shadow the outer one in the translated output.
+TEST_F(HashNamesTest, CollidingNamesEmitDistinctIdentifiers)
+{
+    const std::string &shaderString =
+        R"(precision mediump float;
+        void main()
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/angle_unittests.gni b/src/tests/angle_unittests.gni
index 124657c..628f197 100644
--- a/src/tests/angle_unittests.gni
+++ b/src/tests/angle_unittests.gni
@@ -95,6 +95,7 @@
   "compiler_tests/FloatLex_test.cpp",
   "compiler_tests/GeometryShader_test.cpp",
   "compiler_tests/GlFragDataNotModified_test.cpp",
+  "compiler_tests/HashNames_test.cpp",
   "compiler_tests/ImmutableString_test.cpp",
   "compiler_tests/IntermNode_test.cpp",
   "compiler_tests/NV_draw_buffers_test.cpp",
diff --git a/src/tests/compiler_tests/HashNames_test.cpp b/src/tests/compiler_tests/HashNames_test.cpp
new file mode 100644
index 0000000..505896b
--- /dev/null
+++ b/src/tests/compiler_tests/HashNames_test.cpp
@@ -0,0 +1,84 @@
+//
+// Copyright 2026 The ANGLE Project Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// HashNames_test.cpp:
+//   Tests for hashed identifier names in the translated GLSL output.
+//
+
+#include "GLSLANG/ShaderLang.h"
+#include "angle_gl.h"
+#include "gtest/gtest.h"
+#include "tests/test_utils/compiler_test.h"
+
+using namespace sh;
+
+namespace
+{
+
+// A hash that depends only on the first character so that distinct identifiers
+// beginning with the same letter map to the same hashed name.
+khronos_uint64_t FirstCharHash(const char *str, size_t len)
+{
+    return len > 0 ? static_cast<khronos_uint64_t>(str[0]) : 0u;
+}
+
+class HashNamesTest : public MatchOutputCodeTest
+{
+  public:
+    HashNamesTest() : MatchOutputCodeTest(GL_FRAGMENT_SHADER, SH_ESSL_OUTPUT)
+    {
+        getResources()->HashFunction = FirstCharHash;
+    }
+};
+
+// When two distinct source identifiers map to the same hash value the
+// translator must still emit distinct names so the inner declaration does not
+// shadow the outer one in the translated output.
+TEST_F(HashNamesTest, CollidingNamesEmitDistinctIdentifiers)
+{
+    const std::string &shaderString =
+        R"(precision mediump float;
+        void main()
+        {
+            float aOuter[8];
+            aOuter[0] = 1.0;
+            {
+                float aInner[2];
+                aInner[0] = 2.0;
+                gl_FragColor = vec4(aOuter[0] + aInner[0]);
+            }
+        })";
+    compile(shaderString);
+
+    // The first identifier encountered keeps the bare hashed name.
+    EXPECT_TRUE(foundInCode("webgl_61[8]"));
+    // The second identifier must be emitted with a different name.
+    EXPECT_FALSE(foundInCode("webgl_61[2]"));
+}
+
+// When more than two source identifiers map to the same hash value each must be
+// emitted with its own distinct name.
+TEST_F(HashNamesTest, MultipleCollidingNamesEmitDistinctIdentifiers)
+{
+    const std::string &shaderString =
+        R"(precision mediump float;
+        void main()
+        {
+            float aFirst[5];
+            aFirst[0] = 1.0;
+            float aSecond[3];
+            aSecond[0] = 2.0;
+            float aThird[7];
+            aThird[0] = 3.0;
+            gl_FragColor = vec4(aFirst[0] + aSecond[0] + aThird[0]);
+        })";
+    compile(shaderString);
+
+    EXPECT_TRUE(foundInCode("webgl_61[5]"));
+    EXPECT_FALSE(foundInCode("webgl_61[3]"));
+    EXPECT_FALSE(foundInCode("webgl_61[7]"));
+}
+
+}  // anonymous namespace
Loading diff…

Original Bug Report

reported by rj...@google.com

Potential GPU OOB Access via ANGLE Identifier Hash Collision

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 potential vulnerability exists in ANGLE’s GLSL translator where identifier hash collisions allow for out-of-bounds GPU memory access. When CityHash64 generates identical hashes for different user-defined variable names, the emitted GLSL assigns them the same identifier, causing scope resolution confusion. This allows attackers to bypass AST-based bounds clamping and execute OOB reads/writes in the GPU process.

Affected files:

  • third_party/angle/src/compiler/translator/HashNames.cpp
  • gpu/command_buffer/service/gles2_cmd_decoder.cc

Estimated timestamp from git blame: Unknown (Google3 checkout)

Summary

A potential vulnerability in ANGLE’s GLSL translator allows for out-of-bounds (OOB) memory access on the GPU. The issue arises because Chromium configures ANGLE to use a non-cryptographic 64-bit hash function (base::legacy::CityHash64) for WebGL identifier obfuscation, without checking if the resulting hashed identifier collides with previously generated hashes. This allows an attacker to craft shader identifiers that collide, causing “Scope Resolution Confusion” and neutralizing bounds checks in the final translated GLSL output.

Root Cause Analysis

In third_party/angle/src/compiler/translator/HashNames.cpp, ANGLE hashes user-defined identifier names (e.g., variables, functions) to obfuscate source code and prevent clashes with built-ins. In Chromium’s WebGL implementation, this feature is enabled by default and utilizes base::legacy::CityHash64.

Because CityHash64 is an unseeded, non-cryptographic hash function, an attacker can feasibly compute a 64-bit collision offline in roughly $2^{32}$ operations. When hashing is enabled, sh::HashName computes the hash of the identifier and prefixes it (e.g., webgl_0123456789ABCDEF).

The vulnerability exists in how ANGLE caches these hashes in AddToNameMapIfNotMapped:

void AddToNameMapIfNotMapped(const ImmutableString &name,
                             const ImmutableString &hashedName,
                             NameMap *nameMap)
{
    if (nameMap)
    {
        NameMap::const_iterator it = nameMap->find(name.data());
        if (it != nameMap->end())
        {
            return;
        }
        (*nameMap)[name.data()] = hashedName.data();
    }
}

The implementation successfully checks if the original identifier string (name) already exists in the map. However, it fails to verify if the generated hashedName has already been assigned to a completely different original identifier. Consequently, two distinct AST symbols are mapped to the exact same string in the emitted GLSL.

Potential Exploitation Scenario

Note: These steps outline a potential theoretical attack path, as we do not currently have the capability to execute a working Proof of Concept.

  1. Collision Generation: The attacker computes two distinct strings, N_1 and N_2, that produce a CityHash64 collision.
  2. Shader Construction: In a malicious WebGL shader, the attacker declares N_1 as a large array (e.g., float N_1[16384]) in the global scope, and N_2 as a small array (e.g., float N_2[1]) in a nested inner scope.
  3. AST Construction: ANGLE parses the shader. Because N_1 and N_2 are different strings, they are tracked as distinct TVariables in the symbol table across overlapping scopes. Accessing N_1[index] inside the inner scope correctly resolves to the large outer array.
  4. Bounds Clamping: ANGLE applies security transformations. The ClampIndirectIndices pass inspects N_1[index], notes the size is 16384, and transforms the index to clamp(index, 0, 16383).
  5. Code Emission: During GLSL emission (TOutputGLSLBase), both variables are passed to HashName. Due to the collision, they both yield the exact same identifier (e.g., webgl_DEADBEEF).
  6. Driver Compilation: The browser sends the emitted GLSL to the underlying native GPU driver. The driver parses:
    • Global: float webgl_DEADBEEF[16384];
    • Inner: float webgl_DEADBEEF[1];
    • Access: webgl_DEADBEEF[clamp(index, 0, 16383)];
  7. OOB Access: Following standard GLSL scoping rules, the native driver resolves the access to the inner, size-1 array. However, the clamped index permits bounds up to 16383. When executed on the GPU, this results in out-of-bounds local memory access.

Impact

This vulnerability provides a primitive for out-of-bounds GPU memory reads and writes from a WebGL shader. Local memory on GPUs typically maps to registers or thread-local scratch memory. Accessing this memory out-of-bounds allows an attacker to corrupt GPU thread control flow, leading to arbitrary code execution within the GPU process. On platforms where the GPU process is unsandboxed (e.g., Android), this equates to a full system compromise.

Suggested Fix

There are multiple ways to address this:

  1. Add Collision Detection: Modify AddToNameMapIfNotMapped to also check the values in mNameMap to ensure the generated hashedName is not already in use. If a collision is detected, append a counter or re-hash.
  2. Seed the Hash Function: Seed CityHash64 with a high-entropy random value generated per-context or per-compilation, making offline collision generation impossible.
  3. Use an Incrementing ID: Instead of a hash, simply assign an incrementing integer ID to variables as they are encountered (e.g., webgl_var_1, webgl_var_2), completely eliminating the possibility of collisions.

Evaluated with Chrome root at commit: 65b3256311f3ab6fb9870eaa522de7e6dd2663bb


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