Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in ANGLE
DescriptionInappropriate implementation in ANGLE
ComponentANGLE
Bug ClassLogic Error
Tracker484756087
Fix commitab41985470ba (angle/angle) +152/-9
CISA KEVNot listed
CreditedJason Villaluna
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
switch
src/tests/gl_tests/GLSLTest.cpp
modified

Files Changed

  • src/compiler/translator/tree_ops/PruneNoOps.cpp
  • src/tests/gl_tests/GLSLTest.cpp
From ab41985470ba0a74e80ecfd7ec21e4d9ada428da Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Thu, 07 May 2026 15:25:01 -0400
Subject: [PATCH] AST: Fix variable declaration in dead code in `case`

Previously fixed for IR here:

https://chromium-review.git.corp.google.com/c/angle/angle/+/7589940

A similar transformation is applied to the AST transformation.

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

diff --git a/src/compiler/translator/tree_ops/PruneNoOps.cpp b/src/compiler/translator/tree_ops/PruneNoOps.cpp
index 367dbf3..6e832c1 100644
--- a/src/compiler/translator/tree_ops/PruneNoOps.cpp
+++ b/src/compiler/translator/tree_ops/PruneNoOps.cpp
@@ -109,18 +109,22 @@
   private:
     PruneNoOpsTraverser(TSymbolTable *symbolTable);
     bool visitDeclaration(Visit, TIntermDeclaration *node) override;
+    bool visitSwitch(Visit visit, TIntermSwitch *node) override;
     bool visitBlock(Visit visit, TIntermBlock *node) override;
     bool visitLoop(Visit visit, TIntermLoop *loop) override;
     bool visitBranch(Visit visit, TIntermBranch *node) override;
     TIntermTyped *pruneNoOpCommaExpressions(TIntermTyped *statement);
 
     bool mIsBranchVisited = false;
+
+    TVector<TVector<const TVariable *>> mSwitchPrunedDeclarationsStack;
 };
 
 bool PruneNoOpsTraverser::apply(TCompiler *compiler, TIntermBlock *root, TSymbolTable *symbolTable)
 {
     PruneNoOpsTraverser prune(symbolTable);
     root->traverse(&prune);
+    ASSERT(prune.mSwitchPrunedDeclarationsStack.empty());
     return prune.updateTree(compiler, root);
 }
 
@@ -196,6 +200,35 @@
     return false;
 }
 
+bool PruneNoOpsTraverser::visitSwitch(Visit visit, TIntermSwitch *node)
+{
+    node->getInit()->traverse(this);
+
+    // Before visiting the block, push a list of variable declarations that were pruned because they
+    // were declared directly in the body of the switch but after a branch.  The variable
+    // declarations are instead prepended to the switch, in case they are used in a later case which
+    // references them in live code.
+    mSwitchPrunedDeclarationsStack.push_back({});
+
+    node->getStatementList()->traverse(this);
+    if (!mSwitchPrunedDeclarationsStack.back().empty())
+    {
+        TIntermSequence replacement;
+        for (const TVariable *toDeclare : mSwitchPrunedDeclarationsStack.back())
+        {
+            TIntermDeclaration *decl = new TIntermDeclaration();
+            decl->appendDeclarator(new TIntermSymbol(toDeclare));
+            replacement.push_back(decl);
+        }
+        replacement.push_back(node);
+        mMultiReplacements.emplace_back(getParentNode()->getAsBlock(), node,
+                                        std::move(replacement));
+    }
+
+    mSwitchPrunedDeclarationsStack.pop_back();
+    return false;
+}
+
 bool PruneNoOpsTraverser::visitBlock(Visit visit, TIntermBlock *node)
 {
     ASSERT(visit == PreVisit);
@@ -218,6 +251,42 @@
         // If a branch is visited, prune the statement.  If the statement is a no-op, also prune it.
         if (mIsBranchVisited || IsNoOp(statement))
         {
+            // If this is a declaration, remember the variable.  The declaration is moved to before
+            // the switch, to support cases like:
+            //
+            //    switch(u0){
+            //        case 0:
+            //            break;
+            //            vec4 d = vec4(0);
+            //        default:
+            //            d.a = .0;
+            //    }
+            if (mIsBranchVisited && getParentNode()->getAsSwitchNode() != nullptr)
+            {
+                TIntermDeclaration *decl = statement->getAsDeclarationNode();
+                if (decl != nullptr)
+                {
+                    for (TIntermNode *declarator : *decl->getSequence())
+                    {
+                        TIntermSymbol *symbol        = declarator->getAsSymbolNode();
+                        const TVariable *declaredVar = nullptr;
+                        if (symbol != nullptr)
+                        {
+                            declaredVar = &symbol->variable();
+                        }
+                        else
+                        {
+                            TIntermBinary *initNode = declarator->getAsBinaryNode();
+                            ASSERT(initNode && initNode->getOp() == EOpInitialize);
+                            ASSERT(initNode->getLeft()->getAsSymbolNode());
+                            declaredVar = &initNode->getLeft()->getAsSymbolNode()->variable();
+                        }
+
+                        mSwitchPrunedDeclarationsStack.back().push_back(declaredVar);
+                    }
+                }
+            }
+
             continue;
         }
 
diff --git a/src/tests/gl_tests/GLSLTest.cpp b/src/tests/gl_tests/GLSLTest.cpp
index f0d3e8f..ea13530 100644
--- a/src/tests/gl_tests/GLSLTest.cpp
+++ b/src/tests/gl_tests/GLSLTest.cpp
@@ -22759,9 +22759,6 @@
 // Test variable declaration in switch case after dead code, but used in the next case.
 TEST_P(GLSLTest_ES3, VariableDeclaredInCaseDeadCode1)
 {
-    // The PruneNoOps AST pass does not retain the variable in dead code.
-    ANGLE_SKIP_TEST_IF(!getEGLWindow()->isFeatureEnabled(Feature::UseIr));
-
     constexpr char kFS[] = R"(#version 300 es
 precision mediump float;
 uniform int u0;
@@ -22787,9 +22784,6 @@
 // The dead code is after divergence and reconvergence.
 TEST_P(GLSLTest_ES3, VariableDeclaredInCaseDeadCode2)
 {
-    // The PruneNoOps AST pass does not retain the variable in dead code.
-    ANGLE_SKIP_TEST_IF(!getEGLWindow()->isFeatureEnabled(Feature::UseIr));
-
     constexpr char kFS[] = R"(#version 300 es
 precision mediump float;
 uniform int u0;
@@ -22821,9 +22815,6 @@
 // The dead code itself has divergence and reconvergence.
 TEST_P(GLSLTest_ES3, VariableDeclaredInCaseDeadCode3)
 {
-    // The PruneNoOps AST pass does not retain the variable in dead code.
-    ANGLE_SKIP_TEST_IF(!getEGLWindow()->isFeatureEnabled(Feature::UseIr));
-
     constexpr char kFS[] = R"(#version 300 es
 precision mediump float;
 uniform int u0;
@@ -22919,6 +22910,89 @@
     EXPECT_GL_NO_ERROR();
 }
 
+// Test variable declaration in switch case after dead code, but used in the next case.
+TEST_P(GLSLTest_ES3, VariableDeclaredInCaseDeadCode6)
+{
+    constexpr char kFS[] = R"(#version 300 es
+precision mediump float;
+uniform int u0;
+out vec4 fragOut;
+void main(){
+    vec4 d = vec4(1);
+    switch(u0){
+        case 0:
+            break;
+            vec4 d = vec4(0);
+        default:
+            d.a = .0;
+    }
+    fragOut = vec4(1, 0, 0, 1);
+})";
+    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
+    glUseProgram(program);
+    drawQuad(program, essl3_shaders::PositionAttrib(), 0.0);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+    EXPECT_GL_NO_ERROR();
+}
+
+// Test variable declaration in switch case after dead code, but used in the next case.
+TEST_P(GLSLTest_ES3, VariableDeclaredInCaseDeadCode7)
+{
+    constexpr char kFS[] = R"(#version 300 es
+precision mediump float;
+uniform int u0;
+out vec4 fragOut;
+void main(){
+    vec4 d = vec4(1);
+    switch(u0){
+        case 0:
+            break;
+            vec4 d = vec4(0), d1 = vec4(0);
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 f0d3e8f..ea13530 100644
--- a/src/tests/gl_tests/GLSLTest.cpp
+++ b/src/tests/gl_tests/GLSLTest.cpp
@@ -22759,9 +22759,6 @@
 // Test variable declaration in switch case after dead code, but used in the next case.
 TEST_P(GLSLTest_ES3, VariableDeclaredInCaseDeadCode1)
 {
-    // The PruneNoOps AST pass does not retain the variable in dead code.
-    ANGLE_SKIP_TEST_IF(!getEGLWindow()->isFeatureEnabled(Feature::UseIr));
-
     constexpr char kFS[] = R"(#version 300 es
 precision mediump float;
 uniform int u0;
@@ -22787,9 +22784,6 @@
 // The dead code is after divergence and reconvergence.
 TEST_P(GLSLTest_ES3, VariableDeclaredInCaseDeadCode2)
 {
-    // The PruneNoOps AST pass does not retain the variable in dead code.
-    ANGLE_SKIP_TEST_IF(!getEGLWindow()->isFeatureEnabled(Feature::UseIr));
-
     constexpr char kFS[] = R"(#version 300 es
 precision mediump float;
 uniform int u0;
@@ -22821,9 +22815,6 @@
 // The dead code itself has divergence and reconvergence.
 TEST_P(GLSLTest_ES3, VariableDeclaredInCaseDeadCode3)
 {
-    // The PruneNoOps AST pass does not retain the variable in dead code.
-    ANGLE_SKIP_TEST_IF(!getEGLWindow()->isFeatureEnabled(Feature::UseIr));
-
     constexpr char kFS[] = R"(#version 300 es
 precision mediump float;
 uniform int u0;
@@ -22919,6 +22910,89 @@
     EXPECT_GL_NO_ERROR();
 }
 
+// Test variable declaration in switch case after dead code, but used in the next case.
+TEST_P(GLSLTest_ES3, VariableDeclaredInCaseDeadCode6)
+{
+    constexpr char kFS[] = R"(#version 300 es
+precision mediump float;
+uniform int u0;
+out vec4 fragOut;
+void main(){
+    vec4 d = vec4(1);
+    switch(u0){
+        case 0:
+            break;
+            vec4 d = vec4(0);
+        default:
+            d.a = .0;
+    }
+    fragOut = vec4(1, 0, 0, 1);
+})";
+    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
+    glUseProgram(program);
+    drawQuad(program, essl3_shaders::PositionAttrib(), 0.0);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+    EXPECT_GL_NO_ERROR();
+}
+
+// Test variable declaration in switch case after dead code, but used in the next case.
+TEST_P(GLSLTest_ES3, VariableDeclaredInCaseDeadCode7)
+{
+    constexpr char kFS[] = R"(#version 300 es
+precision mediump float;
+uniform int u0;
+out vec4 fragOut;
+void main(){
+    vec4 d = vec4(1);
+    switch(u0){
+        case 0:
+            break;
+            vec4 d = vec4(0), d1 = vec4(0);
+            vec4 d2 = vec4(0);
+        default:
+            d.a = .0;
+            d1.a = .0;
+            d2.a = .0;
+    }
+    fragOut = vec4(1, 0, 0, 1);
+})";
+    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
+    glUseProgram(program);
+    drawQuad(program, essl3_shaders::PositionAttrib(), 0.0);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+    EXPECT_GL_NO_ERROR();
+}
+
+// Test variable declaration in switch case after dead code, but used in the next case.
+TEST_P(GLSLTest_ES3, VariableDeclaredInCaseDeadCode8)
+{
+    constexpr char kFS[] = R"(#version 300 es
+precision mediump float;
+uniform int u0;
+out vec4 fragOut;
+void main(){
+    switch(u0){
+        case 0:
+            break;
+            switch(u0){
+                case 0:
+                    break;
+                    vec4 d = vec4(0);
+                default:
+                    d.a = .0;
+            }
+        default:
+            break;
+    }
+    fragOut = vec4(1, 0, 0, 1);
+})";
+    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
+    glUseProgram(program);
+    drawQuad(program, essl3_shaders::PositionAttrib(), 0.0);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+    EXPECT_GL_NO_ERROR();
+}
+
 // Test that a for loop initializer can be a variable with a struct declaration.
 TEST_P(GLSLTest_ES31, StructDeclarationInForLoop)
 {
Loading diff…

Original Bug Report

reported by ja...@gmail.com

A crafted GLSL ES 3.0 shader triggers an AST transformation bug in ANGLE's shared compiler frontend that produces an internally inconsistent AST (dangling variable references).


Report description

A crafted GLSL ES 3.0 shader triggers an AST transformation bug in ANGLE’s shared compiler frontend that produces an internally inconsistent AST (dangling variable references).


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules

Which URL (or repository) have you found the vulnerability in?

https://chromium.googlesource.com/angle/angle/


The problem

Please describe the technical details of the vulnerability

Summary

A crafted GLSL ES 3.0 shader triggers an AST transformation bug in ANGLE’s shared compiler frontend that produces an internally inconsistent AST (dangling variable references). On the D3D (Windows) and Metal (macOS) backends, ANGLE’s validateAST safety check is explicitly disabled (false), so the corrupted AST passes through uncaught. ANGLE reports successful compilation and emits malformed output code containing undeclared variable references, which is then handed directly to the platform GPU driver shader compiler (FXC/DXC or Metal shader compiler). This violates ANGLE’s role as the security boundary between untrusted WebGL content and GPU drivers.

Affected Platforms

Platform Backend validateAST Impact
Windows D3D9/D3D11 false Malformed HLSL reaches FXC/DXC
macOS Metal false Malformed MSL reaches Metal compiler
Linux GL true Caught by validation (no bypass)
Android GL true Caught by validation (no bypass)

Reproducer

GLSL ES 3.0 shader (262 bytes):

#version 300 es
precision mediump float;
int u0mode; out vec4 fragOut;
void main(){
  vec4 c = vec4(0);
  for(int i=0;i<0;i++){ if(0==u0mode){break;} }
  switch(u0mode){ case 0: case 1: ; }
  switch(u0mode){
    case 0: break;
      vec4 c = vec4(0);
      for(int i=0;i<0;i++) fragOut.g=.0;
    default: c.a=.0;
  }
}

Trigger: vec4 c is declared at function scope, then re-declared (shadowed) inside a switch/case block. Multiple switch statements with empty cases cause a tree transformation pass to drop the outer c declaration while retaining references to it, creating a dangling symbol reference in the AST.

Root Cause

  1. Shader declares vec4 c at function scope
  2. Inside a switch block, vec4 c is re-declared (shadowing)
  3. A tree transformation pass (SeparateDeclarations, PruneEmptyCases, or PruneNoOps) processes the repeated switch blocks
  4. The transformation drops the outer c declaration but leaves references to it in the default: case — AST now contains symbol ID 3006 referencing a removed declaration (symbol ID 3004)
  5. On GL/Vulkan backends, validateAST catches this (ERROR: 'c' : Found reference to undeclared or inconsistently transformed variable). On D3D/Metal, validateAST is false — the corruption passes through silently

Location: src/compiler/translator/Compiler.cpp:774 (validation gate) D3D disable: src/libANGLE/renderer/d3d/ShaderD3D.cpp:338 (validateAST = false) Metal disable: src/libANGLE/renderer/metal/ShaderMtl.mm:63 (validateAST = false)

Malformed Output Evidence

ANGLE produces the following output while reporting compilation success:

HLSL (Windows D3D path):

switch (_u0mode) {
  case (0):
    break;
  default:
    (_c3006.w = 0.0);    // UNDECLARED - variable declaration was dropped
    break;
}

ESSL / GLSL 4.50 output:

switch (_uu0mode) {
  case (0):
    break;
  default:
    (_uc.w = 0.0);       // UNDECLARED - same corruption
}

In all output formats, ANGLE returns compilation success while emitting code that references a variable (_c3006 / _uc) that is never declared.

Impact analysis

Any web page can exploit this without user interaction. By serving a crafted WebGL shader, an attacker bypasses ANGLE’s shader validation boundary on Windows (D3D) and macOS (Metal), where the validateAST safety check is disabled. ANGLE reports successful compilation and emits malformed output code containing undeclared variable references, which is then passed directly to the platform GPU driver shader compiler (FXC/DXC on Windows, Metal compiler on macOS).

The attacker gains the ability to deliver structurally invalid shader code to GPU driver compilers that assume their input has been validated. The GPU process runs with broader privileges than the sandboxed renderer process. I believe the GPU driver shader compiler represents a separate security boundary, any vulnerability in how it handles malformed input would be a distinct bug requiring its own fix. ANGLE’s role is to ensure malformed shaders never reach that boundary in the first place.


The cause

What version of Chrome have you found the security issue in?

Tested on Chrome 145.0.7632.76 (Official Build) (64-bit) (cohort: Stable) (Windows)

No, it is not related to a crash.

Choose the type of vulnerability

Exploit Mitigation Bypass

How would you like to be publicly acknowledged for your report?

Jason Villaluna

View on issue tracker