Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in ANGLE
DescriptionOut of bounds read in ANGLE
ComponentANGLE
Bug ClassOOB
Tracker499031961
Fix commit268683ad32ba (angle/angle) +36/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Files Changed

  • src/compiler/translator/Symbol.cpp
  • src/compiler/translator/Types.cpp
  • src/tests/gl_tests/GLSLValidationTest.cpp
From 268683ad32ba2c93c64518d2dc7a8d8058061ac3 Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Tue, 07 Apr 2026 17:07:23 -0400
Subject: [PATCH] Translator: Fix struct name mangling collision

Bug: chromium:499031961
Change-Id: I13a459386b40bcbaaf9333bd726b0a912f89827f
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7736235
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
---

diff --git a/src/compiler/translator/Symbol.cpp b/src/compiler/translator/Symbol.cpp
index 9c88538..2271e2c 100644
--- a/src/compiler/translator/Symbol.cpp
+++ b/src/compiler/translator/Symbol.cpp
@@ -32,8 +32,6 @@
 constexpr const ImmutableString kImageAtomicExchangeName("imageAtomicExchange");
 constexpr const ImmutableString kAtomicCounterName("atomicCounter");
 
-static const char kFunctionMangledNameSeparator = '(';
-
 }  // anonymous namespace
 
 TSymbol::TSymbol(TSymbolTable *symbolTable,
@@ -230,6 +228,8 @@
 
 ImmutableString TFunction::buildMangledName() const
 {
+    constexpr char kFunctionMangledNameSeparator = '(';
+
     ImmutableString name = this->name();
     std::string newName(name.data(), name.length());
     newName += kFunctionMangledNameSeparator;
diff --git a/src/compiler/translator/Types.cpp b/src/compiler/translator/Types.cpp
index c0882da..a3564df 100644
--- a/src/compiler/translator/Types.cpp
+++ b/src/compiler/translator/Types.cpp
@@ -463,6 +463,8 @@
     }
     else
     {
+        constexpr char kStructMangledNameSeparator = ':';
+
         ASSERT(type == EbtStruct || type == EbtInterfaceBlock);
         switch (type)
         {
@@ -472,12 +474,14 @@
                 {
                     mangledName += mStructure->name().data();
                 }
+                mangledName += kStructMangledNameSeparator;
                 mangledName += mStructure->mangledFieldList();
                 mangledName += '}';
                 break;
             case EbtInterfaceBlock:
                 mangledName += "{i";
                 mangledName += mInterfaceBlock->name().data();
+                mangledName += kStructMangledNameSeparator;
                 mangledName += mInterfaceBlock->mangledFieldList();
                 mangledName += '}';
                 break;
diff --git a/src/tests/gl_tests/GLSLValidationTest.cpp b/src/tests/gl_tests/GLSLValidationTest.cpp
index 438df0d..4b40128 100644
--- a/src/tests/gl_tests/GLSLValidationTest.cpp
+++ b/src/tests/gl_tests/GLSLValidationTest.cpp
@@ -2291,6 +2291,36 @@
                   "l-value required (can't modify an input \"f\")");
 }
 
+// Test no mangling collision in structs
+TEST_P(GLSLValidationTest, ManglingCollisionInStruct)
+{
+    constexpr char kFS[] = R"(precision mediump float;
+struct A00B { vec4 y; };
+struct A    { float x; vec4 y; };
+
+void foo(A00B p);
+void foo(A p) {}
+
+void main() {
+    A00B v = A00B(vec4(0));
+    foo(v);
+})";
+    validateError(GL_FRAGMENT_SHADER, kFS, "Function foo() called by main() is undefined");
+}
+
+// Test no mangling collision in function parameters
+TEST_P(GLSLValidationTest, ManglingCollisionInFunctionParams)
+{
+    constexpr char kFS[] = R"(precision mediump float;
+void fooA00B(vec4 y);
+void foo(float x, vec4 y) {}
+
+void main() {
+    fooA00B(vec4(0));
+})";
+    validateError(GL_FRAGMENT_SHADER, kFS, "Function fooA00B() called by main() is undefined");
+}
+
 // Test that infinite loop with while(true) is rejected
 TEST_P(WebGL2GLSLValidationTest, InfiniteLoopWhileTrue)
 {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/GLSLValidationTest.cpp b/src/tests/gl_tests/GLSLValidationTest.cpp
index 438df0d..4b40128 100644
--- a/src/tests/gl_tests/GLSLValidationTest.cpp
+++ b/src/tests/gl_tests/GLSLValidationTest.cpp
@@ -2291,6 +2291,36 @@
                   "l-value required (can't modify an input \"f\")");
 }
 
+// Test no mangling collision in structs
+TEST_P(GLSLValidationTest, ManglingCollisionInStruct)
+{
+    constexpr char kFS[] = R"(precision mediump float;
+struct A00B { vec4 y; };
+struct A    { float x; vec4 y; };
+
+void foo(A00B p);
+void foo(A p) {}
+
+void main() {
+    A00B v = A00B(vec4(0));
+    foo(v);
+})";
+    validateError(GL_FRAGMENT_SHADER, kFS, "Function foo() called by main() is undefined");
+}
+
+// Test no mangling collision in function parameters
+TEST_P(GLSLValidationTest, ManglingCollisionInFunctionParams)
+{
+    constexpr char kFS[] = R"(precision mediump float;
+void fooA00B(vec4 y);
+void foo(float x, vec4 y) {}
+
+void main() {
+    fooA00B(vec4(0));
+})";
+    validateError(GL_FRAGMENT_SHADER, kFS, "Function fooA00B() called by main() is undefined");
+}
+
 // Test that infinite loop with while(true) is rejected
 TEST_P(WebGL2GLSLValidationTest, InfiniteLoopWhileTrue)
 {
Loading diff…

Original Bug Report

reported by vm...@google.com

ANGLE struct mangled-name collision enables invalid SPIR-V generation

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: ANGLE’s GLSL translator lacks a separator between a struct’s name and its mangled field list, allowing distinct structs to produce identical mangled names. This type confusion mutates function prototypes, leading to the generation of type-mismatched SPIR-V code that bypasses validation and is passed to the GPU driver.

Affected files:

  • third_party/angle/src/compiler/translator/Types.cpp
  • third_party/angle/src/compiler/translator/SymbolTable.cpp
  • third_party/angle/src/compiler/translator/ParseContext.cpp
  • third_party/angle/src/compiler/translator/Symbol.cpp
  • third_party/angle/src/compiler/translator/spirv/OutputSPIRV.cpp

Estimated timestamp from git blame: 2018-03-13

Summary

A potential vulnerability in ANGLE’s GLSL translator allows an attacker to cause a mangled-name collision between structurally distinct structs. This can be abused to trigger type confusion in the compiler’s symbol table, resulting in ANGLE generating invalid SPIR-V instructions. Because ANGLE disables SPIR-V validation in release builds, the malformed instructions are passed directly to the underlying GPU driver. This could potentially lead to driver-level memory corruption and a GPU process sandbox escape.

Note: These steps are based on static code analysis; a working Proof of Concept has not yet been executed by our tooling.

Root Cause Analysis

In third_party/angle/src/compiler/translator/Types.cpp, TType::buildMangledName() constructs a mangled name for a struct by concatenating "{s", the struct name, and the mangledFieldList() without any delimiter. Since the mangled field list uses standard alphanumeric characters, two distinct structs can produce the exact same mangled string.

For example:

  • struct A00B { vec4 y; } (Name: A00B, Field: vec4 mangles to 30B) -> Mangled Name: 0{sA00B30B}
  • struct A { float x; vec4 y; } (Name: A, Fields: float mangles to 00B, vec4 to 30B) -> Mangled Name: 0{sA00B30B}

Potential Exploitation Steps

An attacker could exploit this type confusion by supplying a malicious WebGL shader:

  1. The shader defines the two colliding structs, a function prototype void foo(A00B p);, and a function definition void foo(A p) { ... }.
  2. During parsing, ANGLE computes identical mangled names for both the prototype and the definition (e.g., foo(0{sA00B30B}).
  3. In ParseContext.cpp, the definition is mapped to the existing prototype. TSymbolTable::setFunctionParameterNamesFromDefinition is called, which invokes firstDeclaration->shareParameters(*function).
  4. TFunction::shareParameters performs a shallow copy of the definition’s parameter pointers (mParameters) onto the prototype. The prototype is now mutated and internally expects an argument of type struct A instead of struct A00B.
  5. The attacker calls foo(v) inside main() with an argument v of type struct A00B. The parser matches this call to the mutated prototype because the mangled names match.
  6. During SPIR-V translation (spirv/OutputSPIRV.cpp), createFunctionCall allocates a temporary variable for the expected parameter type (struct A*).
  7. It then emits an OpStore instruction storing the caller’s argument object (struct A00B) into the temporary variable (struct A*).
  8. This violates the SPIR-V specification, which strictly requires the object type to match the pointer’s pointed-to type.
  9. In Release builds of Chromium, spirv::Validate() is skipped (it relies on ASSERT and a constexpr flag that disables it outside of debug builds). The malformed SPIR-V is handed off to the Vulkan driver.
  10. GPU driver compilers generally trust the validation phase of the front-end toolchain. Interpreting an A00B object as an A object in memory can lead to out-of-bounds memory accesses or memory corruption within the highly privileged GPU process.

Suggested Fix

Update TType::buildMangledName() in src/compiler/translator/Types.cpp to include a unique delimiter (such as - or ;) between the struct name and the field list.

For example:

mangledName += mStructure->name().data();
mangledName += '-'; // Add delimiter
mangledName += mStructure->mangledFieldList();

This ensures that structurally distinct structs will always have unique mangled names.

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.

View on issue tracker