[clang] [LifetimeSafety] Add origin tracking for lambda captures (PR #185216)

Zhijie Wang via cfe-commits cfe-commits at lists.llvm.org
Sat Mar 7 10:33:09 PST 2026


https://github.com/aeft created https://github.com/llvm/llvm-project/pull/185216

This is the first step toward pointer-field sensitivity (#184344).

- `hasOrigins` extension: lambda closure types whose fields have origins now participate in origin tracking.
- `VisitLambdaExpr`: each lambda gets a single merged origin.
- Lambda closure copy/move constructors now propagate origins.

>From ee2dcc8f33a2df8f7cd3e2bcafa28210e65aa157 Mon Sep 17 00:00:00 2001
From: Alex Wang <yesterda9 at gmail.com>
Date: Sat, 7 Mar 2026 10:19:58 -0800
Subject: [PATCH] [LifetimeSafety] Add origin tracking for lambda captures

---
 .../Analyses/LifetimeSafety/FactsGenerator.h  |  1 +
 .../LifetimeSafety/FactsGenerator.cpp         | 29 +++++++
 clang/lib/Analysis/LifetimeSafety/Origins.cpp | 14 +++-
 clang/test/Sema/warn-lifetime-safety.cpp      | 75 +++++++++++++++++++
 .../unittests/Analysis/LifetimeSafetyTest.cpp | 47 ++++++++++++
 5 files changed, 165 insertions(+), 1 deletion(-)

diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
index dbe5a1eeb498e..ddaa69719b666 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
@@ -50,6 +50,7 @@ class FactsGenerator : public ConstStmtVisitor<FactsGenerator> {
   void VisitInitListExpr(const InitListExpr *ILE);
   void VisitCXXBindTemporaryExpr(const CXXBindTemporaryExpr *BTE);
   void VisitMaterializeTemporaryExpr(const MaterializeTemporaryExpr *MTE);
+  void VisitLambdaExpr(const LambdaExpr *LE);
 
 private:
   OriginList *getOriginsList(const ValueDecl &D);
diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index f39d677758393..61fd7359633d2 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -181,6 +181,14 @@ void FactsGenerator::VisitCXXConstructExpr(const CXXConstructExpr *CCE) {
     handleGSLPointerConstruction(CCE);
     return;
   }
+  if (const auto *RD = CCE->getType()->getAsCXXRecordDecl();
+      RD && RD->isLambda() && CCE->getNumArgs() == 1) {
+    const Expr *Arg = CCE->getArg(0);
+    if (OriginList *ArgList = getRValueOrigins(Arg, getOriginsList(*Arg))) {
+      flow(getOriginsList(*CCE), ArgList, /*Kill=*/true);
+      return;
+    }
+  }
   handleFunctionCall(CCE, CCE->getConstructor(),
                      {CCE->getArgs(), CCE->getNumArgs()},
                      /*IsGslConstruction=*/false);
@@ -431,6 +439,27 @@ void FactsGenerator::VisitMaterializeTemporaryExpr(
   }
 }
 
+void FactsGenerator::VisitLambdaExpr(const LambdaExpr *LE) {
+  // The lambda gets a single merged origin that aggregates all captured
+  // pointer-like origins. Currently we only need to detect whether the lambda
+  // outlives any capture.
+  OriginList *LambdaList = getOriginsList(*LE);
+  if (!LambdaList)
+    return;
+  bool Kill = true;
+  for (unsigned I = 0; I < LE->capture_size(); ++I) {
+    const Expr *Init = LE->capture_init_begin()[I];
+    if (!Init)
+      continue;
+    OriginList *InitList = getOriginsList(*Init);
+    if (!InitList)
+      continue;
+    CurrentBlockFacts.push_back(FactMgr.createFact<OriginFlowFact>(
+        LambdaList->getOuterOriginID(), InitList->getOuterOriginID(), Kill));
+    Kill = false;
+  }
+}
+
 void FactsGenerator::handleLifetimeEnds(const CFGLifetimeEnds &LifetimeEnds) {
   const VarDecl *LifetimeEndsVD = LifetimeEnds.getVarDecl();
   if (!LifetimeEndsVD)
diff --git a/clang/lib/Analysis/LifetimeSafety/Origins.cpp b/clang/lib/Analysis/LifetimeSafety/Origins.cpp
index a9e40d6b7aaf1..0122f7a734541 100644
--- a/clang/lib/Analysis/LifetimeSafety/Origins.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Origins.cpp
@@ -51,7 +51,19 @@ class MissingOriginCollector
 } // namespace
 
 bool hasOrigins(QualType QT) {
-  return QT->isPointerOrReferenceType() || isGslPointerType(QT);
+  if (QT->isPointerOrReferenceType() || isGslPointerType(QT))
+    return true;
+  const auto *RD = QT->getAsCXXRecordDecl();
+  if (!RD)
+    return false;
+  // TODO: Limit to lambdas for now. This will be extended to user-defined
+  // structs with pointer-like fields.
+  if (!RD->isLambda())
+    return false;
+  for (const auto *FD : RD->fields())
+    if (hasOrigins(FD->getType()))
+      return true;
+  return false;
 }
 
 /// Determines if an expression has origins that need to be tracked.
diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp
index a75c70aa3674a..dc7ce22913755 100644
--- a/clang/test/Sema/warn-lifetime-safety.cpp
+++ b/clang/test/Sema/warn-lifetime-safety.cpp
@@ -1784,3 +1784,78 @@ void test_optional_view_arrow() {
     (void)*p;
 }
 } // namespace OwnerArrowOperator
+
+namespace lambda_captures {
+  auto return_ref_capture() {
+    int local = 1;
+    auto lambda = [&local]() { return local; }; // expected-warning {{address of stack memory is returned later}}
+    return lambda; // expected-note {{returned here}}
+  }
+
+  void safe_ref_capture() {
+    int local = 1;
+    auto lambda = [&local]() { return local; };
+    lambda();
+  }
+
+  auto capture_int_by_value() {
+    int x = 1;
+    auto lambda = [x]() { return x; };
+    return lambda;
+  }
+
+  auto capture_view_by_value() {
+    MyObj obj;
+    View v(obj); // expected-warning {{address of stack memory is returned later}}
+    auto lambda = [v]() { return v; };
+    return lambda; // expected-note {{returned here}}
+  }
+
+  void capture_view_by_value_safe() {
+    MyObj obj;
+    View v(obj);
+    auto lambda = [v]() { return v; };
+    lambda();
+  }
+
+  auto capture_pointer_by_ref() {
+    MyObj obj;
+    MyObj* p = &obj;
+    auto lambda = [&p]() { return p; }; // expected-warning {{address of stack memory is returned later}}
+    return lambda; // expected-note {{returned here}}
+  }
+
+  auto capture_multiple() {
+    int a, b;
+    auto lambda = [&a, &b]() { return a + b; }; // expected-warning 2 {{address of stack memory is returned later}}
+    return lambda; // expected-note 2 {{returned here}}
+  }
+
+  auto capture_raw_pointer_by_value() {
+    int x;
+    int* p = &x; // expected-warning {{address of stack memory is returned later}}
+    auto lambda = [p]() { return p; };
+    return lambda; // expected-note {{returned here}}
+  }
+
+  auto capture_raw_pointer_init_capture() {
+    int x;
+    int* p = &x; // expected-warning {{address of stack memory is returned later}}
+    auto lambda = [q = p]() { return q; };
+    return lambda; // expected-note {{returned here}}
+  }
+
+  auto capture_view_init_capture() {
+    MyObj obj;
+    View v(obj); // expected-warning {{address of stack memory is returned later}}
+    auto lambda = [w = v]() { return w; };
+    return lambda; // expected-note {{returned here}}
+  }
+
+  auto capture_lambda() {
+    int x;
+    auto inner = [&x]() { return x; }; // expected-warning {{address of stack memory is returned later}}
+    auto outer = [inner]() { return inner(); };
+    return outer; // expected-note {{returned here}}
+  }
+} // namespace lambda_captures
diff --git a/clang/unittests/Analysis/LifetimeSafetyTest.cpp b/clang/unittests/Analysis/LifetimeSafetyTest.cpp
index a27f746fffb60..2116f7736c4be 100644
--- a/clang/unittests/Analysis/LifetimeSafetyTest.cpp
+++ b/clang/unittests/Analysis/LifetimeSafetyTest.cpp
@@ -1904,5 +1904,52 @@ TEST_F(LifetimeAnalysisTest, DerivedViewWithNoAnnotation) {
   // EXPECT_THAT(Origin("view"), HasLoansTo({"my_obj_or"}, "p1"));
 }
 
+TEST_F(LifetimeAnalysisTest, LambdaCaptureByRef) {
+  SetupTest(R"(
+    void target() {
+      int x;
+      int* p = &x;
+      auto lambda = [&p]() { return p; };
+      POINT(after_lambda);
+    }
+  )");
+  EXPECT_THAT(Origin("lambda"), HasLoansTo({"p"}, "after_lambda"));
+}
+
+TEST_F(LifetimeAnalysisTest, LambdaCaptureViewByValue) {
+  SetupTest(R"(
+    void target() {
+      MyObj obj;
+      View v(obj);
+      auto lambda = [v]() { return v; };
+      POINT(after_lambda);
+    }
+  )");
+  EXPECT_THAT(Origin("lambda"), HasLoansTo({"obj"}, "after_lambda"));
+}
+
+TEST_F(LifetimeAnalysisTest, LambdaInitCaptureRawPointerByValue) {
+  SetupTest(R"(
+    void target() {
+      int x;
+      int* p = &x;
+      auto lambda = [q = p]() { return q; };
+      POINT(after_lambda);
+    }
+  )");
+  EXPECT_THAT(Origin("lambda"), HasLoansTo({"x"}, "after_lambda"));
+}
+
+TEST_F(LifetimeAnalysisTest, LambdaInitCaptureViewByValue) {
+  SetupTest(R"(
+    void target() {
+      MyObj obj;
+      View v(obj);
+      auto lambda = [w = v]() { return w; };
+      POINT(after_lambda);
+    }
+  )");
+  EXPECT_THAT(Origin("lambda"), HasLoansTo({"obj"}, "after_lambda"));
+}
 } // anonymous namespace
 } // namespace clang::lifetimes::internal



More information about the cfe-commits mailing list