CVE-2026-13859
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
switchsrc/tests/gl_tests/GLSLTest.cpp |
modified |
Files Changed
src/compiler/translator/tree_ops/PruneNoOps.cppsrc/tests/gl_tests/GLSLTest.cpp
Patch
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);
Regression Test / PoC
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)
{
Original Bug Report
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
- Shader declares
vec4 cat function scope - Inside a
switchblock,vec4 cis re-declared (shadowing) - A tree transformation pass (
SeparateDeclarations,PruneEmptyCases, orPruneNoOps) processes the repeated switch blocks - The transformation drops the outer
cdeclaration but leaves references to it in thedefault:case — AST now contains symbol ID 3006 referencing a removed declaration (symbol ID 3004) - On GL/Vulkan backends,
validateASTcatches this (ERROR: 'c' : Found reference to undeclared or inconsistently transformed variable). On D3D/Metal,validateASTisfalse— 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)
Is the security issue related to a crash?
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