[clang] [LifetimeSafety] Model scope-exit destructor and cleanup callback as a use (PR #204650)
Gábor Horváth via cfe-commits
cfe-commits at lists.llvm.org
Mon Jun 22 08:59:38 PDT 2026
https://github.com/Xazax-hun updated https://github.com/llvm/llvm-project/pull/204650
>From 716bfe1d63988b64b3a9a83adbe8443168cfda27 Mon Sep 17 00:00:00 2001
From: Gabor Horvath <gaborh at apple.com>
Date: Thu, 18 Jun 2026 18:39:11 +0100
Subject: [PATCH] [LifetimeSafety] Model scope-exit destructor and cleanup
callback as a use
A non-trivial destructor or __attribute__((cleanup(fn))) callback running at
scope exit may read a borrow the object holds (e.g. a [[gsl::Pointer]] whose
out-of-line ~T() or cleanup function dereferences its captured view). The
analysis never sees those bodies, so model the action as a use of the object,
keeping the borrow live to that point: a borrowed-from object destroyed earlier
(reverse-declaration order) is now reported instead of silently dangling.
The implicit use has no source expression, so UseFact gains a SourceLocation
anchor and reportUseAfterScope/reportUseAfterInvalidation gain SourceLocation
overloads for the "used here" note. Owners are excluded from the destructor
case (their destruction frees their own storage, already modeled by the
ExpireFact).
Assisted-by: Claude Opus 4.8
---
.../Analysis/Analyses/LifetimeSafety/Facts.h | 14 +++++
.../Analyses/LifetimeSafety/FactsGenerator.h | 2 +
.../Analyses/LifetimeSafety/LifetimeSafety.h | 14 +++++
clang/lib/Analysis/LifetimeSafety/Checker.cpp | 20 +++++-
.../LifetimeSafety/FactsGenerator.cpp | 33 ++++++++++
.../Analysis/LifetimeSafety/LiveOrigins.cpp | 2 +-
clang/lib/Sema/SemaLifetimeSafety.h | 47 ++++++++++++++
.../Sema/LifetimeSafety/invalidations.cpp | 50 +++++++++++++++
clang/test/Sema/LifetimeSafety/safety.cpp | 62 +++++++++++++++++++
9 files changed, 241 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..612f726a1a206 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
@@ -240,16 +240,30 @@ class UseFact : public Fact {
// True if this use is a write operation (e.g., left-hand side of assignment).
// Write operations are exempted from use-after-free checks.
bool IsWritten = false;
+ // For an implicit use with no source expression (a scope-exit destructor or
+ // cleanup callback reading a borrow): the location to anchor diagnostics at.
+ SourceLocation ImplicitLoc;
public:
static bool classof(const Fact *F) { return F->getKind() == Kind::Use; }
UseFact(const Expr *UseExpr, const OriginList *OList)
: Fact(Kind::Use), UseExpr(UseExpr), OList(OList) {}
+ UseFact(SourceLocation ImplicitLoc, const OriginList *OList)
+ : Fact(Kind::Use), UseExpr(nullptr), OList(OList),
+ ImplicitLoc(ImplicitLoc) {}
const OriginList *getUsedOrigins() const { return OList; }
void setUsedOrigins(const OriginList *NewList) { OList = NewList; }
const Expr *getUseExpr() const { return UseExpr; }
+ /// True if this use has no source expression; use getImplicitLoc() instead.
+ bool isImplicit() const { return UseExpr == nullptr; }
+ SourceLocation getImplicitLoc() const { return ImplicitLoc; }
+ /// The location to anchor diagnostics at: the use expression's location, or
+ /// the explicit anchor location for an implicit use (no source expression).
+ SourceLocation getUseLoc() const {
+ return UseExpr ? UseExpr->getExprLoc() : ImplicitLoc;
+ }
void markAsWritten() { IsWritten = true; }
bool isWritten() const { return IsWritten; }
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
index 5ac67263681ac..c923b99dfc743 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
@@ -84,6 +84,8 @@ class FactsGenerator : public ConstStmtVisitor<FactsGenerator> {
void handleLifetimeEnds(const CFGLifetimeEnds &LifetimeEnds);
+ void handleCleanupFunction(const CFGCleanupFunction &CleanupFunction);
+
void handleFullExprCleanup(const CFGFullExprCleanup &FullExprCleanup);
void handleExitBlock();
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h
index 28886b826f72f..ec207be0823ff 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h
@@ -65,6 +65,12 @@ class LifetimeSafetySemaHelper {
const Expr *MovedExpr,
SourceLocation FreeLoc,
llvm::ArrayRef<const Expr *> ExprChain) {}
+ // Variant for an implicit use with no source expression; `UseLoc` anchors the
+ // "used here" note.
+ virtual void reportUseAfterScope(const Expr *IssueExpr, SourceLocation UseLoc,
+ const Expr *MovedExpr,
+ SourceLocation FreeLoc,
+ llvm::ArrayRef<const Expr *> ExprChain) {}
virtual void reportUseAfterReturn(const Expr *IssueExpr,
const Expr *ReturnExpr,
@@ -88,6 +94,14 @@ class LifetimeSafetySemaHelper {
virtual void reportUseAfterInvalidation(const ParmVarDecl *PVD,
const Expr *UseExpr,
const Expr *InvalidationExpr) {}
+ // Variants for an implicit use with no source expression; `UseLoc` anchors
+ // the "used here" note.
+ virtual void reportUseAfterInvalidation(const Expr *IssueExpr,
+ SourceLocation UseLoc,
+ const Expr *InvalidationExpr) {}
+ virtual void reportUseAfterInvalidation(const ParmVarDecl *PVD,
+ SourceLocation UseLoc,
+ const Expr *InvalidationExpr) {}
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 d41d6f43f837b..269d898de0f85 100644
--- a/clang/lib/Analysis/LifetimeSafety/Checker.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Checker.cpp
@@ -72,7 +72,7 @@ class LifetimeChecker {
static SourceLocation
GetFactLoc(llvm::PointerUnion<const UseFact *, const OriginEscapesFact *> F) {
if (const auto *UF = F.dyn_cast<const UseFact *>())
- return UF->getUseExpr()->getExprLoc();
+ return UF->getUseLoc();
if (const auto *OEF = F.dyn_cast<const OriginEscapesFact *>()) {
if (auto *ReturnEsc = dyn_cast<ReturnEscapeFact>(OEF))
return ReturnEsc->getReturnExpr()->getExprLoc();
@@ -254,7 +254,23 @@ class LifetimeChecker {
SourceLocation ExpiryLoc = Warning.ExpiryLoc;
if (const auto *UF = CausingFact.dyn_cast<const UseFact *>()) {
- if (Warning.InvalidatedByExpr) {
+ // An implicit use has no source expression; anchor diagnostics at its
+ // location.
+ if (UF->isImplicit()) {
+ if (Warning.InvalidatedByExpr) {
+ if (IssueExpr)
+ SemaHelper->reportUseAfterInvalidation(
+ IssueExpr, UF->getImplicitLoc(), Warning.InvalidatedByExpr);
+ else if (InvalidatedPVD)
+ SemaHelper->reportUseAfterInvalidation(InvalidatedPVD,
+ UF->getImplicitLoc(),
+ Warning.InvalidatedByExpr);
+ } else {
+ SemaHelper->reportUseAfterScope(
+ IssueExpr, UF->getImplicitLoc(), MovedExpr, ExpiryLoc,
+ getExprChain(LoanPropagation.buildOriginFlowChain(UF, LID)));
+ }
+ } else if (Warning.InvalidatedByExpr) {
if (IssueExpr)
// Use-after-invalidation of an object on stack.
SemaHelper->reportUseAfterInvalidation(IssueExpr, UF->getUseExpr(),
diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index 3861117005752..d3f5a3d252b33 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -125,6 +125,9 @@ void FactsGenerator::run() {
else if (std::optional<CFGLifetimeEnds> LifetimeEnds =
Element.getAs<CFGLifetimeEnds>())
handleLifetimeEnds(*LifetimeEnds);
+ else if (std::optional<CFGCleanupFunction> CleanupFunction =
+ Element.getAs<CFGCleanupFunction>())
+ handleCleanupFunction(*CleanupFunction);
else if (std::optional<CFGFullExprCleanup> FullExprCleanup =
Element.getAs<CFGFullExprCleanup>()) {
handleFullExprCleanup(*FullExprCleanup);
@@ -792,6 +795,20 @@ void FactsGenerator::handleLifetimeEnds(const CFGLifetimeEnds &LifetimeEnds) {
const VarDecl *LifetimeEndsVD = LifetimeEnds.getVarDecl();
if (!LifetimeEndsVD)
return;
+ // A non-trivial destructor at scope exit may read a borrow the object holds
+ // (e.g. a [[gsl::Pointer]] whose out-of-line ~T() dereferences its view). The
+ // analysis never sees that body, so model the destruction as a use, keeping
+ // the borrow live to that point so a borrowed-from object destroyed earlier
+ // (reverse-declaration order) is reported. Owners are excluded: their
+ // destruction frees their own storage (modeled by the ExpireFact), not a
+ // borrow into another object.
+ QualType VDTy = LifetimeEndsVD->getType();
+ if (const CXXRecordDecl *RD = VDTy->getAsCXXRecordDecl();
+ RD && RD->hasDefinition() && RD->hasNonTrivialDestructor() &&
+ !isGslOwnerType(VDTy) && hasOrigins(VDTy))
+ if (OriginList *List = getOriginsList(*LifetimeEndsVD))
+ CurrentBlockFacts.push_back(FactMgr.createFact<UseFact>(
+ LifetimeEnds.getTriggerStmt()->getEndLoc(), List));
// Expire the origin when its variable's lifetime ends to ensure liveness
// doesn't persist through loop back-edges.
std::optional<OriginID> ExpiredOID;
@@ -807,6 +824,22 @@ void FactsGenerator::handleLifetimeEnds(const CFGLifetimeEnds &LifetimeEnds) {
ExpiredOID));
}
+void FactsGenerator::handleCleanupFunction(
+ const CFGCleanupFunction &CleanupFunction) {
+ // A variable with __attribute__((cleanup(fn))) has fn(&var) called at scope
+ // exit; like a non-trivial destructor, that callback may read a borrow the
+ // variable holds, so model it as a use of the variable. Unlike the destructor
+ // case, owners are not excluded: the cleanup callback receives the variable's
+ // address explicitly and may read whatever it holds, regardless of whether
+ // the variable is a gsl::Owner.
+ const VarDecl *VD = CleanupFunction.getVarDecl();
+ if (!VD || !hasOrigins(VD->getType()))
+ return;
+ if (OriginList *List = getOriginsList(*VD))
+ CurrentBlockFacts.push_back(
+ FactMgr.createFact<UseFact>(VD->getLocation(), List));
+}
+
void FactsGenerator::handleFullExprCleanup(
const CFGFullExprCleanup &FullExprCleanup) {
for (const auto *MTE : FullExprCleanup.getExpiringMTEs())
diff --git a/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp b/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp
index cfbcacf04b1b0..009dbf5d352ad 100644
--- a/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp
@@ -56,7 +56,7 @@ struct Lattice {
static SourceLocation GetFactLoc(CausingFactType F) {
if (const auto *UF = F.dyn_cast<const UseFact *>())
- return UF->getUseExpr()->getExprLoc();
+ return UF->getUseLoc();
if (const auto *OEF = F.dyn_cast<const OriginEscapesFact *>()) {
if (auto *ReturnEsc = dyn_cast<ReturnEscapeFact>(OEF))
return ReturnEsc->getReturnExpr()->getExprLoc();
diff --git a/clang/lib/Sema/SemaLifetimeSafety.h b/clang/lib/Sema/SemaLifetimeSafety.h
index 4bde272fb40a1..370adde199a1d 100644
--- a/clang/lib/Sema/SemaLifetimeSafety.h
+++ b/clang/lib/Sema/SemaLifetimeSafety.h
@@ -101,6 +101,25 @@ 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;
+
+ S.Diag(IssueExpr->getExprLoc(), DiagID)
+ << getDiagSubjectDescription(IssueExpr) << 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);
+
+ reportAliasingChain(ExprChain);
+
+ S.Diag(UseLoc, diag::note_lifetime_safety_used_here);
+ }
+
void reportUseAfterReturn(const Expr *IssueExpr, const Expr *ReturnExpr,
const Expr *MovedExpr) override {
unsigned DiagID = MovedExpr
@@ -194,6 +213,34 @@ class LifetimeSafetySemaHelperImpl : public LifetimeSafetySemaHelper {
S.Diag(UseExpr->getExprLoc(), diag::note_lifetime_safety_used_here)
<< UseExpr->getSourceRange();
}
+ void reportUseAfterInvalidation(const Expr *IssueExpr, SourceLocation UseLoc,
+ const Expr *InvalidationExpr) override {
+ auto WarnDiag = isa<CXXDeleteExpr>(InvalidationExpr)
+ ? diag::warn_lifetime_safety_use_after_free
+ : diag::warn_lifetime_safety_invalidation;
+ auto UseDiag = isa<CXXDeleteExpr>(InvalidationExpr)
+ ? diag::note_lifetime_safety_freed_here
+ : diag::note_lifetime_safety_invalidated_here;
+ S.Diag(IssueExpr->getExprLoc(), WarnDiag)
+ << getDiagSubjectDescription(IssueExpr) << IssueExpr->getSourceRange();
+ S.Diag(InvalidationExpr->getExprLoc(), UseDiag)
+ << InvalidationExpr->getSourceRange();
+ S.Diag(UseLoc, diag::note_lifetime_safety_used_here);
+ }
+ void reportUseAfterInvalidation(const ParmVarDecl *PVD, SourceLocation UseLoc,
+ const Expr *InvalidationExpr) override {
+ auto WarnDiag = isa<CXXDeleteExpr>(InvalidationExpr)
+ ? diag::warn_lifetime_safety_use_after_free
+ : diag::warn_lifetime_safety_invalidation;
+ auto UseDiag = isa<CXXDeleteExpr>(InvalidationExpr)
+ ? diag::note_lifetime_safety_freed_here
+ : diag::note_lifetime_safety_invalidated_here;
+ S.Diag(PVD->getSourceRange().getBegin(), WarnDiag)
+ << getDiagSubjectDescription(PVD) << PVD->getSourceRange();
+ S.Diag(InvalidationExpr->getExprLoc(), UseDiag)
+ << InvalidationExpr->getSourceRange();
+ S.Diag(UseLoc, diag::note_lifetime_safety_used_here);
+ }
void reportInvalidatedField(const Expr *IssueExpr,
const FieldDecl *DanglingField,
diff --git a/clang/test/Sema/LifetimeSafety/invalidations.cpp b/clang/test/Sema/LifetimeSafety/invalidations.cpp
index 301822f066de8..7add7e39906f3 100644
--- a/clang/test/Sema/LifetimeSafety/invalidations.cpp
+++ b/clang/test/Sema/LifetimeSafety/invalidations.cpp
@@ -920,3 +920,53 @@ void invalid_after_ternary_reset(bool flag) {
}
} // namespace unique_ptr_invalidation
+
+// A non-trivial destructor at scope exit is modeled as an implicit use (a
+// UseFact with no source expression). The live-origins join helper reads such a
+// fact's explicit location rather than dereferencing its null use-expression, so
+// a function with both a tracked borrow and such an implicit use (e.g. a
+// std::function local) is analyzed without crashing.
+namespace implicit_use_join {
+void view_and_callable() {
+ std::string text = "long enough heap-allocated backing string value here!";
+ std::string_view tok = text; // a tracked borrow (live origin)
+ std::function<void()> c = [] {}; // non-trivial dtor at scope exit
+ (void)c;
+ (void)tok.size(); // no-warning (must not crash)
+}
+
+void view_and_callable_mutation() {
+ std::string text = "long enough heap-allocated backing string value here!";
+ std::string_view tok = text; // expected-warning {{local variable 'text' is later invalidated}}
+ std::function<void()> c = [] {};
+ (void)c;
+ text.push_back('x'); // expected-note {{invalidated here}}
+ (void)tok.size(); // expected-note {{later used here}}
+}
+} // namespace implicit_use_join
+
+// A borrow read by a scope-exit destructor or cleanup callback (an implicit use
+// with no source expression) that was freed earlier via `delete` is a
+// use-after-free; the diagnostic is anchored at the implicit use's location.
+namespace implicit_use_after_free {
+struct [[gsl::Pointer]] Ref {
+ const int *p;
+ Ref &operator=(const int *q [[clang::lifetime_capture_by(this)]]);
+ ~Ref(); // non-trivial, out-of-line: may read p
+};
+void cleanup_ref(Ref *r); // out-of-line: may read r->p
+
+void via_destructor() {
+ Ref r;
+ int *h = new int(7); // expected-warning {{allocated object does not live long enough}}
+ r = h;
+ delete h; // expected-note {{freed here}}
+} // expected-note {{later used here}}
+
+void via_cleanup() {
+ Ref g __attribute__((cleanup(cleanup_ref))); // expected-note {{later used here}}
+ int *h = new int(7); // expected-warning {{allocated object does not live long enough}}
+ g = h;
+ delete h; // expected-note {{freed here}}
+}
+} // namespace implicit_use_after_free
diff --git a/clang/test/Sema/LifetimeSafety/safety.cpp b/clang/test/Sema/LifetimeSafety/safety.cpp
index 65bfe69e854ac..9ae84db8c2018 100644
--- a/clang/test/Sema/LifetimeSafety/safety.cpp
+++ b/clang/test/Sema/LifetimeSafety/safety.cpp
@@ -3894,3 +3894,65 @@ struct [[gsl::Pointer()]] PtrWithInt { int x; };
PtrWithInt f() {
return PtrWithInt{10};
}
+
+// A scope-exit destructor or cleanup callback may read a borrow the object
+// holds. The analysis never sees the out-of-line body, so it is modeled as a
+// use of the object: a borrowed-from object destroyed earlier (reverse-
+// declaration order) is reported.
+namespace ScopeExitUse {
+struct [[gsl::Pointer]] Ref {
+ std::string_view sv;
+ Ref() = default;
+ Ref &operator=(std::string_view s [[clang::lifetime_capture_by(this)]]);
+ ~Ref(); // non-trivial, out-of-line: may read sv
+};
+
+void dtor_reverse_order() {
+ Ref r; // destroyed LAST
+ std::string backing; // destroyed FIRST
+ r = backing; // expected-warning {{local variable 'backing' does not live long enough}}
+} // expected-note {{destroyed here}} expected-note {{later used here}}
+
+void dtor_safe_order() {
+ std::string backing;
+ Ref r;
+ r = backing; // no-warning
+}
+
+struct [[gsl::Pointer]] TrivialRef {
+ std::string_view sv;
+ TrivialRef &operator=(std::string_view s [[clang::lifetime_capture_by(this)]]);
+ // trivial destructor cannot read sv
+};
+void trivial_dtor_no_use() {
+ TrivialRef r;
+ std::string backing;
+ r = backing; // no-warning
+}
+
+void cleanup_ref(Ref *r); // out-of-line: may read r->sv
+void cleanup_reverse_order() {
+ Ref g __attribute__((cleanup(cleanup_ref))); // cleaned up LAST // expected-note {{later used here}}
+ std::string backing; // destroyed FIRST
+ g = backing; // expected-warning {{local variable 'backing' does not live long enough}}
+} // expected-note {{destroyed here}}
+
+void cleanup_safe_order() {
+ std::string backing;
+ Ref g __attribute__((cleanup(cleanup_ref)));
+ g = backing; // no-warning
+}
+
+// The destructor-as-use is not limited to gsl::Pointer: any origin-holding type
+// with a non-trivial destructor qualifies. A lambda capturing a Ref by value is
+// not itself a gsl::Pointer, but holds the Ref's origin and has a non-trivial
+// destructor -- so ~closure runs ~Ref(), which reads the captured borrow.
+void closure_dtor_reads_captured_borrow() {
+ std::function<void()> fn = [] {}; // destroyed LAST
+ std::string backing; // destroyed FIRST
+ Ref r;
+ r = backing; // expected-warning {{local variable 'backing' does not live long enough}}
+ fn = [r] {}; // expected-note {{local variable 'r' aliases the storage of local variable 'backing'}}
+} // expected-note {{destroyed here}} expected-note {{later used here}}
+} // namespace ScopeExitUse
+
More information about the cfe-commits
mailing list