[clang] [LifetimeSafety] Catch intra-method lifetime_capture_by(this) dangles (PR #204630)
Gábor Horváth via cfe-commits
cfe-commits at lists.llvm.org
Tue Jun 23 04:03:01 PDT 2026
https://github.com/Xazax-hun updated https://github.com/llvm/llvm-project/pull/204630
>From 91c77a073ce3520dac50f800d37f255d2c330c7f Mon Sep 17 00:00:00 2001
From: Gabor Horvath <gaborh at apple.com>
Date: Thu, 18 Jun 2026 17:28:18 +0100
Subject: [PATCH] [LifetimeSafety] Catch intra-method lifetime_capture_by(this)
dangles
A borrow captured into the object via [[clang::lifetime_capture_by(this)]]
flows into the never-expiring `this` origin, so when the capture and the
captured local's expiry both happen inside one method, `this` is not otherwise
live at the expiry and the dangle was missed. Add a CaptureEscapeFact, emitted
for `this` at function exit, so checkExpiry sees the captured local going out of
scope while still held by the object and reports it as use-after-scope.
Assisted-by: Claude Opus 4.8
---
.../Analysis/Analyses/LifetimeSafety/Facts.h | 29 +++++++++++++++++--
.../Analyses/LifetimeSafety/LifetimeSafety.h | 6 ++++
clang/lib/Analysis/LifetimeSafety/Checker.cpp | 9 ++++++
clang/lib/Analysis/LifetimeSafety/Facts.cpp | 7 +++++
.../LifetimeSafety/FactsGenerator.cpp | 7 +++++
.../Analysis/LifetimeSafety/LiveOrigins.cpp | 2 ++
clang/lib/Sema/SemaLifetimeSafety.h | 19 ++++++++++++
clang/test/Sema/LifetimeSafety/safety.cpp | 27 +++++++++++++++++
8 files changed, 103 insertions(+), 3 deletions(-)
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
index 88b509e1b94df..4db1c9471a968 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
@@ -162,9 +162,10 @@ class OriginEscapesFact : public Fact {
public:
/// The way an origin can escape the current scope.
enum class EscapeKind : uint8_t {
- Return, /// Escapes via return statement.
- Field, /// Escapes via assignment to a field.
- Global, /// Escapes via assignment to global storage.
+ Return, /// Escapes via return statement.
+ Field, /// Escapes via assignment to a field.
+ Global, /// Escapes via assignment to global storage.
+ Capture, /// Captured into the object via lifetime_capture_by(this).
} EscKind;
static bool classof(const Fact *F) {
@@ -234,6 +235,28 @@ class GlobalEscapeFact : public OriginEscapesFact {
const OriginManager &OM) const override;
};
+/// Represents that an origin is captured into the implicit object via
+/// [[clang::lifetime_capture_by(this)]]. The capture flows into the
+/// whole-object `this` origin (we do not know which member receives it), so
+/// this is emitted for `this` at function exit to keep it live: a borrow still
+/// held there spans to exit and dangles if it outlived the captured local.
+class CaptureEscapeFact : public OriginEscapesFact {
+ SourceLocation Loc;
+
+public:
+ CaptureEscapeFact(OriginID OID, SourceLocation Loc)
+ : OriginEscapesFact(OID, EscapeKind::Capture), Loc(Loc) {}
+
+ static bool classof(const Fact *F) {
+ return F->getKind() == Kind::OriginEscapes &&
+ static_cast<const OriginEscapesFact *>(F)->getEscapeKind() ==
+ EscapeKind::Capture;
+ }
+ SourceLocation getLoc() const { return Loc; }
+ void dump(llvm::raw_ostream &OS, const LoanManager &,
+ const OriginManager &OM) 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 28886b826f72f..afef67f59160b 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h
@@ -66,6 +66,12 @@ 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) {}
+
virtual void reportUseAfterReturn(const Expr *IssueExpr,
const Expr *ReturnExpr,
const Expr *MovedExpr) {}
diff --git a/clang/lib/Analysis/LifetimeSafety/Checker.cpp b/clang/lib/Analysis/LifetimeSafety/Checker.cpp
index c258a1dc3596c..b87d75cbf9aba 100644
--- a/clang/lib/Analysis/LifetimeSafety/Checker.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Checker.cpp
@@ -303,6 +303,8 @@ class LifetimeChecker {
Warning.InvalidatedByExpr);
} else if (isa<ReturnEscapeFact>(OEF)) {
// FIXME: Diagnose invalidated return escapes separately.
+ } else if (isa<CaptureEscapeFact>(OEF)) {
+ // FIXME: Diagnose a capture-held loan invalidated through `this`.
} else
llvm_unreachable("Unhandled OriginEscapesFact type");
} else if (const auto *RetEscape = dyn_cast<ReturnEscapeFact>(OEF))
@@ -317,6 +319,13 @@ class LifetimeChecker {
// Global escape.
SemaHelper->reportDanglingGlobal(IssueExpr, GlobalEscape->getGlobal(),
MovedExpr, ExpiryLoc);
+ else if (const auto *CaptureEscape = dyn_cast<CaptureEscapeFact>(OEF))
+ // A borrow is still held by the object (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, CaptureEscape->getLoc(),
+ MovedExpr, ExpiryLoc);
else
llvm_unreachable("Unhandled OriginEscapesFact type");
} else
diff --git a/clang/lib/Analysis/LifetimeSafety/Facts.cpp b/clang/lib/Analysis/LifetimeSafety/Facts.cpp
index 3d7fbcdacc830..0e37851e72743 100644
--- a/clang/lib/Analysis/LifetimeSafety/Facts.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Facts.cpp
@@ -77,6 +77,13 @@ void GlobalEscapeFact::dump(llvm::raw_ostream &OS, const LoanManager &,
OS << ", via Global)\n";
}
+void CaptureEscapeFact::dump(llvm::raw_ostream &OS, const LoanManager &,
+ const OriginManager &OM) const {
+ OS << "OriginEscapes (";
+ OM.dump(getEscapedOriginID(), OS);
+ OS << ", via Capture)\n";
+}
+
void UseFact::dump(llvm::raw_ostream &OS, const LoanManager &,
const OriginManager &OM) const {
OS << "Use (";
diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index 3861117005752..698e7739958fa 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -828,6 +828,13 @@ void FactsGenerator::handleExitBlock() {
FactMgr.createFact<GlobalEscapeFact>(O.ID, VD));
}
}
+
+ // A borrow captured via [[clang::lifetime_capture_by(this)]] flows into the
+ // never-expiring `this` origin, so it is not otherwise live at the captured
+ // local's expiry. Keep `this` live at exit so the dangle is caught.
+ if (auto ThisOrigins = FactMgr.getOriginMgr().getThisOrigins())
+ EscapesInCurrentBlock.push_back(FactMgr.createFact<CaptureEscapeFact>(
+ (*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 cfbcacf04b1b0..de0e082a117b9 100644
--- a/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp
@@ -64,6 +64,8 @@ static SourceLocation GetFactLoc(CausingFactType F) {
return FieldEsc->getFieldDecl()->getLocation();
if (auto *GlobalEsc = dyn_cast<GlobalEscapeFact>(OEF))
return GlobalEsc->getGlobal()->getLocation();
+ if (auto *CaptureEsc = dyn_cast<CaptureEscapeFact>(OEF))
+ return CaptureEsc->getLoc();
}
llvm_unreachable("unhandled causing fact in PointerUnion");
}
diff --git a/clang/lib/Sema/SemaLifetimeSafety.h b/clang/lib/Sema/SemaLifetimeSafety.h
index 1047aecf863fb..6c83e255f89ae 100644
--- a/clang/lib/Sema/SemaLifetimeSafety.h
+++ b/clang/lib/Sema/SemaLifetimeSafety.h
@@ -103,6 +103,25 @@ class LifetimeSafetySemaHelperImpl : public LifetimeSafetySemaHelper {
<< UseExpr->getSourceRange();
}
+ void reportUseAfterScope(const Expr *IssueExpr, SourceLocation UseLoc,
+ const Expr *MovedExpr,
+ SourceLocation FreeLoc) 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;
+
+ S.Diag(UseLoc, diag::note_lifetime_safety_used_here);
+ }
+
void reportUseAfterReturn(const Expr *IssueExpr, const Expr *ReturnExpr,
const Expr *MovedExpr) override {
unsigned DiagID = MovedExpr
diff --git a/clang/test/Sema/LifetimeSafety/safety.cpp b/clang/test/Sema/LifetimeSafety/safety.cpp
index abd3d9c61b784..abac5dc4ef3c3 100644
--- a/clang/test/Sema/LifetimeSafety/safety.cpp
+++ b/clang/test/Sema/LifetimeSafety/safety.cpp
@@ -3895,3 +3895,30 @@ struct [[gsl::Pointer()]] PtrWithInt { int x; };
PtrWithInt f() {
return PtrWithInt{10};
}
+
+// 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_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);
+ }
+};
More information about the cfe-commits
mailing list