High chrome UAF 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebGL
DescriptionUse after free in WebGL
ComponentWebGL
Sub-componentTranslator
Bug ClassUAF
Tracker548066294
Fix commitdfeede58b618 (angle/angle) +1763/-228
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Files Changed

  • src/compiler/translator/tree_ops/FoldConstantSwitch.h
  • src/compiler/translator/tree_ops/FoldConstantSwitch.cpp
  • src/compiler.gni
  • src/tests/compiler_tests/PruneNoOps_test.cpp
  • src/compiler/translator/Compiler.cpp
  • src/tests/angle_end2end_tests_expectations.txt
  • src/tests/gl_tests/GLSLTest.cpp
  • src/compiler/translator/tree_util/IntermTraverse.h
  • src/tests/deqp_support/deqp_gles3_test_expectations.txt
  • src/compiler/translator/ir/src/builder.rs
  • src/compiler/translator/ir/src/output/legacy.rs
  • src/tests/gl_tests/GLSLOutputTest.cpp
  • src/compiler/translator/tree_ops/PruneNoOps.cpp
  • src/tests/angle_unittests.gni
From dfeede58b6183e8964ae4b6d9612fe1d579403e3 Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <[email protected]>
Date: Thu, 24 Sep 2026 14:06:31 -0400
Subject: [PATCH] Translator: Fold constant switch statements

When a switch statement has a constant selector, previously there was no
dead-code elimination done for it in AST.  In IR, cases that would not
be hit were eliminated, but the switch remained with its single matching
case.  Primarily, this was done to avoid having to deal with break and
continue statements nested in the switch.

However, due to driver bugs, a pass is added to completely remove switch
statements that have a constant selector.  Where there are nested break
and continue statements, the switch statement is replaced with a
`while (true) { ... break; }` to allow breaking out of it.  To support
continue statements, a variable is introduced to be set instead of
continue, and the loop is followed by an `if (flag) continue;`.

Some tests are credit zork@

Bug: chromium:548066294
Change-Id: I8b8ad101c37029358c4d4f03282fa463b318c420
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8465623
Reviewed-by: Matthew Denton <[email protected]>
Reviewed-by: Yuxin Hu <[email protected]>
Commit-Queue: Shahbaz Youssefi <[email protected]>
---
diff --git a/src/compiler/translator/Compiler.cpp b/src/compiler/translator/Compiler.cpp
index 8b15540..adf87da 100644
--- a/src/compiler/translator/Compiler.cpp
+++ b/src/compiler/translator/Compiler.cpp
@@ -32,6 +32,7 @@
 #include "compiler/translator/tree_ops/EmulateGLFragColorBroadcast.h"
 #include "compiler/translator/tree_ops/EmulateMultiDrawShaderBuiltins.h"
 #include "compiler/translator/tree_ops/ExpandFragmentOutputsToVec4.h"
+#include "compiler/translator/tree_ops/FoldConstantSwitch.h"
 #include "compiler/translator/tree_ops/FoldExpressions.h"
 #include "compiler/translator/tree_ops/InitializeVariables.h"
 #include "compiler/translator/tree_ops/PruneEmptyCases.h"
@@ -789,8 +790,8 @@
         }
     }
 
-    // Fold expressions that could not be folded before validation that was done as a part of
-    // parsing.
+    // Fold expressions that could not be folded before validation or otherwise that was done as a
+    // part of parsing.
     if (!FoldExpressions(this, root, &mDiagnostics))
     {
         return false;
@@ -798,6 +799,13 @@
     // Folding should only be able to generate warnings.
     ASSERT(mDiagnostics.numErrors() == 0);
 
+    // Fold switch statements with constant expression.  Run after FoldExpressions because the
+    // switch selector may need folding.
+    if (!FoldConstantSwitch(this, root, &mSymbolTable))
+    {
+        return false;
+    }
+
     const bool hasAnyClipCullDistance =
         parseContext.isExtensionEnabled(TExtension::ANGLE_clip_cull_distance) ||
         parseContext.isExtensionEnabled(TExtension::EXT_clip_cull_distance) ||
diff --git a/src/compiler/translator/ir/src/builder.rs b/src/compiler/translator/ir/src/builder.rs
index d2825f2..d208f83 100644
--- a/src/compiler/translator/ir/src/builder.rs
+++ b/src/compiler/translator/ir/src/builder.rs
@@ -62,6 +62,44 @@
     }
 }
 
+// Information needed to constant-fold switch statements
+#[cfg_attr(debug_assertions, derive(Debug))]
+struct ConstantSwitchInfo {
+    // Whether the current target of `break` is a `switch` (possibly nested under another switch)
+    // with a constant expression.  In particular, this switch would have to specially handle
+    // `continue` (see comments below).
+    is_constant_switch: bool,
+    // If the current constant-switch has had a `continue` branch, a variable is declared before
+    // the switch and set to true at the point of `continue`, and the branch is changed to
+    // `break`.  An `if (flag) continue;` statement is added after the (replaced) switch.
+    continue_variable: Option<(VariableId, TypedId)>,
+}
+
+impl ConstantSwitchInfo {
+    fn new() -> ConstantSwitchInfo {
+        ConstantSwitchInfo { is_constant_switch: false, continue_variable: None }
+    }
+
+    fn get_continue_variable(&mut self, ir_meta: &mut IRMeta) -> TypedId {
+        if let Some(continue_variable) = self.continue_variable {
+            continue_variable.1
+        } else {
+            let variable = ir_meta.declare_variable(
+                Name::new_temp("continue_flag"),
+                TYPE_ID_BOOL,
+                Precision::NotApplicable,
+                false,
+                Decorations::new_none(),
+                None,
+                Some(CONSTANT_ID_FALSE),
+                VariableScope::Local,
+            );
+            self.continue_variable = Some(variable);
+            variable.1
+        }
+    }
+}
+
 // Helper to construct the CFG of a function.
 #[cfg_attr(debug_assertions, derive(Debug))]
 struct CFGBuilder {
@@ -75,16 +113,24 @@
     // The blocks of the CFG that is being built.  Once a function is complete, this stack
     // should be empty.
     interm_blocks: Vec<IntermediateBlock>,
+    // A stack of nested switch / loop info used for folding switch statements with a constant
+    // selector.
+    constant_switch_stack: Vec<ConstantSwitchInfo>,
 }
 
 impl CFGBuilder {
     fn new() -> CFGBuilder {
-        CFGBuilder { current_block: IntermediateBlock::new(), interm_blocks: Vec::new() }
+        CFGBuilder {
+            current_block: IntermediateBlock::new(),
+            interm_blocks: Vec::new(),
+            constant_switch_stack: Vec::new(),
+        }
     }
 
     fn clear(&mut self) {
         let _ = std::mem::replace(&mut self.current_block, IntermediateBlock::new());
         self.interm_blocks.clear();
+        self.constant_switch_stack.clear();
     }
 
     fn is_empty(&self) -> bool {
@@ -192,14 +238,18 @@
     // Add an instruction to the current block.  This may be a break, continue, return or discard,
     // in which case the block terminates early.  If another instruction is added after the block
     // is terminated, it is dropped as dead-code.
-    fn add_void_instruction(&mut self, op: OpCode) {
+    fn add_void_instruction(&mut self, ir_meta: &mut IRMeta, op: OpCode) {
         if !self.current_block.new_instructions_are_dead_code {
             if matches!(op, OpCode::Discard | OpCode::Return(_) | OpCode::Break | OpCode::Continue)
             {
                 self.current_block.new_instructions_are_dead_code = true;
             }
 
-            self.current_block.block.add_void_instruction(op);
+            // If `continue` is observed under a switch with a constant selector, it's replaced with
+            // a `break` after setting a flag (to `continue` after the `switch`).
+            if !matches!(op, OpCode::Continue) || !self.constant_switch_nested_continue(ir_meta) {
+                self.current_block.block.add_void_instruction(op);
+            }
         }
     }
     fn add_typed_instruction(&mut self, id: RegisterId) {
@@ -379,6 +429,8 @@
             self.current_block.block.terminate(OpCode::Loop);
         }
         self.push_block();
+
+        self.constant_switch_stack.push(ConstantSwitchInfo::new());
     }
 
     fn end_loop_condition_common(&mut self, condition: TypedId) {
@@ -426,6 +478,8 @@
             self.current_block.block.terminate(OpCode::Continue);
         }
 
+        self.constant_switch_stack.pop();
+
         // Retrieve and set the body block of the loop.  The header of the loop is already at the
         // top of the stack.  Prepare a merge block to continue what comes after the loop.
         //
@@ -482,6 +536,8 @@
             self.current_block.block.terminate(OpCode::DoLoop);
         }
         self.push_block();
+
+        self.constant_switch_stack.push(ConstantSwitchInfo::new());
     }
 
     fn begin_do_loop_condition(&mut self) {
@@ -504,6 +560,8 @@
     fn end_do_loop(&mut self, condition: TypedId) {
         self.end_loop_condition_common(condition);
 
+        self.constant_switch_stack.pop();
+
         // Prepare a merge block to continue what comes after the do-loop.  If the entire do-loop
         // was dead-code eliminated however, restore the previous (already terminated) block.
         if self.are_new_instructions_dead_code_in_parent_block() {
@@ -522,6 +580,15 @@
             self.current_block.block.terminate(OpCode::Switch(value, Vec::new()));
         }
         self.push_block();
+
+        let mut switch_info = ConstantSwitchInfo::new();
+        switch_info.is_constant_switch = value.id.is_constant()
+            || self
+                .constant_switch_stack
+                .last()
+                .map(|info| info.is_constant_switch)
+                .unwrap_or(false);
+        self.constant_switch_stack.push(switch_info);
     }
 
     fn is_empty_switch_block(block: &Block) -> bool {
@@ -624,10 +691,12 @@
         (true, None)
     }
 
-    fn end_switch(&mut self) {
+    fn end_switch(&mut self, ir_meta: &mut IRMeta) {
         // Terminate the last case, if not already.
         self.end_previous_case();
 
+        let constant_switch_info = self.constant_switch_stack.pop().unwrap();
+
         // The case blocks have already been processed.  The header of the switch is already at the
         // top of the stack.  Prepare a merge block to continue what comes after the switch.
         //
@@ -658,9 +727,12 @@
         // - The switch has a constant expression, but none of the cases match it: In this case, the
         //   switch can be removed as well.
         // - The switch has a constant expression with a matching case: In this case, the
-        //   non-matching cases (that also cannot be fallen-through) can be removed.  Note that
-        //   sometimes the entire switch can be replaced with the matching blocks, but it can get
-        //   tricky with break and continue in it.
+        //   non-matching cases (that also cannot be fallen-through) can be removed.  The blocks in
+        //   the matching cases are placed under an infinite loop, which is broken out of with the
+        //   `break` instructions. Note that `continue` instructions are already turned into `break`
+        //   instructions, in which case a "continue variable" is declared and needs to be added to
+        //   the parent block. After the block, an `if (continue_variable) continue` statement
+        //   should be added.
 
         let (has_matching_case, matching_block_index) =
             Self::switch_has_matching_case(&mut switch_block.block);
@@ -672,50 +744,145 @@
         } else {
             // If the switch expression is constant, remove the case blocks that cannot be executed
             if let Some(matching_block_index) = matching_block_index {
-                let case_blocks = std::mem::take(&mut switch_block.block.case_blocks);
+                self.constant_switch_prune(switch_block, matching_block_index)
+            } else {
+                // The switch block has to remain (whether constant and folded or not), push it back
+                // and make the current block a merge block.
+                self.interm_blocks.push(switch_block);
+            }
+            self.constant_switch_declare_continue_variable(ir_meta, constant_switch_info);
+            self.current_block.is_merge_block = true;
+        }
+    }
 
-                // Starting from the matching block, take blocks while their terminator is
-                // `Passthrough` and then the final block with a different terminator.  Note that
-                // take_while stops _after_ the first element that doesn't match the predicate,
-                // i.e. that element is lost.  Because of that, the first element after
-                // `Passthrough` is also matched.
-                let mut iter = case_blocks.into_iter().skip(matching_block_index);
-                let mut matching_case_block = iter.next().unwrap();
-                let mut fallthrough_chain_last_block =
-                    matching_case_block.get_merge_chain_last_block_mut();
+    // Handle `continue` nested under a switch with a constant selector.  If `false` is returned,
+    // the instruction was not handled.
+    fn constant_switch_nested_continue(&mut self, ir_meta: &mut IRMeta) -> bool {
+        if let Some(info) = self.constant_switch_stack.last_mut()
+            && info.is_constant_switch
+        {
+            let continue_variable = info.get_continue_variable(ir_meta);
+            self.current_block
+                .block
+                .add_void_instruction(OpCode::Store(continue_variable, TYPED_CONSTANT_ID_TRUE));
+            self.current_block.block.add_void_instruction(OpCode::Break);
+            return true;
+        }
+        return false;
+    }
 
-                for fallthrough_block in iter {
-                    // If the previous block was fallthrough, chain this block to it. Otherwise
-                    // this block and all after it are unreachable.
-                    if !matches!(
-                        fallthrough_chain_last_block.get_terminating_op(),
-                        &OpCode::Passthrough
-                    ) {
-                        break;
-                    }
+    fn constant_switch_extract_matching_blocks(
+        switch_block: &mut IntermediateBlock,
+        matching_block_index: usize,
+    ) -> Block {
+        let case_blocks = std::mem::take(&mut switch_block.block.case_blocks);
 
-                    fallthrough_chain_last_block.unterminate();
-                    fallthrough_chain_last_block.terminate(OpCode::NextBlock);
-                    fallthrough_chain_last_block.set_merge_block(fallthrough_block);
-                    fallthrough_chain_last_block =
-                        fallthrough_chain_last_block.get_merge_chain_last_block_mut();
-                }
+        // Starting from the matching block, take blocks while their terminator is
+        // `Passthrough` and then the final block with a different terminator.  Note that
+        // take_while stops _after_ the first element that doesn't match the predicate,
+        // i.e. that element is lost.  Because of that, the first element after
+        // `Passthrough` is also matched.
+        let mut iter = case_blocks.into_iter().skip(matching_block_index);
+        let mut matching_case_block = iter.next().unwrap();
+        let mut fallthrough_chain_last_block = matching_case_block.get_merge_chain_last_block_mut();
 
-                let (switch_expression, switch_cases) =
-                    switch_block.block.get_terminating_op().get_switch_expression_and_cases();
-                let matching_case = switch_cases[matching_block_index];
-                // Replace the switch with one that has only one target, that is the matching case.
-                switch_block.block.unterminate();
-                switch_block
-                    .block
-                    .terminate(OpCode::Switch(switch_expression, vec![matching_case]));
-                switch_block.block.case_blocks = vec![matching_case_block];
+        for fallthrough_block in iter {
+            // If the previous block was fallthrough, chain this block to it. Otherwise
+            // this block and all after it are unreachable.
+            if !matches!(fallthrough_chain_last_block.get_terminating_op(), &OpCode::Passthrough) {
+                break;
             }
 
-            // The switch has to remain (whether constant or not), push it back and make the
-            // current block a merge block.
-            self.interm_blocks.push(switch_block);
-            self.current_block.is_merge_block = true;
+            fallthrough_chain_last_block.unterminate();
+            fallthrough_chain_last_block.terminate(OpCode::NextBlock);
+            fallthrough_chain_last_block.set_merge_block(fallthrough_block);
+            fallthrough_chain_last_block =
+                fallthrough_chain_last_block.get_merge_chain_last_block_mut();
+        }
+
+        matching_case_block
+    }
+
+    fn constant_switch_prune(
+        &mut self,
+        mut switch_block: IntermediateBlock,
+        matching_block_index: usize,
+    ) {
+        let matching_case_block =
+            Self::constant_switch_extract_matching_blocks(&mut switch_block, matching_block_index);
+
+        // Build the infinite loop with the matching_case_block as its body.
+        let mut condition_block = Block::new();
+        condition_block.terminate(OpCode::LoopIf(TYPED_CONSTANT_ID_TRUE));
+
+        let mut loop_block = Block::new();
+        loop_block.set_loop_condition_block(condition_block);
+        loop_block.set_loop_body_block(matching_case_block);
+        loop_block.terminate(OpCode::Loop);
+
+        // Instead of issuing a Switch, jump to the loop block directly.
+        switch_block.block.unterminate();
+        switch_block.block.terminate(OpCode::NextBlock);
+
+        self.interm_blocks.push(switch_block);
+
+        self.interm_blocks.push(IntermediateBlock {
+            block: loop_block,
+            is_merge_block: true,
+            new_instructions_are_dead_code: false,
+        });
+    }
+
+    fn constant_switch_declare_continue_variable(
+        &mut self,
+        ir_meta: &mut IRMeta,
+        constant_switch_info: ConstantSwitchInfo,
+    ) {
+        // If there's a continue variable, declare it in the loop block.  Also add
+        // `if (continue_variable) continue;` after the block.  Note that if nested under a switch
+        // with constant selector, this `continue` should be replaced with `break` too.
+        if let Some(continue_variable) = constant_switch_info.continue_variable {
+            self.interm_blocks.last_mut().unwrap().block.variables.push(continue_variable.0);
+
+            let mut if_body = Block::new();
+
+            if let Some(parent_info) = self.constant_switch_stack.last_mut()
+                && parent_info.is_constant_switch
+            {
+                // Generate:
+                //
+                //     Store parent_continue_variable true
+                //     Break
+                let parent_continue_variable = parent_info.get_continue_variable(ir_meta);
+                if_body.add_void_instruction(OpCode::Store(
+                    parent_continue_variable,
+                    TYPED_CONSTANT_ID_TRUE,
+                ));
+                if_body.terminate(OpCode::Break);
+            } else {
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index b0be817..f8ade1b 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -662,6 +662,7 @@
 
 // D3D
 42264653 WIN D3D11 : GLSLTestLoops.*ContinueInSwitch/* = SKIP
+42264653 WIN D3D11 : GLSLTest_ES3.NestedSwitchContinueInEnclosingLoop3/* = SKIP
 // D3D does not support compressed textures where the base mip level is not a multiple of 4
 40644741 WIN D3D11 : DXT1CompressedTextureTest.NonBlockSizesMipLevels/* = SKIP
 42265582 WIN D3D11 : ClearTestRGB_ES3.*/* = SKIP
diff --git a/src/tests/angle_unittests.gni b/src/tests/angle_unittests.gni
index b83a422..4d52f09 100644
--- a/src/tests/angle_unittests.gni
+++ b/src/tests/angle_unittests.gni
@@ -104,7 +104,6 @@
   "compiler_tests/Parse_test.cpp",
   "compiler_tests/PruneEmptyCases_test.cpp",
   "compiler_tests/PruneEmptyDeclarations_test.cpp",
-  "compiler_tests/PruneNoOps_test.cpp",
   "compiler_tests/PrunePureLiteralStatements_test.cpp",
   "compiler_tests/PruneUnusedFunctions_test.cpp",
   "compiler_tests/QualificationOrderESSL31_test.cpp",
diff --git a/src/tests/compiler_tests/PruneNoOps_test.cpp b/src/tests/compiler_tests/PruneNoOps_test.cpp
deleted file mode 100644
index b60f17e..0000000
--- a/src/tests/compiler_tests/PruneNoOps_test.cpp
+++ /dev/null
@@ -1,101 +0,0 @@
-//
-// Copyright 2024 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.
-//
-// PruneNoOps_test.cpp:
-//   Tests for pruning no-op statements.
-//
-
-#include "GLSLANG/ShaderLang.h"
-#include "angle_gl.h"
-#include "gtest/gtest.h"
-#include "tests/test_utils/compiler_test.h"
-
-using namespace sh;
-
-namespace
-{
-
-class PruneNoOpsTest : public MatchOutputCodeTest
-{
-  public:
-    PruneNoOpsTest() : MatchOutputCodeTest(GL_FRAGMENT_SHADER, SH_GLSL_150_CORE_OUTPUT) {}
-};
-
-// Test that a switch statement with a constant expression without a matching case is pruned.
-TEST_F(PruneNoOpsTest, SwitchStatementWithConstantExpressionNoMatchingCase)
-{
-    const std::string shaderString = R"(#version 300 es
-precision mediump float;
-out vec4 color;
-
-void main(void)
-{
-    switch (10)
-    {
-        case 0:
-            color = vec4(0);
-            break;
-        case 1:
-            color = vec4(1);
-            break;
-    }
-})";
-    compile(shaderString);
-    ASSERT_TRUE(notFoundInCode("switch"));
-    ASSERT_TRUE(notFoundInCode("case"));
-}
-
-// Test that a switch statement with a constant expression with a default is not pruned.
-TEST_F(PruneNoOpsTest, SwitchStatementWithConstantExpressionWithDefault)
-{
-    const std::string shaderString = R"(#version 300 es
-precision mediump float;
-out vec4 color;
-
-void main(void)
-{
-    switch (10)
-    {
-        case 0:
-            color = vec4(0);
-            break;
-        case 1:
-            color = vec4(1);
-            break;
-        default:
-            color = vec4(0.5);
-            break;
-    }
-})";
-    compile(shaderString);
-    ASSERT_TRUE(foundInCode("switch"));
-    ASSERT_TRUE(foundInCode("case"));
-}
-
-// Test that a switch statement with a constant expression with a matching case is not pruned.
-TEST_F(PruneNoOpsTest, SwitchStatementWithConstantExpressionWithMatchingCase)
-{
-    const std::string shaderString = R"(#version 300 es
-precision mediump float;
-out vec4 color;
-
-void main(void)
-{
-    switch (10)
-    {
-        case 0:
-            color = vec4(0);
-            break;
-        case 10:
-            color = vec4(1);
-            break;
-    }
-})";
-    compile(shaderString);
-    ASSERT_TRUE(foundInCode("switch"));
-    ASSERT_TRUE(foundInCode("case"));
-}
-
-}  // namespace
diff --git a/src/tests/deqp_support/deqp_gles3_test_expectations.txt b/src/tests/deqp_support/deqp_gles3_test_expectations.txt
index 196e59f..ae34d6d 100644
--- a/src/tests/deqp_support/deqp_gles3_test_expectations.txt
+++ b/src/tests/deqp_support/deqp_gles3_test_expectations.txt
@@ -792,6 +792,7 @@
 // Translator IR failures
 // IR's use of `while` and `if (!condition) break` instead of `for(..)` makes the HLSL generator think the loops are discontinuous, and as a result it drops the derivative built-ins!
 349994211 IR D3D11 NVIDIA : dEQP-GLES3.functional.shaders.derivate.*.uniform_loop* = SKIP
+349994211 IR D3D11 NVIDIA : dEQP-GLES3.functional.shaders.derivate.*.static_switch* = SKIP
 // Untriaged failures with HLSL
 349994211 IR D3D11 NVIDIA : dEQP-GLES3.functional.shaders.switch.default_label_static_* = SKIP
 349994211 IR D3D11 NVIDIA : dEQP-GLES3.functional.shaders.switch.default_not_last_static_* = SKIP
diff --git a/src/tests/gl_tests/GLSLOutputTest.cpp b/src/tests/gl_tests/GLSLOutputTest.cpp
index eb22cd8..b75c7c5 100644
--- a/src/tests/gl_tests/GLSLOutputTest.cpp
+++ b/src/tests/gl_tests/GLSLOutputTest.cpp
@@ -688,6 +688,232 @@
     verifyIsInTranslation(GL_FRAGMENT_SHADER, "vec4(511.0, 511.0, 511.0, 511.0)));");
     verifyIsInTranslation(GL_FRAGMENT_SHADER, "vec4(599.0, 599.0, 599.0, 599.0)));");
 }
+
+// Test that a constant switch with matching case is folded away in the translated shader.
+TEST_P(GLSLOutputGLSLTest_ES3, BasicConstantSwitch)
+{
+    constexpr char kFS[] = R"(#version 300 es
+precision highp float;
+out vec4 color;
+void main()
+{
+    float f = 0.0;
+    switch (0)
+    {
+        case 156: f = 1234.0; break;
+        case 0:   f = 2.0; break;
+        default:  f = 3.0; break;
+    }
+    color = f == 2.0 ? vec4(0, 1, 0, 1) : vec4(1, 0, 0, 1);
+})";
+    compileShader(GL_FRAGMENT_SHADER, kFS);
+    verifyIsNotInTranslation(GL_FRAGMENT_SHADER, "switch");
+}
+
+// Test that a constant switch without matching case is completely pruned in the translated shader.
+TEST_P(GLSLOutputGLSLTest_ES3, BasicConstantSwitchPruned)
+{
+    constexpr char kFS[] = R"(#version 300 es
+precision highp float;
+out vec4 color;
+void main()
+{
+    float f = 0.0;
+    switch (10)
+    {
+        case 156: f = 1234.0; break;
+        case 0:   f = 567.0; break;
+    }
+    color = f == 0.0 ? vec4(0, 1, 0, 1) : vec4(1, 0, 0, 1);
+})";
+    compileShader(GL_FRAGMENT_SHADER, kFS);
+    verifyIsNotInTranslation(GL_FRAGMENT_SHADER, "switch");
+}
+
+// Test that nested constant switch statements (https://crbug.com/548066294) are folded away.
+TEST_P(GLSLOutputGLSLTest_ES3, NestedConstantSwitch)
+{
+    constexpr char kFS[] = R"(#version 300 es
+precision highp float;
+out vec4 color;
+void main()
+{
+    int result = 0;
+    switch (0)
+    {
+        case 0:
+            result += 1;
+            switch (1)
+            {
+                case 0:
+                    result += 10;
+                    break;
+                case 1:
+                    result += 20;
+                    switch (2)
+                    {
+                        case 2:
+                            result += 300;
+                            break;
+                        default:
+                            result += 400;
+                            break;
+                    }
+                    break;
+                default:
+                    result += 50;
+                    break;
+            }
+            break;
+        case 1:
+            result += 1000;
+            break;
+        default:
+            result += 2000;
+            break;
+    }
+    color = result == 321 ? vec4(0, 1, 0, 1) : vec4(1, 0, 0, 1);
+})";
+    compileShader(GL_FRAGMENT_SHADER, kFS);
+    verifyIsNotInTranslation(GL_FRAGMENT_SHADER, "switch");
+}
+
+// Test that fall-through across cases in a constant switch is folded away.
+TEST_P(GLSLOutputGLSLTest_ES3, FallThrough)
+{
+    constexpr char kFS[] = R"(#version 300 es
+precision highp float;
+out vec4 color;
+void main()
+{
+    float f = 0.0;
+    switch (1)
+    {
+        case 1: f += 5.0;
+        case 2: f += 2.0; break;
+        case 3: f += 4.0; break;
+    }
+    color = f == 7.0 ? vec4(0, 1, 0, 1) : vec4(1, 0, 0, 1);
+})";
+    compileShader(GL_FRAGMENT_SHADER, kFS);
+    verifyIsNotInTranslation(GL_FRAGMENT_SHADER, "switch");
+}
+
+// Test that conditional break inside folded switch statements emits a do-while loop wrapper.
+TEST_P(GLSLOutputGLSLTest_ES3, ConditionalBreak)
+{
+    constexpr char kFS[] = R"(#version 300 es
+precision highp float;
+uniform int u_zero;
+out vec4 color;
+void main()
+{
+    float f = 0.0;
+    switch (0)
+    {
+        case 0:
+            if (u_zero != 0) { break; }
+            f = 2.0;
+            break;
+        default:
+            f = 5.0;
+            break;
+    }
+    color = f == 2.0 ? vec4(0, 1, 0, 1) : vec4(1, 0, 0, 1);
+})";
+    compileShader(GL_FRAGMENT_SHADER, kFS);
+    verifyIsNotInTranslation(GL_FRAGMENT_SHADER, "switch");
+}
+
+// Test that continue targeting an enclosing loop is preserved when folding switch statements.
+TEST_P(GLSLOutputGLSLTest_ES3, ContinueInEnclosingLoop)
+{
+    constexpr char kFS[] = R"(#version 300 es
+precision highp float;
+uniform int u_zero;
+out vec4 color;
+void main()
+{
+    int count = 0;
+    for (int i = 0; i < 4; ++i)
+    {
... (truncated)
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.