[clang] 4f41079 - [LifetimeSafety] Catch intra-method lifetime_capture_by(this) dangles (#204630)

via cfe-commits cfe-commits at lists.llvm.org
Fri Oct 23 08:46:11 PDT 2026


Author: Gábor Horváth
Date: 2026-10-23T08:46:05-07:00
New Revision: 4f410791085ba7c79a2df527622db4bd390a449d

URL: https://github.com/llvm/llvm-project/commit/4f410791085ba7c79a2df527622db4bd390a449d
DIFF: https://github.com/llvm/llvm-project/commit/4f410791085ba7c79a2df527622db4bd390a449d.diff

LOG: [LifetimeSafety] Catch intra-method lifetime_capture_by(this) dangles (#204630)

Added: 
    

Modified: 
    clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
    clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h
    clang/lib/Analysis/LifetimeSafety/Checker.cpp
    clang/lib/Analysis/LifetimeSafety/Facts.cpp
    clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
    clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp
    clang/lib/Sema/SemaLifetimeSafety.h
    clang/test/Sema/LifetimeSafety/invalidations.cpp
    clang/test/Sema/LifetimeSafety/noescape-violation.cpp
    clang/test/Sema/LifetimeSafety/safety.cpp

Removed: 
    


################################################################################
diff  --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
index 16fe31a577fa1b..a78d22a80b527e 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
@@ -174,6 +174,8 @@ class OriginEscapesFact : public Fact {
     Return, /// Escapes via return statement.
     Field,  /// Escapes via assignment to a field.
     Global, /// Escapes via assignment to global storage.
+    This,   /// Escapes via the enclosing object `this`, which outlives the
+            /// method.
   } EscKind;
 
   static bool classof(const Fact *F) {
@@ -243,6 +245,28 @@ class GlobalEscapeFact : public OriginEscapesFact {
             const LoanPropagationAnalysis *LPA = nullptr) const override;
 };
 
+/// Represents an origin escaping through the enclosing object `this`, which
+/// outlives the current method. Emitted for `this` at function exit so a borrow
+/// still held by the object there is seen as escaping -- e.g. a borrow captured
+/// via [[clang::lifetime_capture_by_this]] that outlived the captured local is
+/// reported as a use-after-scope.
+class ThisEscapeFact : public OriginEscapesFact {
+  SourceLocation Loc;
+
+public:
+  ThisEscapeFact(OriginID OID, SourceLocation Loc)
+      : OriginEscapesFact(OID, EscapeKind::This), Loc(Loc) {}
+
+  static bool classof(const Fact *F) {
+    return F->getKind() == Kind::OriginEscapes &&
+           static_cast<const OriginEscapesFact *>(F)->getEscapeKind() ==
+               EscapeKind::This;
+  }
+  SourceLocation getLoc() const { return Loc; }
+  void dump(llvm::raw_ostream &OS, const LoanManager &, const OriginManager &OM,
+            const LoanPropagationAnalysis *LPA = nullptr) const override;
+};
+
 class UseFact : public Fact {
   const Expr *UseExpr;
   const OriginList *OList;

diff  --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h
index f8b7d2008bd789..eb227eb1566868 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h
@@ -75,6 +75,13 @@ class LifetimeSafetySemaHelper {
                                    SourceLocation FreeLoc,
                                    llvm::ArrayRef<const Expr *> ExprChain) {}
 
+  // Overload for a use with only a location and no expression (e.g. a borrow
+  // captured into the object and still held at the capturing method's exit).
+  virtual void reportUseAfterScope(const Expr *IssueExpr, SourceLocation UseLoc,
+                                   const Expr *MovedExpr,
+                                   SourceLocation FreeLoc,
+                                   llvm::ArrayRef<const Expr *> ExprChain) {}
+
   // TODO: Report where the object was destroyed when that happens before the
   // return (inner scopes, temporaries).
   virtual void reportUseAfterReturn(const Expr *IssueExpr,
@@ -104,6 +111,14 @@ class LifetimeSafetySemaHelper {
   reportUseAfterInvalidation(const ParmVarDecl *PVD, const Expr *UseExpr,
                              const Expr *InvalidationExpr,
                              llvm::ArrayRef<const Expr *> ExprChain) {}
+  virtual void
+  reportUseAfterInvalidation(const Expr *IssueExpr, SourceLocation UseLoc,
+                             const Expr *InvalidationExpr,
+                             llvm::ArrayRef<const Expr *> ExprChain) {}
+  virtual void
+  reportUseAfterInvalidation(const ParmVarDecl *PVD, SourceLocation UseLoc,
+                             const Expr *InvalidationExpr,
+                             llvm::ArrayRef<const Expr *> ExprChain) {}
   virtual void reportInvalidatedField(const Expr *IssueExpr,
                                       const FieldDecl *Field,
                                       const Expr *InvalidationExpr) {}

diff  --git a/clang/lib/Analysis/LifetimeSafety/Checker.cpp b/clang/lib/Analysis/LifetimeSafety/Checker.cpp
index 3d2cab92b384bf..ae0297af25d8d5 100644
--- a/clang/lib/Analysis/LifetimeSafety/Checker.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Checker.cpp
@@ -351,6 +351,19 @@ class LifetimeChecker {
                                                   Warning.InvalidatedByExpr);
           } else if (isa<ReturnEscapeFact>(OEF)) {
             // FIXME: Diagnose invalidated return escapes separately.
+          } else if (const auto *ThisEscape = dyn_cast<ThisEscapeFact>(OEF)) {
+            // A loan still held by the object at the method's exit was
+            // invalidated.
+            llvm::SmallVector<const Expr *> ExprChain = getExprChain(
+                LoanPropagation.buildOriginFlowChain(OEF, LID, Cfg));
+            if (IssueExpr)
+              SemaHelper->reportUseAfterInvalidation(
+                  IssueExpr, ThisEscape->getLoc(), Warning.InvalidatedByExpr,
+                  ExprChain);
+            else if (InvalidatedPVD)
+              SemaHelper->reportUseAfterInvalidation(
+                  InvalidatedPVD, ThisEscape->getLoc(),
+                  Warning.InvalidatedByExpr, ExprChain);
           } else
             llvm_unreachable("Unhandled OriginEscapesFact type");
         } else if (const auto *RetEscape = dyn_cast<ReturnEscapeFact>(OEF)) {
@@ -373,7 +386,16 @@ class LifetimeChecker {
             IsMain = Func->isMain();
           SemaHelper->reportDanglingGlobal(IssueExpr, GlobalEscape->getGlobal(),
                                            MovedExpr, ExpiryLoc, IsMain);
-        } else
+        } else if (const auto *ThisEscape = dyn_cast<ThisEscapeFact>(OEF))
+          // A borrow is still held by the object (e.g. captured via
+          // lifetime_capture_by_this) when the captured local goes out of
+          // scope; reuse the use-after-scope diagnostic at the capturing
+          // method.
+          SemaHelper->reportUseAfterScope(
+              IssueExpr, ThisEscape->getLoc(), MovedExpr, ExpiryLoc,
+              getExprChain(
+                  LoanPropagation.buildOriginFlowChain(OEF, LID, Cfg)));
+        else
           llvm_unreachable("Unhandled OriginEscapesFact type");
       } else
         llvm_unreachable("Unhandled CausingFact type");

diff  --git a/clang/lib/Analysis/LifetimeSafety/Facts.cpp b/clang/lib/Analysis/LifetimeSafety/Facts.cpp
index 2d3c161ae8f113..9d3428b59b769c 100644
--- a/clang/lib/Analysis/LifetimeSafety/Facts.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Facts.cpp
@@ -155,6 +155,14 @@ void GlobalEscapeFact::dump(llvm::raw_ostream &OS, const LoanManager &,
   OS << ", via Global)\n";
 }
 
+void ThisEscapeFact::dump(llvm::raw_ostream &OS, const LoanManager &,
+                          const OriginManager &OM,
+                          const LoanPropagationAnalysis *) const {
+  OS << "OriginEscapes (";
+  OM.dump(getEscapedOriginID(), OS);
+  OS << ", via This)\n";
+}
+
 void UseFact::dump(llvm::raw_ostream &OS, const LoanManager &,
                    const OriginManager &OM,
                    const LoanPropagationAnalysis *) const {

diff  --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index 6a805bb7be2d86..f222ddfb277a8f 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -858,6 +858,17 @@ void FactsGenerator::handleExitBlock() {
             FactMgr.createFact<GlobalEscapeFact>(O.ID, VD));
       }
     }
+
+  // Keep the never-expiring `this` origin live at exit: the object outlives
+  // the method, so whatever it holds (e.g. borrows captured via
+  // [[clang::lifetime_capture_by_this]], or loans reachable through its
+  // fields) is considered used after the method returns. This is emitted for
+  // every method with a `this` origin, not only those that capture something.
+  // Destructors are skipped since the object is being destroyed.
+  if (auto ThisOrigins = FactMgr.getOriginMgr().getThisOrigins();
+      ThisOrigins && !IsDestructor)
+    EscapesInCurrentBlock.push_back(FactMgr.createFact<ThisEscapeFact>(
+        (*ThisOrigins)->getOuterOriginID(), AC.getDecl()->getEndLoc()));
 }
 
 void FactsGenerator::handleGSLPointerConstruction(const CXXConstructExpr *CCE) {

diff  --git a/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp b/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp
index 7f827a28657823..cc9ee66ab0f868 100644
--- a/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp
@@ -69,6 +69,8 @@ static SourceLocation GetFactLoc(CausingFactType F) {
       return FieldEsc->getFieldDecl()->getLocation();
     if (auto *GlobalEsc = dyn_cast<GlobalEscapeFact>(OEF))
       return GlobalEsc->getGlobal()->getLocation();
+    if (auto *ThisEsc = dyn_cast<ThisEscapeFact>(OEF))
+      return ThisEsc->getLoc();
   }
   llvm_unreachable("unhandled causing fact in PointerUnion");
 }

diff  --git a/clang/lib/Sema/SemaLifetimeSafety.h b/clang/lib/Sema/SemaLifetimeSafety.h
index 3db298b6fec85c..39a6c545b49b07 100644
--- a/clang/lib/Sema/SemaLifetimeSafety.h
+++ b/clang/lib/Sema/SemaLifetimeSafety.h
@@ -137,7 +137,7 @@ inline LifetimeSafetyOpts GetLifetimeSafetyOpts(Sema &S, const Decl *D) {
   return LSOpts;
 }
 
-class LifetimeSafetySemaHelperImpl : public LifetimeSafetySemaHelper {
+class LifetimeSafetySemaHelperImpl final : public LifetimeSafetySemaHelper {
 
 public:
   LifetimeSafetySemaHelperImpl(Sema &S) : S(S) {}
@@ -164,6 +164,27 @@ class LifetimeSafetySemaHelperImpl : public LifetimeSafetySemaHelper {
         << UseExpr->getSourceRange();
   }
 
+  void reportUseAfterScope(const Expr *IssueExpr, SourceLocation UseLoc,
+                           const Expr *MovedExpr, SourceLocation FreeLoc,
+                           llvm::ArrayRef<const Expr *> ExprChain) override {
+    unsigned DiagID = MovedExpr
+                          ? diag::warn_lifetime_safety_use_after_scope_moved
+                          : diag::warn_lifetime_safety_use_after_scope;
+    std::string DestroyedSubject = getDiagSubjectDescription(IssueExpr);
+
+    S.Diag(IssueExpr->getExprLoc(), DiagID)
+        << DestroyedSubject << IssueExpr->getSourceRange();
+    if (MovedExpr)
+      S.Diag(MovedExpr->getExprLoc(), diag::note_lifetime_safety_moved_here)
+          << MovedExpr->getSourceRange();
+    S.Diag(FreeLoc, diag::note_lifetime_safety_destroyed_here)
+        << DestroyedSubject;
+
+    reportAliasingChain(ExprChain);
+
+    S.Diag(UseLoc, diag::note_lifetime_safety_used_here);
+  }
+
   void reportUseAfterReturn(const Expr *IssueExpr, const Expr *ReturnExpr,
                             const Expr *MovedExpr,
                             llvm::ArrayRef<const Expr *> ExprChain) override {
@@ -240,33 +261,37 @@ class LifetimeSafetySemaHelperImpl : public LifetimeSafetySemaHelper {
   reportUseAfterInvalidation(const Expr *IssueExpr, const Expr *UseExpr,
                              const Expr *InvalidationExpr,
                              llvm::ArrayRef<const Expr *> ExprChain) override {
-    auto WarnDiag = isa<CXXDeleteExpr>(InvalidationExpr)
-                        ? diag::warn_lifetime_safety_use_after_free
-                        : diag::warn_lifetime_safety_invalidation;
-    std::string InvalidatedSubject = getDiagSubjectDescription(IssueExpr);
-    S.Diag(IssueExpr->getExprLoc(), WarnDiag)
-        << InvalidatedSubject << IssueExpr->getSourceRange();
-    reportInvalidationSite(InvalidationExpr, InvalidatedSubject);
-    reportAliasingChain(ExprChain);
-    S.Diag(UseExpr->getExprLoc(), diag::note_lifetime_safety_used_here)
-        << UseExpr->getSourceRange();
+    reportUseAfterInvalidationImpl(
+        IssueExpr->getExprLoc(), IssueExpr->getSourceRange(),
+        getDiagSubjectDescription(IssueExpr), InvalidationExpr, ExprChain,
+        UseExpr->getExprLoc(), UseExpr->getSourceRange());
   }
   void
   reportUseAfterInvalidation(const ParmVarDecl *PVD, const Expr *UseExpr,
                              const Expr *InvalidationExpr,
                              llvm::ArrayRef<const Expr *> ExprChain) override {
-
-    auto WarnDiag = isa<CXXDeleteExpr>(InvalidationExpr)
-                        ? diag::warn_lifetime_safety_use_after_free
-                        : diag::warn_lifetime_safety_invalidation;
-    std::string InvalidatedSubject = getDiagSubjectDescription(PVD);
-
-    S.Diag(PVD->getSourceRange().getBegin(), WarnDiag)
-        << InvalidatedSubject << PVD->getSourceRange();
-    reportInvalidationSite(InvalidationExpr, InvalidatedSubject);
-    reportAliasingChain(ExprChain);
-    S.Diag(UseExpr->getExprLoc(), diag::note_lifetime_safety_used_here)
-        << UseExpr->getSourceRange();
+    reportUseAfterInvalidationImpl(
+        PVD->getSourceRange().getBegin(), PVD->getSourceRange(),
+        getDiagSubjectDescription(PVD), InvalidationExpr, ExprChain,
+        UseExpr->getExprLoc(), UseExpr->getSourceRange());
+  }
+  void
+  reportUseAfterInvalidation(const Expr *IssueExpr, SourceLocation UseLoc,
+                             const Expr *InvalidationExpr,
+                             llvm::ArrayRef<const Expr *> ExprChain) override {
+    reportUseAfterInvalidationImpl(
+        IssueExpr->getExprLoc(), IssueExpr->getSourceRange(),
+        getDiagSubjectDescription(IssueExpr), InvalidationExpr, ExprChain,
+        UseLoc, SourceRange());
+  }
+  void
+  reportUseAfterInvalidation(const ParmVarDecl *PVD, SourceLocation UseLoc,
+                             const Expr *InvalidationExpr,
+                             llvm::ArrayRef<const Expr *> ExprChain) override {
+    reportUseAfterInvalidationImpl(
+        PVD->getSourceRange().getBegin(), PVD->getSourceRange(),
+        getDiagSubjectDescription(PVD), InvalidationExpr, ExprChain, UseLoc,
+        SourceRange());
   }
 
   void reportInvalidatedField(const Expr *IssueExpr,
@@ -602,6 +627,22 @@ class LifetimeSafetySemaHelperImpl : public LifetimeSafetySemaHelper {
         << InvalidatedSubject << InvalidationExpr->getSourceRange();
   }
 
+  void reportUseAfterInvalidationImpl(SourceLocation IssueLoc,
+                                      SourceRange IssueRange,
+                                      StringRef InvalidatedSubject,
+                                      const Expr *InvalidationExpr,
+                                      llvm::ArrayRef<const Expr *> ExprChain,
+                                      SourceLocation UseLoc,
+                                      SourceRange UseRange) {
+    auto WarnDiag = isa<CXXDeleteExpr>(InvalidationExpr)
+                        ? diag::warn_lifetime_safety_use_after_free
+                        : diag::warn_lifetime_safety_invalidation;
+    S.Diag(IssueLoc, WarnDiag) << InvalidatedSubject << IssueRange;
+    reportInvalidationSite(InvalidationExpr, InvalidatedSubject);
+    reportAliasingChain(ExprChain);
+    S.Diag(UseLoc, diag::note_lifetime_safety_used_here) << UseRange;
+  }
+
   std::string getLifetimeBoundFixItText(SourceLocation Loc, bool LeadingSpace,
                                         bool AllowGNUAttrMacro = true) {
     const bool UseCXX11AttrSpelling =

diff  --git a/clang/test/Sema/LifetimeSafety/invalidations.cpp b/clang/test/Sema/LifetimeSafety/invalidations.cpp
index 4bed13b2cc844d..d9955ec42a2080 100644
--- a/clang/test/Sema/LifetimeSafety/invalidations.cpp
+++ b/clang/test/Sema/LifetimeSafety/invalidations.cpp
@@ -992,3 +992,20 @@ void invalid_after_ternary_reset(bool flag) {
 }
 
 } // namespace unique_ptr_invalidation
+
+namespace CapturedByThis {
+struct S {
+  void store(const std::string &s [[clang::lifetime_capture_by_this]]);
+
+  void InvalidatedParam(std::string &s) { // expected-warning {{parameter 's' is later invalidated}}
+    store(s);
+    s.clear(); // expected-note {{parameter 's' is invalidated here}}
+  }            // expected-note {{later used here}}
+
+  void InvalidatedLocal() {
+    std::string s;
+    store(s);  // expected-warning {{local variable 's' is later invalidated}}
+    s.clear(); // expected-note {{local variable 's' is invalidated here}}
+  }            // expected-note {{later used here}}
+};
+} // namespace CapturedByThis

diff  --git a/clang/test/Sema/LifetimeSafety/noescape-violation.cpp b/clang/test/Sema/LifetimeSafety/noescape-violation.cpp
index 9978850a92e51b..0ee36d240903b7 100644
--- a/clang/test/Sema/LifetimeSafety/noescape-violation.cpp
+++ b/clang/test/Sema/LifetimeSafety/noescape-violation.cpp
@@ -192,6 +192,18 @@ int* return_spaced_brackets(int* p [ [clang::noescape] /*some comment*/ ]) { //
   return p; // expected-note {{returned here}}
 }
 
+// FIXME: A [[clang::noescape]] parameter captured into the object via
+// [[clang::lifetime_capture_by_this]] escapes the function and should be
+// diagnosed. It currently is not: the escape is modeled as a ThisEscapeFact at
+// exit, which does not feed the noescape-violation machinery.
+struct CaptureByThisNoescape {
+  const int *p;
+  void store(const int &x [[clang::lifetime_capture_by_this]]);
+  void captures_noescape(const int &n [[clang::noescape]]) {
+    store(n); // FIXME-warning: parameter is marked [[clang::noescape]] but escapes
+  }
+};
+
 namespace callable_wrappers {
 
 std::function<void()> escape_noescape_via_function(int &x [[clang::noescape]]) { // expected-warning {{parameter is marked [[clang::noescape]] but escapes}}

diff  --git a/clang/test/Sema/LifetimeSafety/safety.cpp b/clang/test/Sema/LifetimeSafety/safety.cpp
index 18800830cb59f4..ada2ebf3e35171 100644
--- a/clang/test/Sema/LifetimeSafety/safety.cpp
+++ b/clang/test/Sema/LifetimeSafety/safety.cpp
@@ -4874,3 +4874,43 @@ GslOnlyDerived *DerivedAnnotatedMismatchDropsBorrow() {
   return static_cast<GslOnlyDerived *>(b); // no note
 }
 } // namespace DifferentOriginShapes
+
+const int &passthrough(const int &x [[clang::lifetimebound]]);
+
+// A borrow captured into the implicit object via
+// [[clang::lifetime_capture_by_this]] that outlives the captured local is
+// caught at the capturing method's exit.
+struct CaptureByThis {
+  int member;
+  const int *p;
+  void store(const int &x [[clang::lifetime_capture_by_this]]);
+  void captures_dangling_local() {
+    int local = 0;
+    store(local); // expected-warning {{local variable 'local' does not live long enough}}
+  }               // expected-note {{local variable 'local' is destroyed here}} expected-note {{later used here}}
+  void captures_through_alias_chain() {
+    int local = 0;
+    const int &r = passthrough(local); // expected-warning {{local variable 'local' does not live long enough}} \
+                                       // expected-note {{result of call to 'passthrough' aliases the storage of local variable 'local' because parameter 'x' is marked as lifetimebound}}
+    store(passthrough(r));             // expected-note {{result of call to 'passthrough' aliases the storage of local variable 'local'}}
+  } // expected-note {{local variable 'local' is destroyed here}} expected-note {{later used here}}
+  void captures_in_nested_scope() {
+    {
+      int local = 0;
+      store(local); // expected-warning {{local variable 'local' does not live long enough}}
+    }               // expected-note {{local variable 'local' is destroyed here}}
+  }                 // expected-note {{later used here}}
+  // Negative: capturing a caller-scoped reference into `this` does not dangle.
+  void captures_caller_ref(const int &caller_ref) {
+    store(caller_ref);
+  }
+  // Negative: capturing a member (same lifetime as the object) does not dangle.
+  void captures_member() {
+    store(member);
+  }
+  // Negative: the object is being destroyed, so nothing outlives the local.
+  ~CaptureByThis() {
+    int local = 0;
+    store(local);
+  }
+};


        


More information about the cfe-commits mailing list