Use structural reachability in CFG checks (#4849)

Fixes https://crbug.com/oss-fuzz48578

* Adds structural reachability to basic blocks
  * calculated in same manner as reachability, but using structural
    successors
* Change structured CFG validation to use structural reachability
  instead of reachability
* Fix some invalid reducer cases

diff --git a/source/val/basic_block.cpp b/source/val/basic_block.cpp
index a489ae0..da05db3 100644
--- a/source/val/basic_block.cpp
+++ b/source/val/basic_block.cpp
@@ -30,6 +30,7 @@
       successors_(),
       type_(0),
       reachable_(false),
+      structurally_reachable_(false),
       label_(nullptr),
       terminator_(nullptr) {}
 
diff --git a/source/val/basic_block.h b/source/val/basic_block.h
index f778aa0..be5657e 100644
--- a/source/val/basic_block.h
+++ b/source/val/basic_block.h
@@ -84,9 +84,12 @@
     return &structural_successors_;
   }
 
-  /// Returns true if the block is structurally reachable in the CFG.
+  /// Returns true if the block is reachable in the CFG.
   bool reachable() const { return reachable_; }
 
+  /// Returns true if the block is structurally reachable in the CFG.
+  bool structurally_reachable() const { return structurally_reachable_; }
+
   /// Returns true if BasicBlock is of the given type
   bool is_type(BlockType type) const {
     if (type == kBlockTypeUndefined) return type_.none();
@@ -96,6 +99,11 @@
   /// Sets the reachability of the basic block in the CFG
   void set_reachable(bool reachability) { reachable_ = reachability; }
 
+  /// Sets the structural reachability of the basic block in the CFG
+  void set_structurally_reachable(bool reachability) {
+    structurally_reachable_ = reachability;
+  }
+
   /// Sets the type of the BasicBlock
   void set_type(BlockType type) {
     if (type == kBlockTypeUndefined)
@@ -283,6 +291,9 @@
   /// True if the block is reachable in the CFG
   bool reachable_;
 
+  /// True if the block is structurally reachable in the CFG
+  bool structurally_reachable_;
+
   /// label of this block, if any.
   const Instruction* label_;
 
diff --git a/source/val/validate_cfg.cpp b/source/val/validate_cfg.cpp
index 506b5e5..d4ca20e 100644
--- a/source/val/validate_cfg.cpp
+++ b/source/val/validate_cfg.cpp
@@ -466,7 +466,7 @@
   std::vector<BasicBlock*> stack;
   stack.push_back(target_block);
   std::unordered_set<const BasicBlock*> visited;
-  bool target_reachable = target_block->reachable();
+  bool target_reachable = target_block->structurally_reachable();
   int target_depth = function->GetBlockDepth(target_block);
   while (!stack.empty()) {
     auto block = stack.back();
@@ -476,7 +476,7 @@
 
     if (!visited.insert(block).second) continue;
 
-    if (target_reachable && block->reachable() &&
+    if (target_reachable && block->structurally_reachable() &&
         target_block->structurally_dominates(*block)) {
       // Still in the case construct.
       for (auto successor : *block->successors()) {
@@ -549,7 +549,8 @@
     if (seen_iter == seen_to_fall_through.end()) {
       const auto target_block = function->GetBlock(target).first;
       // OpSwitch must dominate all its case constructs.
-      if (header->reachable() && target_block->reachable() &&
+      if (header->structurally_reachable() &&
+          target_block->structurally_reachable() &&
           !header->structurally_dominates(*target_block)) {
         return _.diag(SPV_ERROR_INVALID_CFG, header->label())
                << "Selection header " << _.getIdName(header->id())
@@ -653,7 +654,7 @@
     }
 
     // Skip unreachable blocks.
-    if (!block->reachable()) continue;
+    if (!block->structurally_reachable()) continue;
 
     if (terminator->opcode() == SpvOpBranchConditional) {
       const auto true_label = terminator->GetOperandAs<uint32_t>(1);
@@ -708,7 +709,7 @@
 
   // Check the loop headers have exactly one back-edge branching to it
   for (BasicBlock* loop_header : function->ordered_blocks()) {
-    if (!loop_header->reachable()) continue;
+    if (!loop_header->structurally_reachable()) continue;
     if (!loop_header->is_type(kBlockTypeLoop)) continue;
     auto loop_header_id = loop_header->id();
     auto num_latch_blocks = loop_latch_blocks[loop_header_id].size();
@@ -723,7 +724,7 @@
   // Check construct rules
   for (const Construct& construct : function->constructs()) {
     auto header = construct.entry_block();
-    if (!header->reachable()) continue;
+    if (!header->structurally_reachable()) continue;
     auto merge = construct.exit_block();
 
     if (!merge) {
@@ -784,7 +785,7 @@
       // Check that for all non-header blocks, all predecessors are within this
       // construct.
       for (auto pred : *block->predecessors()) {
-        if (pred->reachable() && !construct_blocks.count(pred)) {
+        if (pred->structurally_reachable() && !construct_blocks.count(pred)) {
           return _.diag(SPV_ERROR_INVALID_CFG, _.FindDef(pred->id()))
                  << "block <ID> " << pred->id() << " branches to the "
                  << construct_name << " construct, but not to the "
@@ -800,7 +801,7 @@
             merge_inst.opcode() == SpvOpLoopMerge) {
           uint32_t merge_id = merge_inst.GetOperandAs<uint32_t>(0);
           auto merge_block = function->GetBlock(merge_id).first;
-          if (merge_block->reachable() &&
+          if (merge_block->structurally_reachable() &&
               !construct_blocks.count(merge_block)) {
             return _.diag(SPV_ERROR_INVALID_CFG, _.FindDef(block->id()))
                    << "Header block " << _.getIdName(block->id())
@@ -1113,6 +1114,26 @@
       }
     }
   }
+
+  // Repeat for structural reachability.
+  for (auto& f : _.functions()) {
+    std::vector<BasicBlock*> stack;
+    auto entry = f.first_block();
+    // Skip function declarations.
+    if (entry) stack.push_back(entry);
+
+    while (!stack.empty()) {
+      auto block = stack.back();
+      stack.pop_back();
+
+      if (block->structurally_reachable()) continue;
+
+      block->set_structurally_reachable(true);
+      for (auto succ : *block->structural_successors()) {
+        stack.push_back(succ);
+      }
+    }
+  }
 }
 
 spv_result_t ControlFlowPass(ValidationState_t& _, const Instruction* inst) {
diff --git a/test/reduce/structured_loop_to_selection_test.cpp b/test/reduce/structured_loop_to_selection_test.cpp
index 0cfcfdf..d203f3e 100644
--- a/test/reduce/structured_loop_to_selection_test.cpp
+++ b/test/reduce/structured_loop_to_selection_test.cpp
@@ -2957,7 +2957,7 @@
                OpLoopMerge %12 %13 None
                OpBranch %12
          %13 = OpLabel
-               OpBranchConditional %6 %9 %11
+               OpBranch %11
          %12 = OpLabel
                OpBranch %10
          %10 = OpLabel
@@ -2999,7 +2999,7 @@
                OpLoopMerge %12 %13 None
                OpBranch %12
          %13 = OpLabel
-               OpBranchConditional %6 %9 %11
+               OpBranch %11
          %12 = OpLabel
                OpBranch %9
          %10 = OpLabel
@@ -3036,7 +3036,7 @@
                OpSelectionMerge %12 None
                OpBranchConditional %6 %12 %12
          %13 = OpLabel
-               OpBranchConditional %6 %9 %11
+               OpBranch %11
          %12 = OpLabel
                OpBranch %9
          %10 = OpLabel
@@ -3050,8 +3050,7 @@
 
 TEST(StructuredLoopToSelectionReductionPassTest,
      UnreachableInnerLoopContinueBranchingToOuterLoopMerge2) {
-  // In this test, the branch to the outer loop merge from the inner loop's
-  // continue is part of a structured selection.
+  // In this test, the unreachable continue is composed of multiple blocks.
   std::string shader = R"(
                OpCapability Shader
           %1 = OpExtInstImport "GLSL.std.450"
@@ -3073,8 +3072,7 @@
                OpLoopMerge %12 %13 None
                OpBranch %12
          %13 = OpLabel
-               OpSelectionMerge %14 None
-               OpBranchConditional %6 %9 %14
+               OpBranch %14
          %14 = OpLabel
                OpBranch %11
          %12 = OpLabel
@@ -3118,8 +3116,7 @@
                OpLoopMerge %12 %13 None
                OpBranch %12
          %13 = OpLabel
-               OpSelectionMerge %14 None
-               OpBranchConditional %6 %9 %14
+               OpBranch %14
          %14 = OpLabel
                OpBranch %11
          %12 = OpLabel
@@ -3158,8 +3155,7 @@
                OpSelectionMerge %12 None
                OpBranchConditional %6 %12 %12
          %13 = OpLabel
-               OpSelectionMerge %14 None
-               OpBranchConditional %6 %9 %14
+               OpBranch %14
          %14 = OpLabel
                OpBranch %11
          %12 = OpLabel
diff --git a/test/val/val_cfg_test.cpp b/test/val/val_cfg_test.cpp
index 2423b1e..ede51a9 100644
--- a/test/val/val_cfg_test.cpp
+++ b/test/val/val_cfg_test.cpp
@@ -4555,6 +4555,36 @@
                         "must be different labels"));
 }
 
+TEST_F(ValidateCFG, BadBackEdgeUnreachableContinue) {
+  const std::string text = R"(
+OpCapability Shader
+OpCapability Linkage
+OpMemoryModel Logical GLSL450
+%1 = OpTypeVoid
+%2 = OpTypeFunction %1
+%3 = OpFunction %1 None %2
+%4 = OpLabel
+OpBranch %5
+%5 = OpLabel
+OpLoopMerge %6 %7 None
+OpBranch %8
+%8 = OpLabel
+OpBranch %5
+%7 = OpLabel
+OpUnreachable
+%6 = OpLabel
+OpUnreachable
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(text);
+  EXPECT_EQ(SPV_ERROR_INVALID_CFG, ValidateInstructions());
+  EXPECT_THAT(
+      getDiagnosticString(),
+      HasSubstr("The continue construct with the continue target 7[%7] "
+                "does not structurally dominate the back-edge block 8[%8]"));
+}
+
 }  // namespace
 }  // namespace val
 }  // namespace spvtools