[clang] [CallGraph] Collect callables from global variable initializers (PR #206458)

via cfe-commits cfe-commits at lists.llvm.org
Mon Jun 29 04:11:09 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-clang

Author: Gábor Horváth (Xazax-hun)

<details>
<summary>Changes</summary>

CallGraph::TraverseStmt is a no-op, so declaration traversal never descends into a variable initializer. A lambda or block defined in a global-storage variable's initializer was therefore never added to the graph. TU-end lifetime safety analysis walks the call graph to find functions to analyze, so such a lambda was silently skipped and a real lifetime bug in it went unreported, while the default per-function mode caught it.

Walk the initializer of every global-storage variable during declaration visitation and add any callables defined within it. The hasGlobalStorage guard excludes parameter default arguments, which CGBuilder already handles at call sites.

Assisted-by: Claude Opus 4.8

---
Full diff: https://github.com/llvm/llvm-project/pull/206458.diff


3 Files Affected:

- (modified) clang/include/clang/Analysis/CallGraph.h (+11) 
- (modified) clang/lib/Analysis/CallGraph.cpp (+14) 
- (modified) clang/test/Sema/LifetimeSafety/safety.cpp (+38) 


``````````diff
diff --git a/clang/include/clang/Analysis/CallGraph.h b/clang/include/clang/Analysis/CallGraph.h
index f80790902e6b1..d7cb9bf74537f 100644
--- a/clang/include/clang/Analysis/CallGraph.h
+++ b/clang/include/clang/Analysis/CallGraph.h
@@ -135,12 +135,23 @@ class CallGraph : public DynamicRecursiveASTVisitor {
     return true;
   }
 
+  /// Part of recursive declaration visitation. A global-storage variable's
+  /// initializer can define callables (lambdas, blocks) no function body
+  /// reaches; TraverseStmt is a no-op, so collect them here.
+  bool VisitVarDecl(VarDecl *VD) override {
+    addNodesForVarInit(VD);
+    return true;
+  }
+
   // We are only collecting the declarations, so do not step into the bodies.
   bool TraverseStmt(Stmt *S) override { return true; }
 
 private:
   /// Add the given declaration to the call graph.
   void addNodeForDecl(Decl *D, bool IsGlobal);
+
+  /// Walk a variable's initializer to add any callables defined within it.
+  void addNodesForVarInit(VarDecl *VD);
 };
 
 class CallGraphNode {
diff --git a/clang/lib/Analysis/CallGraph.cpp b/clang/lib/Analysis/CallGraph.cpp
index 26646cd6b6502..dd4074a10beb6 100644
--- a/clang/lib/Analysis/CallGraph.cpp
+++ b/clang/lib/Analysis/CallGraph.cpp
@@ -196,6 +196,20 @@ void CallGraph::addNodeForDecl(Decl* D, bool IsGlobal) {
   }
 }
 
+void CallGraph::addNodesForVarInit(VarDecl *VD) {
+  // Only variables with static or thread storage duration need this: their
+  // initializers run with no function caller and are not reached by any body
+  // walk, so a lambda or block defined in one would be missed. Local variables
+  // are covered by the enclosing body walk, and parameter default arguments by
+  // CGBuilder at call sites. Attach discovered calls to the root.
+  if (!VD->hasGlobalStorage())
+    return;
+  if (Expr *Init = VD->getInit()) {
+    CGBuilder builder(this, Root);
+    builder.Visit(Init);
+  }
+}
+
 CallGraphNode *CallGraph::getNode(const Decl *F) const {
   FunctionMapTy::const_iterator I = FunctionMap.find(F);
   if (I == FunctionMap.end()) return nullptr;
diff --git a/clang/test/Sema/LifetimeSafety/safety.cpp b/clang/test/Sema/LifetimeSafety/safety.cpp
index 2cbf651eb46b5..57369a95d0e82 100644
--- a/clang/test/Sema/LifetimeSafety/safety.cpp
+++ b/clang/test/Sema/LifetimeSafety/safety.cpp
@@ -2390,6 +2390,44 @@ auto capture_multilevel_pointer() {
 }
 } // namespace lambda_captures
 
+namespace global_init_lambda {
+// A lambda defined in a global-storage variable initializer is reached by the
+// call graph, so TU-end analysis (not just per-function mode) sees a stack
+// address escaping to a global.
+int *leaked = nullptr; // expected-note 3 {{this global dangles}}
+
+int escaping = [] {
+  int local = 0;
+  leaked = &local; // expected-warning {{stack memory associated with local variable 'local' escapes to the global variable 'leaked' which will dangle}}
+  return 1;
+}();
+
+// A lambda nested inside another global-init lambda is reached too.
+int nested = [] {
+  auto inner = [] {
+    int local = 0;
+    leaked = &local; // expected-warning {{stack memory associated with local variable 'local' escapes to the global variable 'leaked' which will dangle}}
+    return 0;
+  };
+  return inner();
+}();
+
+// A static data member initializer is reached the same way.
+struct S {
+  static inline int member = [] {
+    int local = 0;
+    leaked = &local; // expected-warning {{stack memory associated with local variable 'local' escapes to the global variable 'leaked' which will dangle}}
+    return 1;
+  }();
+};
+
+// A clean lambda introduces no false positive.
+int ok = [] {
+  int local = 42;
+  return local;
+}();
+} // namespace global_init_lambda
+
 namespace LoopLocalPointers {
 
 void conditional_assignment_in_loop() {

``````````

</details>


https://github.com/llvm/llvm-project/pull/206458


More information about the cfe-commits mailing list