https://github.com/steakhal created 
https://github.com/llvm/llvm-project/pull/212138

A GCC statement-expression containing control flow finishes the current CFG 
block and nulls out the builder's `Block`. Several places assumed `Block` 
survives `addStmt()`, so clang crashed on:
```c++
  struct S { explicit operator bool(); };
  void foo(int n) {
    while (S s = ({ while (S t{}) {} S{}; }))
      --n;
  }
```
`VisitWhileStmt` and `VisitForStmt` asserted `Block == EntryConditionBlock` 
after adding the condition variable's initializer. Both already stored the 
result of `addStmt(Init)` in `EntryConditionBlock` and used it correctly 
afterwards, so only the assert was wrong; thread the returned block instead, as 
`VisitSwitchStmt` already does.

`VisitDeclSubExpr` tracked `LastBlock` precisely because `Block` can be nulled 
out here, but then passed the stale `Block` to 
`maybeAddScopeBeginForVarDecl()`, which dereferences it. With `cfg-scopes=true` 
that is a null dereference rather than a failed assert, so it crashed release 
builds too, affecting `if` and `switch` condition variables and C++17 
init-statements.

Document the invariant on `addStmt()` and point the callers at it.

Also tighten the adjacent `if (Block && badCFG)` guards to `if (badCFG)`, so a 
`badCFG` set while traversing the initializer is not missed once `Block` is 
null. This is hardening, not part of the crash fix.

Not analyzer-only: Sema's `AnalysisBasedWarnings` builds a CFG, so this also 
reproduces on a plain compile with `-fsyntax-only -Wunreachable-code`.

Fixes #211976
Assisted-by: claude

From 7542cabf5175985e39e86ad1d9836417ad043c48 Mon Sep 17 00:00:00 2001
From: Balazs Benics <[email protected]>
Date: Sun, 26 Jul 2026 15:42:44 +0100
Subject: [PATCH] [clang][CFG] Fix crash on statement-expression in condition
 variable

A GCC statement-expression containing control flow finishes the current CFG
block and nulls out the builder's `Block`. Several places assumed `Block`
survives `addStmt()`, so clang crashed on:

  struct S { explicit operator bool(); };
  void foo(int n) {
    while (S s = ({ while (S t{}) {} S{}; }))
      --n;
  }

`VisitWhileStmt` and `VisitForStmt` asserted `Block == EntryConditionBlock`
after adding the condition variable's initializer. Both already stored the
result of `addStmt(Init)` in `EntryConditionBlock` and used it correctly
afterwards, so only the assert was wrong; thread the returned block instead,
as `VisitSwitchStmt` already does.

`VisitDeclSubExpr` tracked `LastBlock` precisely because `Block` can be nulled
out here, but then passed the stale `Block` to `maybeAddScopeBeginForVarDecl()`,
which dereferences it. With `cfg-scopes=true` that is a null dereference rather
than a failed assert, so it crashed release builds too, affecting `if` and
`switch` condition variables and C++17 init-statements.

Document the invariant on `addStmt()` and point the callers at it.

Also tighten the adjacent `if (Block && badCFG)` guards to `if (badCFG)`, so a
`badCFG` set while traversing the initializer is not missed once `Block` is
null. This is hardening, not part of the crash fix.

Not analyzer-only: Sema's `AnalysisBasedWarnings` builds a CFG, so this also
reproduces on a plain compile with `-fsyntax-only -Wunreachable-code`.

Fixes #211976
Assisted-by: claude
---
 clang/docs/ReleaseNotes.md                    |   5 +
 clang/lib/Analysis/CFG.cpp                    |  34 +++--
 clang/test/Analysis/cfg.cpp                   | 124 +++++++++++++++++-
 clang/test/Analysis/scopes-cfg-output.cpp     |  69 ++++++++++
 clang/test/SemaCXX/stmt-expr-in-loop-cond.cpp |  73 +++++++++++
 5 files changed, 291 insertions(+), 14 deletions(-)
 create mode 100644 clang/test/SemaCXX/stmt-expr-in-loop-cond.cpp

diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index 61e8378b1704a..ae6295115de26 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -459,6 +459,11 @@ features cannot lower the translation-unit ABI level;
 
 #### Crash and bug fixes
 
+- Fixed a crash during CFG construction when a condition variable or an
+  init-statement of a `while`, `for`, `if` or `switch` was initialized by a
+  statement-expression containing control flow, e.g.
+  `while (S s = ({ while (...) {} S{}; }))`. (#GH211976)
+
 % comment:
 % This is for the Static Analyzer.
 % Use `####` headings for subsections:
diff --git a/clang/lib/Analysis/CFG.cpp b/clang/lib/Analysis/CFG.cpp
index efd1da9c9a4db..df44821ab3c99 100644
--- a/clang/lib/Analysis/CFG.cpp
+++ b/clang/lib/Analysis/CFG.cpp
@@ -797,6 +797,21 @@ class CFGBuilder {
   CFGBlock *createBlock(bool add_successor = true);
   CFGBlock *createNoReturnBlock();
 
+  /// Add a statement to the CFG, returning the block it was added to.
+  ///
+  /// Beware that this does not necessarily leave \c Block current, because the
+  /// statement may itself contain control flow. In particular, a GCC
+  /// statement-expression holding a loop (e.g. \c ({ while (...) {} v; })) 
ends
+  /// the block it started in, so afterwards \c Block may be null and the
+  /// returned block may differ from the one on entry. Callers that need the
+  /// block a statement ended up in must therefore use the returned value 
rather
+  /// than \c Block, and keep the last non-null result when adding several
+  /// statements in a row. The same caveat applies to \c Visit(), which this
+  /// wraps.
+  ///
+  /// May also return null when no block was created, e.g. for a NullStmt when
+  /// \c Block is not current; callers therefore have to keep the previous
+  /// non-null block rather than overwrite it unconditionally.
   CFGBlock *addStmt(Stmt *S) {
     return Visit(S, AddStmtChoice::AlwaysAdd);
   }
@@ -3209,9 +3224,8 @@ CFGBlock *CFGBuilder::VisitDeclSubExpr(DeclStmt *DS) {
       ConstructionContextLayer::create(cfg->getBumpVectorContext(), DS),
       AILE ? AILE->getSubExpr() : Init);
 
-  // Keep track of the last non-null block, as 'Block' can be nulled out
-  // if the initializer expression is something like a 'while' in a
-  // statement-expression.
+  // Keep track of the last non-null block, as 'Block' can be nulled out by
+  // the initializer. See the comment on addStmt().
   CFGBlock *LastBlock = Block;
 
   if (Init) {
@@ -3237,7 +3251,7 @@ CFGBlock *CFGBuilder::VisitDeclSubExpr(DeclStmt *DS) {
       LastBlock = newBlock;
   }
 
-  maybeAddScopeBeginForVarDecl(Block, VD, DS);
+  maybeAddScopeBeginForVarDecl(LastBlock, VD, DS);
 
   // Remove variable from local scope.
   if (ScopePos && VD == *ScopePos)
@@ -3823,13 +3837,13 @@ CFGBlock *CFGBuilder::VisitForStmt(ForStmt *F) {
               ConstructionContextLayer::create(cfg->getBumpVectorContext(), 
DS),
               Init);
           appendStmt(Block, DS);
-          EntryConditionBlock = addStmt(Init);
-          assert(Block == EntryConditionBlock);
+          if (CFGBlock *NewBlock = addStmt(Init))
+            EntryConditionBlock = NewBlock;
           maybeAddScopeBeginForVarDecl(EntryConditionBlock, VD, C);
         }
       }
 
-      if (Block && badCFG)
+      if (badCFG)
         return nullptr;
 
       KnownVal = tryEvaluateBool(C);
@@ -4149,13 +4163,13 @@ CFGBlock *CFGBuilder::VisitWhileStmt(WhileStmt *W) {
                                              const_cast<DeclStmt *>(DS)),
             Init);
         appendStmt(Block, DS);
-        EntryConditionBlock = addStmt(Init);
-        assert(Block == EntryConditionBlock);
+        if (CFGBlock *NewBlock = addStmt(Init))
+          EntryConditionBlock = NewBlock;
         maybeAddScopeBeginForVarDecl(EntryConditionBlock, VD, C);
       }
     }
 
-    if (Block && badCFG)
+    if (badCFG)
       return nullptr;
 
     // See if this is a known constant.
diff --git a/clang/test/Analysis/cfg.cpp b/clang/test/Analysis/cfg.cpp
index 2a88b73d27756..f6330168495df 100644
--- a/clang/test/Analysis/cfg.cpp
+++ b/clang/test/Analysis/cfg.cpp
@@ -1,7 +1,9 @@
-// RUN: %clang_analyze_cc1 -analyzer-checker=debug.DumpCFG -triple 
x86_64-apple-darwin12 -Wno-error=invalid-gnu-asm-cast -std=c++11 
-analyzer-config cfg-rich-constructors=false %s > %t 2>&1
-// RUN: FileCheck --input-file=%t -check-prefixes=CHECK,WARNINGS %s
-// RUN: %clang_analyze_cc1 -analyzer-checker=debug.DumpCFG -triple 
x86_64-apple-darwin12 -Wno-error=invalid-gnu-asm-cast -std=c++11 
-analyzer-config cfg-rich-constructors=true %s > %t 2>&1
-// RUN: FileCheck --input-file=%t -check-prefixes=CHECK,ANALYZER %s
+
+// DEFINE: %{clang-dump-cfg} = %clang_analyze_cc1 
-analyzer-checker=debug.DumpCFG -triple x86_64-apple-darwin12 
-Wno-error=invalid-gnu-asm-cast -std=c++11
+// RUN: %{clang-dump-cfg} -analyzer-config cfg-rich-constructors=false %s 2>&1 
| \
+// RUN:   FileCheck -check-prefixes=CHECK,WARNINGS %s
+// RUN: %{clang-dump-cfg} -analyzer-config cfg-rich-constructors=true  %s 2>&1 
| \
+// RUN:   FileCheck -check-prefixes=CHECK,ANALYZER %s
 
 // This file tests how we construct two different flavors of the Clang CFG -
 // the CFG used by the Sema analysis-based warnings and the CFG used by the
@@ -491,6 +493,120 @@ int foo() {
 }
 } // namespace statement_expression_in_return
 
+// A statement-expression initializing a loop condition variable finishes the
+// current block, so the entry to the condition is the block the nested loop
+// starts in.  The loop-back edge has to point there, not at the block holding
+// the condition variable.
+namespace statement_expression_in_loop_condition_variable {
+struct S { explicit operator bool(); };
+
+// CHECK-LABEL: void while_loop(int n)
+// CHECK:       [B6 (ENTRY)]
+// CHECK-NEXT:    Succs (1): B5
+// CHECK:       [B1]
+// CHECK-NEXT:    Preds (1): B2
+// CHECK-NEXT:    Succs (1): B5
+// CHECK:       [B2]
+// CHECK-NEXT:    1: n
+// CHECK-NEXT:    2: --[B2.1]
+// CHECK-NEXT:    Preds (1): B3
+// CHECK-NEXT:    Succs (1): B1
+// CHECK:       [B3]
+// CHECK-NEXT:    1: {}
+// CHECK-NEXT:    2: S[B3.1] (CXXFunctionalCastExpr, NoOp, S)
+// CHECK-NEXT:    3: [B3.2]
+// CHECK-NEXT:    4: [B3.3] (CXXConstructExpr, S)
+// CHECK-NEXT:    5: ({ ... ;  })
+// CHECK-NEXT:    6: [B3.5]
+// WARNINGS-NEXT:    7: [B3.6] (CXXConstructExpr, S)
+// ANALYZER-NEXT:    7: [B3.6] (CXXConstructExpr, [B3.8], S)
+// CHECK-NEXT:    8: S s = ({
+// CHECK-NEXT:     while (S t{})
+// CHECK-NEXT:         {
+// CHECK-NEXT:         }
+// CHECK-NEXT:     S{};
+// CHECK-NEXT: });
+// CHECK-NEXT:    9: s
+// CHECK-NEXT:   10: [B3.9].operator bool
+// CHECK-NEXT:   11: [B3.9]
+// CHECK-NEXT:   12: [B3.11] (ImplicitCastExpr, UserDefinedConversion, _Bool)
+// CHECK-NEXT:    T: while [B3.12]
+// CHECK-NEXT:    Preds (1): B5
+// CHECK-NEXT:    Succs (2): B2 B0
+// CHECK:       [B4]
+// CHECK-NEXT:    Preds (1): B5
+// CHECK-NEXT:    Succs (1): B5
+// CHECK:       [B5]
+// CHECK-NEXT:    1: {}
+// CHECK-NEXT:    2: S t{};
+// CHECK-NEXT:    3: t
+// CHECK-NEXT:    4: [B5.3].operator bool
+// CHECK-NEXT:    5: [B5.3]
+// CHECK-NEXT:    6: [B5.5] (ImplicitCastExpr, UserDefinedConversion, _Bool)
+// CHECK-NEXT:    T: while [B5.6]
+// CHECK-NEXT:    Preds (3): B4 B1 B6
+// CHECK-NEXT:    Succs (2): B4 B3
+// CHECK:       [B0 (EXIT)]
+// CHECK-NEXT:    Preds (1): B3
+void while_loop(int n) {
+  while (S s = ({ while (S t{}) {} S{}; }))
+    --n;
+}
+
+// CHECK-LABEL: void for_loop(int n)
+// CHECK:       [B6 (ENTRY)]
+// CHECK-NEXT:    Succs (1): B5
+// CHECK:       [B1]
+// CHECK-NEXT:    Preds (1): B2
+// CHECK-NEXT:    Succs (1): B5
+// CHECK:       [B2]
+// CHECK-NEXT:    1: n
+// CHECK-NEXT:    2: --[B2.1]
+// CHECK-NEXT:    Preds (1): B3
+// CHECK-NEXT:    Succs (1): B1
+// CHECK:       [B3]
+// CHECK-NEXT:    1: {}
+// CHECK-NEXT:    2: S[B3.1] (CXXFunctionalCastExpr, NoOp, S)
+// CHECK-NEXT:    3: [B3.2]
+// CHECK-NEXT:    4: [B3.3] (CXXConstructExpr, S)
+// CHECK-NEXT:    5: ({ ... ;  })
+// CHECK-NEXT:    6: [B3.5]
+// WARNINGS-NEXT:    7: [B3.6] (CXXConstructExpr, S)
+// ANALYZER-NEXT:    7: [B3.6] (CXXConstructExpr, [B3.8], S)
+// CHECK-NEXT:    8: S s = ({
+// CHECK-NEXT:     while (S t{})
+// CHECK-NEXT:         {
+// CHECK-NEXT:         }
+// CHECK-NEXT:     S{};
+// CHECK-NEXT: });
+// CHECK-NEXT:    9: s
+// CHECK-NEXT:   10: [B3.9].operator bool
+// CHECK-NEXT:   11: [B3.9]
+// CHECK-NEXT:   12: [B3.11] (ImplicitCastExpr, UserDefinedConversion, _Bool)
+// CHECK-NEXT:    T: for (; [B3.12]; )
+// CHECK-NEXT:    Preds (1): B5
+// CHECK-NEXT:    Succs (2): B2 B0
+// CHECK:       [B4]
+// CHECK-NEXT:    Preds (1): B5
+// CHECK-NEXT:    Succs (1): B5
+// CHECK:       [B5]
+// CHECK-NEXT:    1: {}
+// CHECK-NEXT:    2: S t{};
+// CHECK-NEXT:    3: t
+// CHECK-NEXT:    4: [B5.3].operator bool
+// CHECK-NEXT:    5: [B5.3]
+// CHECK-NEXT:    6: [B5.5] (ImplicitCastExpr, UserDefinedConversion, _Bool)
+// CHECK-NEXT:    T: while [B5.6]
+// CHECK-NEXT:    Preds (3): B4 B1 B6
+// CHECK-NEXT:    Succs (2): B4 B3
+// CHECK:       [B0 (EXIT)]
+// CHECK-NEXT:    Preds (1): B3
+void for_loop(int n) {
+  for (; S s = ({ while (S t{}) {} S{}; });)
+    --n;
+}
+} // namespace statement_expression_in_loop_condition_variable
+
 // CHECK-LABEL: void vla_simple(int x)
 // CHECK: [B1]
 // CHECK-NEXT:   1: x
diff --git a/clang/test/Analysis/scopes-cfg-output.cpp 
b/clang/test/Analysis/scopes-cfg-output.cpp
index ac8ff1179a994..2297d33443faa 100644
--- a/clang/test/Analysis/scopes-cfg-output.cpp
+++ b/clang/test/Analysis/scopes-cfg-output.cpp
@@ -1484,3 +1484,72 @@ void cleanup_F(F *f);
 void test() {
   F f __attribute((cleanup(cleanup_F)));
 }
+
+// An initializer that is a statement-expression containing control flow
+// finishes the current block, so the scope of the condition variable has to
+// begin in the block the initializer actually starts in.
+// CHECK:      [B7 (ENTRY)]
+// CHECK-NEXT:   Succs (1): B6
+// CHECK:      [B1]
+// CHECK-NEXT:   1: [B3.7].~D() (Implicit destructor)
+// CHECK-NEXT:   2: CFGScopeEnd(d)
+// CHECK-NEXT:   Preds (2): B2 B3
+// CHECK-NEXT:   Succs (1): B0
+// CHECK:      [B2]
+// CHECK-NEXT:   1: n
+// CHECK-NEXT:   2: --[B2.1]
+// CHECK-NEXT:   Preds (1): B3
+// CHECK-NEXT:   Succs (1): B1
+// CHECK:      [B3]
+// CHECK-NEXT:   1: [B6.4].~D() (Implicit destructor)
+// CHECK-NEXT:   2: CFGScopeEnd(t)
+// CHECK-NEXT:   3: D{} (CXXConstructExpr, [B3.4], D)
+// CHECK-NEXT:   4: [B3.3] (BindTemporary)
+// CHECK-NEXT:   5: ({ ... ;  })
+// CHECK-NEXT:   6: [B3.5] (BindTemporary)
+// CHECK-NEXT:   7: D d = ({
+// CHECK-NEXT:    while (D t{})
+// CHECK-NEXT:        {
+// CHECK-NEXT:        }
+// CHECK-NEXT:    D{};
+// CHECK-NEXT: });
+// CHECK-NEXT:   8: d
+// CHECK-NEXT:   9: [B3.8].operator bool
+// CHECK-NEXT:  10: [B3.8]
+// CHECK-NEXT:  11: [B3.10] (ImplicitCastExpr, UserDefinedConversion, _Bool)
+// CHECK-NEXT:   T: if [B3.11]
+// CHECK-NEXT:   Preds (1): B6
+// CHECK-NEXT:   Succs (2): B2 B1
+// CHECK:      [B4]
+// CHECK-NEXT:   Preds (1): B5
+// CHECK-NEXT:   Succs (1): B6
+// CHECK:      [B5]
+// CHECK-NEXT:   1: [B6.4].~D() (Implicit destructor)
+// CHECK-NEXT:   2: CFGScopeEnd(t)
+// CHECK-NEXT:   Preds (1): B6
+// CHECK-NEXT:   Succs (1): B4
+// CHECK:      [B6]
+// CHECK-NEXT:   1: CFGScopeBegin(d)
+// CHECK-NEXT:   2: CFGScopeBegin(t)
+// CHECK-NEXT:   3: {} (CXXConstructExpr, [B6.4], D)
+// CHECK-NEXT:   4: D t{};
+// CHECK-NEXT:   5: t
+// CHECK-NEXT:   6: [B6.5].operator bool
+// CHECK-NEXT:   7: [B6.5]
+// CHECK-NEXT:   8: [B6.7] (ImplicitCastExpr, UserDefinedConversion, _Bool)
+// CHECK-NEXT:   T: while [B6.8]
+// CHECK-NEXT:   Preds (2): B4 B7
+// CHECK-NEXT:   Succs (2): B5 B3
+// CHECK:      [B0 (EXIT)]
+// CHECK-NEXT:   Preds (1): B1
+class D {
+public:
+  D();
+  ~D();
+  explicit operator bool();
+};
+
+void if_cond_var_stmt_expr(int n) {
+  if (D d = ({ while (D t{}) {} D{}; }))
+    --n;
+}
diff --git a/clang/test/SemaCXX/stmt-expr-in-loop-cond.cpp 
b/clang/test/SemaCXX/stmt-expr-in-loop-cond.cpp
new file mode 100644
index 0000000000000..fb1def062b214
--- /dev/null
+++ b/clang/test/SemaCXX/stmt-expr-in-loop-cond.cpp
@@ -0,0 +1,73 @@
+// RUN: %clang_cc1 -std=c++17 -fsyntax-only -Wunreachable-code -verify %s
+
+// Do not crash when a condition variable or an init-statement is initialized
+// by a statement-expression containing control flow.  Such an initializer
+// finishes the current CFG block, so the builder must not assume the block it
+// started with is still current.
+// https://github.com/llvm/llvm-project/issues/211976
+
+// expected-no-diagnostics
+
+struct S { explicit operator bool(); };
+
+// A non-trivial destructor exercises the scope/destructor bookkeeping.
+struct D { D(); ~D(); explicit operator bool(); };
+struct I { I(); ~I(); operator int(); };
+
+void while_loop(int n) {
+  while (S s = ({ while (S t{}) {} S{}; }))
+    --n;
+}
+
+void for_loop(int n) {
+  for (; S s = ({ while (S t{}) {} S{}; });)
+    --n;
+}
+
+void for_loop_with_init_and_inc(int n) {
+  for (int i = 0; S s = ({ while (S t{}) {} S{}; }); ++i) {
+    if (i > n)
+      break;
+    continue;
+  }
+}
+
+void do_loop(int n) {
+  do {
+    --n;
+  } while (({ while (S t{}) {} n; }));
+}
+
+void if_condition_variable(int n) {
+  if (D d = ({ while (D t{}) {} D{}; }))
+    --n;
+}
+
+void if_init_statement(int n) {
+  if (D d = ({ while (D t{}) {} D{}; }); n)
+    --n;
+}
+
+void switch_condition_variable(int n) {
+  switch (I i = ({ while (D t{}) {} I{}; })) {
+  default:
+    break;
+  }
+}
+
+void switch_init_statement(int n) {
+  switch (D d = ({ while (D t{}) {} D{}; }); n) {
+  default:
+    break;
+  }
+}
+
+// The condition variable is usable in the body, and the nested loop must be
+// re-evaluated on every iteration.
+void condition_variable_used_in_body(int n) {
+  while (D d = ({ while (D t{}) {} D{}; })) {
+    if (d)
+      continue;
+    break;
+  }
+}

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to