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

Gábor Horváth via cfe-commits cfe-commits at lists.llvm.org
Mon Jun 29 06:38:26 PDT 2026


https://github.com/Xazax-hun updated https://github.com/llvm/llvm-project/pull/206458

>From 30bef5ed27c8fa9ba2b22363852a70e7897b95fa Mon Sep 17 00:00:00 2001
From: Gabor Horvath <gaborh at apple.com>
Date: Mon, 29 Jun 2026 11:43:49 +0100
Subject: [PATCH 1/3] [CallGraph] Collect callables from global variable
 initializers

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
---
 clang/include/clang/Analysis/CallGraph.h  | 11 +++++++
 clang/lib/Analysis/CallGraph.cpp          | 14 +++++++++
 clang/test/Sema/LifetimeSafety/safety.cpp | 38 +++++++++++++++++++++++
 3 files changed, 63 insertions(+)

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() {

>From 425cf54a01bf6b2eda357a99a83af397e0e398e0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?G=C3=A1bor=20Horv=C3=A1th?= <xazax.hun at gmail.com>
Date: Mon, 29 Jun 2026 14:03:49 +0100
Subject: [PATCH 2/3] Update clang/lib/Analysis/CallGraph.cpp
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: Balázs Benics <benicsbalazs at gmail.com>
---
 clang/lib/Analysis/CallGraph.cpp | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/clang/lib/Analysis/CallGraph.cpp b/clang/lib/Analysis/CallGraph.cpp
index dd4074a10beb6..c0e73c1d3ce11 100644
--- a/clang/lib/Analysis/CallGraph.cpp
+++ b/clang/lib/Analysis/CallGraph.cpp
@@ -205,8 +205,7 @@ void CallGraph::addNodesForVarInit(VarDecl *VD) {
   if (!VD->hasGlobalStorage())
     return;
   if (Expr *Init = VD->getInit()) {
-    CGBuilder builder(this, Root);
-    builder.Visit(Init);
+    CGBuilder{this, Root}.Visit(Init);
   }
 }
 

>From 876c611a29207f79c8782d550ab5d2040c13e362 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?G=C3=A1bor=20Horv=C3=A1th?= <xazax.hun at gmail.com>
Date: Mon, 29 Jun 2026 14:38:15 +0100
Subject: [PATCH 3/3] Address review comment.

---
 clang/lib/Analysis/CallGraph.cpp | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/clang/lib/Analysis/CallGraph.cpp b/clang/lib/Analysis/CallGraph.cpp
index c0e73c1d3ce11..d5ba64475da10 100644
--- a/clang/lib/Analysis/CallGraph.cpp
+++ b/clang/lib/Analysis/CallGraph.cpp
@@ -204,9 +204,8 @@ void CallGraph::addNodesForVarInit(VarDecl *VD) {
   // CGBuilder at call sites. Attach discovered calls to the root.
   if (!VD->hasGlobalStorage())
     return;
-  if (Expr *Init = VD->getInit()) {
+  if (Expr *Init = VD->getInit())
     CGBuilder{this, Root}.Visit(Init);
-  }
 }
 
 CallGraphNode *CallGraph::getNode(const Decl *F) const {



More information about the cfe-commits mailing list