https://github.com/aeft updated https://github.com/llvm/llvm-project/pull/191123
>From a1778114f5e7214562e5c7832f255f968c9fda99 Mon Sep 17 00:00:00 2001 From: Zhijie Wang <[email protected]> Date: Thu, 9 Apr 2026 00:02:22 -0700 Subject: [PATCH 01/12] [LifetimeSafety] Track origins through std::function --- .../LifetimeSafety/LifetimeAnnotations.h | 4 ++ .../LifetimeSafety/FactsGenerator.cpp | 26 ++++++++- .../LifetimeSafety/LifetimeAnnotations.cpp | 8 +++ clang/lib/Analysis/LifetimeSafety/Origins.cpp | 15 +++-- clang/test/Sema/Inputs/lifetime-analysis.h | 10 ++++ clang/test/Sema/warn-lifetime-safety.cpp | 57 +++++++++++++++++++ 6 files changed, 109 insertions(+), 11 deletions(-) diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h index aa9ae4b2a5e6a..098c15f4a7fb4 100644 --- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h +++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h @@ -80,6 +80,10 @@ bool isUniquePtrRelease(const CXXMethodDecl &MD); // https://en.cppreference.com/w/cpp/container#Iterator_invalidation bool isContainerInvalidationMethod(const CXXMethodDecl &MD); +/// Returns true for standard library callable wrappers (e.g., std::function) +/// that can propagate the stored lambda's origins. +bool isStdCallableWrapperType(const CXXRecordDecl *RD); + } // namespace clang::lifetimes #endif // LLVM_CLANG_ANALYSIS_ANALYSES_LIFETIMEANNOTATIONS_H diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp index 2a2ef88987286..8f86f35ca2f34 100644 --- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp +++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp @@ -190,9 +190,8 @@ void FactsGenerator::VisitCXXConstructExpr(const CXXConstructExpr *CCE) { return; } // For defaulted (implicit or `= default`) copy/move constructors, propagate - // origins directly. User-defined copy/move constructors have opaque semantics - // and fall through to `handleFunctionCall`, where [[clang::lifetimebound]] is - // needed to propagate origins. + // origins directly. User-defined copy/move constructors are not handled here + // as they have opaque semantics. if (CCE->getConstructor()->isCopyOrMoveConstructor() && CCE->getConstructor()->isDefaulted() && CCE->getNumArgs() == 1 && hasOrigins(CCE->getType())) { @@ -202,6 +201,16 @@ void FactsGenerator::VisitCXXConstructExpr(const CXXConstructExpr *CCE) { return; } } + // Standard library callable wrappers (e.g., std::function) propagate the + // stored lambda's origins. + if (const auto *RD = CCE->getType()->getAsCXXRecordDecl(); + RD && isStdCallableWrapperType(RD) && 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); @@ -400,6 +409,10 @@ void FactsGenerator::handleAssignment(const Expr *LHSExpr, } else markUseAsWrite(DRE_LHS); } + // RHS may not have tracked origins (e.g., assigning a non-lambda functor + // to a std::function). Skip the flow in that case. + if (!RHSList) + return; // Kill the old loans of the destination origin and flow the new loans // from the source origin. flow(LHSList->peelOuterOrigin(), RHSList, /*Kill=*/true); @@ -493,6 +506,13 @@ void FactsGenerator::VisitCXXOperatorCallExpr(const CXXOperatorCallExpr *OCE) { handleAssignment(OCE->getArg(0), OCE->getArg(1)); return; } + // Standard library callable wrappers (e.g., std::function) can propagate + // the stored lambda's origins. + if (const auto *RD = LHSTy->getAsCXXRecordDecl(); + RD && isStdCallableWrapperType(RD)) { + handleAssignment(OCE->getArg(0), OCE->getArg(1)); + return; + } // Other tracked types: only defaulted operator= propagates origins. // User-defined operator= has opaque semantics, so don't handle them now. if (const auto *MD = diff --git a/clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp b/clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp index 4852f444a51b3..27d95821dd0b4 100644 --- a/clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp +++ b/clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp @@ -382,4 +382,12 @@ bool isContainerInvalidationMethod(const CXXMethodDecl &MD) { return InvalidatingMethods->contains(MD.getName()); } + +bool isStdCallableWrapperType(const CXXRecordDecl *RD) { + if (!RD || !isInStlNamespace(RD)) + return false; + StringRef Name = getName(*RD); + return Name == "function" || Name == "move_only_function"; +} + } // namespace clang::lifetimes diff --git a/clang/lib/Analysis/LifetimeSafety/Origins.cpp b/clang/lib/Analysis/LifetimeSafety/Origins.cpp index fdd2671dee2e0..cda537bf98f75 100644 --- a/clang/lib/Analysis/LifetimeSafety/Origins.cpp +++ b/clang/lib/Analysis/LifetimeSafety/Origins.cpp @@ -107,14 +107,13 @@ bool OriginManager::hasOrigins(QualType QT) const { 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; + // Standard library callable wrappers (e.g., std::function) can propagate the + // stored lambda's origins. + if (isStdCallableWrapperType(RD)) + return true; + // TODO: Extend origin tracking to user-defined structs that may carry + // origins. + return RD->isLambda(); } /// Determines if an expression has origins that need to be tracked. diff --git a/clang/test/Sema/Inputs/lifetime-analysis.h b/clang/test/Sema/Inputs/lifetime-analysis.h index d1e847d20cc50..58ba1aa0c03c3 100644 --- a/clang/test/Sema/Inputs/lifetime-analysis.h +++ b/clang/test/Sema/Inputs/lifetime-analysis.h @@ -268,4 +268,14 @@ struct true_type { template<class T> struct is_pointer : false_type {}; template<class T> struct is_pointer<T*> : true_type {}; template<class T> struct is_pointer<T* const> : true_type {}; + +template<class> class function; +template<class R, class... Args> +class function<R(Args...)> { +public: + template<class F> function(F) {} + template<class F> function& operator=(F) { return *this; } + ~function(); +}; + } diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp index 2aca44daeb0aa..8e1f1b395875e 100644 --- a/clang/test/Sema/warn-lifetime-safety.cpp +++ b/clang/test/Sema/warn-lifetime-safety.cpp @@ -2552,3 +2552,60 @@ struct Y : X { } }; } // namespace base_class_fields + +namespace callable_wrappers { + +std::function<void()> direct_return() { + int x; + return [&x]() { (void)x; }; // expected-warning {{address of stack memory is returned later}} \ + // expected-note {{returned here}} +} + +std::function<void()> copy_function() { + int x; + std::function<void()> f = [&x]() { (void)x; }; // expected-warning {{address of stack memory is returned later}} + std::function<void()> f2 = f; + return f2; // expected-note {{returned here}} +} + +std::function<void()> reassign_safe_then_unsafe() { + static int safe = 1; + int local = 2; + std::function<void()> f = []() { (void)safe; }; + f = [&local]() { (void)local; }; // expected-warning {{address of stack memory is returned later}} + return f; // expected-note {{returned here}} +} + +std::function<void()> reassign_unsafe_then_safe() { + static int safe = 1; + int local = 2; + std::function<void()> f = [&local]() { (void)local; }; + f = []() { (void)safe; }; + return f; +} + +std::function<void()> non_capturing_lambda() { + return []() {}; +} + +void free_function(); + +std::function<void()> reassign_lambda_to_function_pointer() { + int local; + std::function<void()> f = [&local]() { (void)local; }; + f = &free_function; + return f; +} + +struct Functor { void operator()() const; }; + +// FIXME: False positive. +std::function<void()> reassign_lambda_to_functor() { + int local; + Functor c; + std::function<void()> f = [&local]() { (void)local; }; // expected-warning {{address of stack memory is returned later}} + f = c; + return f; // expected-note {{returned here}} +} + +} // namespace callable_wrappers >From c36bfcda901cede1701a80a3d056f277316ebb22 Mon Sep 17 00:00:00 2001 From: Zhijie Wang <[email protected]> Date: Sat, 11 Apr 2026 16:52:54 -0700 Subject: [PATCH 02/12] Add a kill-only mechanism (e.g., OriginFlowFact with no source) to clear old loans when the RHS has no origins --- .../clang/Analysis/Analyses/LifetimeSafety/Facts.h | 9 ++++++--- clang/lib/Analysis/LifetimeSafety/Facts.cpp | 10 ++++++---- clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp | 11 ++++++++--- clang/lib/Analysis/LifetimeSafety/LoanPropagation.cpp | 10 ++++++---- clang/lib/Analysis/LifetimeSafety/Origins.cpp | 11 ++++++++--- clang/test/Sema/warn-lifetime-safety.cpp | 5 ++--- 6 files changed, 36 insertions(+), 20 deletions(-) diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h index 6be8f6e455bc2..265f783183553 100644 --- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h +++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h @@ -130,7 +130,9 @@ class ExpireFact : public Fact { class OriginFlowFact : public Fact { OriginID OIDDest; - OriginID OIDSrc; + // The source origin to flow from. Absent when only clearing the destination's + // loans. + std::optional<OriginID> OIDSrc; // True if the destination origin should be killed (i.e., its current loans // cleared) before the source origin's loans are flowed into it. bool KillDest; @@ -140,12 +142,13 @@ class OriginFlowFact : public Fact { return F->getKind() == Kind::OriginFlow; } - OriginFlowFact(OriginID OIDDest, OriginID OIDSrc, bool KillDest) + OriginFlowFact(OriginID OIDDest, std::optional<OriginID> OIDSrc, + bool KillDest) : Fact(Kind::OriginFlow), OIDDest(OIDDest), OIDSrc(OIDSrc), KillDest(KillDest) {} OriginID getDestOriginID() const { return OIDDest; } - OriginID getSrcOriginID() const { return OIDSrc; } + std::optional<OriginID> getSrcOriginID() const { return OIDSrc; } bool getKillDest() const { return KillDest; } void dump(llvm::raw_ostream &OS, const LoanManager &, diff --git a/clang/lib/Analysis/LifetimeSafety/Facts.cpp b/clang/lib/Analysis/LifetimeSafety/Facts.cpp index 1bc0521a72359..7a04fd1dfe2f2 100644 --- a/clang/lib/Analysis/LifetimeSafety/Facts.cpp +++ b/clang/lib/Analysis/LifetimeSafety/Facts.cpp @@ -43,10 +43,12 @@ void OriginFlowFact::dump(llvm::raw_ostream &OS, const LoanManager &, OS << "\tDest: "; OM.dump(getDestOriginID(), OS); OS << "\n"; - OS << "\tSrc: "; - OM.dump(getSrcOriginID(), OS); - OS << (getKillDest() ? "" : ", Merge"); - OS << "\n"; + if (getSrcOriginID()) { + OS << "\tSrc: "; + OM.dump(*getSrcOriginID(), OS); + OS << (getKillDest() ? "" : ", Merge"); + OS << "\n"; + } } void MovedOriginFact::dump(llvm::raw_ostream &OS, const LoanManager &, diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp index 8f86f35ca2f34..b6c6d37d4cc20 100644 --- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp +++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp @@ -409,10 +409,15 @@ void FactsGenerator::handleAssignment(const Expr *LHSExpr, } else markUseAsWrite(DRE_LHS); } - // RHS may not have tracked origins (e.g., assigning a non-lambda functor - // to a std::function). Skip the flow in that case. - if (!RHSList) + if (!RHSList) { + // RHS has no tracked origins (e.g., assigning a non-lambda functor to a + // std::function). Emit a kill-only flow to clear old loans. + if (OriginList *LHSInner = LHSList->peelOuterOrigin()) + CurrentBlockFacts.push_back(FactMgr.createFact<OriginFlowFact>( + LHSInner->getOuterOriginID(), /*OIDSrc=*/std::nullopt, + /*KillDest=*/true)); return; + } // Kill the old loans of the destination origin and flow the new loans // from the source origin. flow(LHSList->peelOuterOrigin(), RHSList, /*Kill=*/true); diff --git a/clang/lib/Analysis/LifetimeSafety/LoanPropagation.cpp b/clang/lib/Analysis/LifetimeSafety/LoanPropagation.cpp index e437fb7d41268..a53e8d0419282 100644 --- a/clang/lib/Analysis/LifetimeSafety/LoanPropagation.cpp +++ b/clang/lib/Analysis/LifetimeSafety/LoanPropagation.cpp @@ -55,7 +55,8 @@ static llvm::BitVector computePersistentOrigins(const FactManager &FactMgr, case Fact::Kind::OriginFlow: { const auto *OF = F->getAs<OriginFlowFact>(); CheckOrigin(OF->getDestOriginID()); - CheckOrigin(OF->getSrcOriginID()); + if (OF->getSrcOriginID()) + CheckOrigin(*OF->getSrcOriginID()); break; } case Fact::Kind::Use: @@ -168,14 +169,15 @@ class AnalysisImpl /// A flow from source to destination. If `KillDest` is true, this replaces /// the destination's loans with the source's. Otherwise, the source's loans - /// are merged into the destination's. + /// are merged into the destination's. If the source is nullopt, this is a + /// kill-only flow that clears the destination without adding new loans. Lattice transfer(Lattice In, const OriginFlowFact &F) { OriginID DestOID = F.getDestOriginID(); - OriginID SrcOID = F.getSrcOriginID(); LoanSet DestLoans = F.getKillDest() ? LoanSetFactory.getEmptySet() : getLoans(In, DestOID); - LoanSet SrcLoans = getLoans(In, SrcOID); + LoanSet SrcLoans = F.getSrcOriginID() ? getLoans(In, *F.getSrcOriginID()) + : LoanSetFactory.getEmptySet(); LoanSet MergedLoans = utils::join(DestLoans, SrcLoans, LoanSetFactory); return setLoans(In, DestOID, MergedLoans); diff --git a/clang/lib/Analysis/LifetimeSafety/Origins.cpp b/clang/lib/Analysis/LifetimeSafety/Origins.cpp index cda537bf98f75..033cbdd75352c 100644 --- a/clang/lib/Analysis/LifetimeSafety/Origins.cpp +++ b/clang/lib/Analysis/LifetimeSafety/Origins.cpp @@ -111,9 +111,14 @@ bool OriginManager::hasOrigins(QualType QT) const { // stored lambda's origins. if (isStdCallableWrapperType(RD)) return true; - // TODO: Extend origin tracking to user-defined structs that may carry - // origins. - return RD->isLambda(); + // 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 8e1f1b395875e..b650cf6ed086c 100644 --- a/clang/test/Sema/warn-lifetime-safety.cpp +++ b/clang/test/Sema/warn-lifetime-safety.cpp @@ -2599,13 +2599,12 @@ std::function<void()> reassign_lambda_to_function_pointer() { struct Functor { void operator()() const; }; -// FIXME: False positive. std::function<void()> reassign_lambda_to_functor() { int local; Functor c; - std::function<void()> f = [&local]() { (void)local; }; // expected-warning {{address of stack memory is returned later}} + std::function<void()> f = [&local]() { (void)local; }; f = c; - return f; // expected-note {{returned here}} + return f; } } // namespace callable_wrappers >From d6f3ea7a9abb2e5e3fe3eaf1711c7f87a9f9c0aa Mon Sep 17 00:00:00 2001 From: Zhijie Wang <[email protected]> Date: Sun, 12 Apr 2026 14:49:47 -0700 Subject: [PATCH 03/12] Add a separate KillOriginFact --- .../Analysis/Analyses/LifetimeSafety/Facts.h | 28 +++++++++++++++---- clang/lib/Analysis/LifetimeSafety/Dataflow.h | 3 ++ clang/lib/Analysis/LifetimeSafety/Facts.cpp | 17 +++++++---- .../LifetimeSafety/FactsGenerator.cpp | 12 ++++---- .../Analysis/LifetimeSafety/LiveOrigins.cpp | 4 +++ .../LifetimeSafety/LoanPropagation.cpp | 17 +++++++---- 6 files changed, 57 insertions(+), 24 deletions(-) diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h index 265f783183553..d375e94ea9771 100644 --- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h +++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h @@ -55,6 +55,9 @@ class Fact { OriginEscapes, /// An origin is invalidated (e.g. vector resized). InvalidateOrigin, + /// All loans are cleared from an origin (e.g., assigning a callable without + /// tracked origins to std::function). + KillOrigin, }; private: @@ -130,9 +133,7 @@ class ExpireFact : public Fact { class OriginFlowFact : public Fact { OriginID OIDDest; - // The source origin to flow from. Absent when only clearing the destination's - // loans. - std::optional<OriginID> OIDSrc; + OriginID OIDSrc; // True if the destination origin should be killed (i.e., its current loans // cleared) before the source origin's loans are flowed into it. bool KillDest; @@ -142,13 +143,12 @@ class OriginFlowFact : public Fact { return F->getKind() == Kind::OriginFlow; } - OriginFlowFact(OriginID OIDDest, std::optional<OriginID> OIDSrc, - bool KillDest) + OriginFlowFact(OriginID OIDDest, OriginID OIDSrc, bool KillDest) : Fact(Kind::OriginFlow), OIDDest(OIDDest), OIDSrc(OIDSrc), KillDest(KillDest) {} OriginID getDestOriginID() const { return OIDDest; } - std::optional<OriginID> getSrcOriginID() const { return OIDSrc; } + OriginID getSrcOriginID() const { return OIDSrc; } bool getKillDest() const { return KillDest; } void dump(llvm::raw_ostream &OS, const LoanManager &, @@ -319,6 +319,22 @@ class TestPointFact : public Fact { const OriginManager &) const override; }; +class KillOriginFact : public Fact { + OriginID OID; + +public: + static bool classof(const Fact *F) { + return F->getKind() == Kind::KillOrigin; + } + + KillOriginFact(OriginID OID) : Fact(Kind::KillOrigin), OID(OID) {} + + OriginID getKilledOrigin() const { return OID; } + + void dump(llvm::raw_ostream &OS, const LoanManager &, + const OriginManager &OM) const override; +}; + class FactManager { public: FactManager(const AnalysisDeclContext &AC, const CFG &Cfg) : OriginMgr(AC) { diff --git a/clang/lib/Analysis/LifetimeSafety/Dataflow.h b/clang/lib/Analysis/LifetimeSafety/Dataflow.h index 0f64ac8a36ef7..fc3049c8bec84 100644 --- a/clang/lib/Analysis/LifetimeSafety/Dataflow.h +++ b/clang/lib/Analysis/LifetimeSafety/Dataflow.h @@ -180,6 +180,8 @@ class DataflowAnalysis { return D->transfer(In, *F->getAs<TestPointFact>()); case Fact::Kind::InvalidateOrigin: return D->transfer(In, *F->getAs<InvalidateOriginFact>()); + case Fact::Kind::KillOrigin: + return D->transfer(In, *F->getAs<KillOriginFact>()); } llvm_unreachable("Unknown fact kind"); } @@ -193,6 +195,7 @@ class DataflowAnalysis { Lattice transfer(Lattice In, const UseFact &) { return In; } Lattice transfer(Lattice In, const TestPointFact &) { return In; } Lattice transfer(Lattice In, const InvalidateOriginFact &) { return In; } + Lattice transfer(Lattice In, const KillOriginFact &) { return In; } }; } // namespace clang::lifetimes::internal #endif // LLVM_CLANG_ANALYSIS_ANALYSES_LIFETIMESAFETY_DATAFLOW_H diff --git a/clang/lib/Analysis/LifetimeSafety/Facts.cpp b/clang/lib/Analysis/LifetimeSafety/Facts.cpp index 7a04fd1dfe2f2..3d7fbcdacc830 100644 --- a/clang/lib/Analysis/LifetimeSafety/Facts.cpp +++ b/clang/lib/Analysis/LifetimeSafety/Facts.cpp @@ -43,12 +43,10 @@ void OriginFlowFact::dump(llvm::raw_ostream &OS, const LoanManager &, OS << "\tDest: "; OM.dump(getDestOriginID(), OS); OS << "\n"; - if (getSrcOriginID()) { - OS << "\tSrc: "; - OM.dump(*getSrcOriginID(), OS); - OS << (getKillDest() ? "" : ", Merge"); - OS << "\n"; - } + OS << "\tSrc: "; + OM.dump(getSrcOriginID(), OS); + OS << (getKillDest() ? "" : ", Merge"); + OS << "\n"; } void MovedOriginFact::dump(llvm::raw_ostream &OS, const LoanManager &, @@ -105,6 +103,13 @@ void TestPointFact::dump(llvm::raw_ostream &OS, const LoanManager &, OS << "TestPoint (Annotation: \"" << getAnnotation() << "\")\n"; } +void KillOriginFact::dump(llvm::raw_ostream &OS, const LoanManager &, + const OriginManager &OM) const { + OS << "KillOrigin ("; + OM.dump(getKilledOrigin(), OS); + OS << ")\n"; +} + llvm::StringMap<ProgramPoint> FactManager::getTestPoints() const { llvm::StringMap<ProgramPoint> AnnotationToPointMap; for (const auto &BlockFacts : BlockToFacts) { diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp index b6c6d37d4cc20..c991438c40e3a 100644 --- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp +++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp @@ -410,12 +410,12 @@ void FactsGenerator::handleAssignment(const Expr *LHSExpr, markUseAsWrite(DRE_LHS); } if (!RHSList) { - // RHS has no tracked origins (e.g., assigning a non-lambda functor to a - // std::function). Emit a kill-only flow to clear old loans. - if (OriginList *LHSInner = LHSList->peelOuterOrigin()) - CurrentBlockFacts.push_back(FactMgr.createFact<OriginFlowFact>( - LHSInner->getOuterOriginID(), /*OIDSrc=*/std::nullopt, - /*KillDest=*/true)); + // RHS has no tracked origins (e.g., assigning a callable without origins + // to std::function). Clear loans of the destination. + for (OriginList *LHSInner = LHSList->peelOuterOrigin(); LHSInner; + LHSInner = LHSInner->peelOuterOrigin()) + CurrentBlockFacts.push_back( + FactMgr.createFact<KillOriginFact>(LHSInner->getOuterOriginID())); return; } // Kill the old loans of the destination origin and flow the new loans diff --git a/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp b/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp index bc7494360624e..cfbcacf04b1b0 100644 --- a/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp +++ b/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp @@ -166,6 +166,10 @@ class AnalysisImpl return Lattice(Factory.remove(In.LiveOrigins, OF.getDestOriginID())); } + Lattice transfer(Lattice In, const KillOriginFact &F) { + return Lattice(Factory.remove(In.LiveOrigins, F.getKilledOrigin())); + } + Lattice transfer(Lattice In, const ExpireFact &F) { if (auto OID = F.getOriginID()) return Lattice(Factory.remove(In.LiveOrigins, *OID)); diff --git a/clang/lib/Analysis/LifetimeSafety/LoanPropagation.cpp b/clang/lib/Analysis/LifetimeSafety/LoanPropagation.cpp index a53e8d0419282..adbc0458516e1 100644 --- a/clang/lib/Analysis/LifetimeSafety/LoanPropagation.cpp +++ b/clang/lib/Analysis/LifetimeSafety/LoanPropagation.cpp @@ -55,8 +55,7 @@ static llvm::BitVector computePersistentOrigins(const FactManager &FactMgr, case Fact::Kind::OriginFlow: { const auto *OF = F->getAs<OriginFlowFact>(); CheckOrigin(OF->getDestOriginID()); - if (OF->getSrcOriginID()) - CheckOrigin(*OF->getSrcOriginID()); + CheckOrigin(OF->getSrcOriginID()); break; } case Fact::Kind::Use: @@ -64,6 +63,9 @@ static llvm::BitVector computePersistentOrigins(const FactManager &FactMgr, Cur = Cur->peelOuterOrigin()) CheckOrigin(Cur->getOuterOriginID()); break; + case Fact::Kind::KillOrigin: + CheckOrigin(F->getAs<KillOriginFact>()->getKilledOrigin()); + break; case Fact::Kind::MovedOrigin: case Fact::Kind::OriginEscapes: case Fact::Kind::Expire: @@ -169,20 +171,23 @@ class AnalysisImpl /// A flow from source to destination. If `KillDest` is true, this replaces /// the destination's loans with the source's. Otherwise, the source's loans - /// are merged into the destination's. If the source is nullopt, this is a - /// kill-only flow that clears the destination without adding new loans. + /// are merged into the destination's. Lattice transfer(Lattice In, const OriginFlowFact &F) { OriginID DestOID = F.getDestOriginID(); + OriginID SrcOID = F.getSrcOriginID(); LoanSet DestLoans = F.getKillDest() ? LoanSetFactory.getEmptySet() : getLoans(In, DestOID); - LoanSet SrcLoans = F.getSrcOriginID() ? getLoans(In, *F.getSrcOriginID()) - : LoanSetFactory.getEmptySet(); + LoanSet SrcLoans = getLoans(In, SrcOID); LoanSet MergedLoans = utils::join(DestLoans, SrcLoans, LoanSetFactory); return setLoans(In, DestOID, MergedLoans); } + Lattice transfer(Lattice In, const KillOriginFact &F) { + return setLoans(In, F.getKilledOrigin(), LoanSetFactory.getEmptySet()); + } + Lattice transfer(Lattice In, const ExpireFact &F) { if (auto OID = F.getOriginID()) return setLoans(In, *OID, LoanSetFactory.getEmptySet()); >From 120b661bad96423cc2ca482681965ecf81808005 Mon Sep 17 00:00:00 2001 From: Zhijie Wang <[email protected]> Date: Sun, 12 Apr 2026 17:29:22 -0700 Subject: [PATCH 04/12] Add copy/move assign tests --- clang/test/Sema/Inputs/lifetime-analysis.h | 4 ++++ clang/test/Sema/warn-lifetime-safety.cpp | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/clang/test/Sema/Inputs/lifetime-analysis.h b/clang/test/Sema/Inputs/lifetime-analysis.h index 58ba1aa0c03c3..2b904f88bc475 100644 --- a/clang/test/Sema/Inputs/lifetime-analysis.h +++ b/clang/test/Sema/Inputs/lifetime-analysis.h @@ -274,7 +274,11 @@ template<class R, class... Args> class function<R(Args...)> { public: template<class F> function(F) {} + function(const function&) {} + function(function&&) {} template<class F> function& operator=(F) { return *this; } + function& operator=(const function&) { return *this; } + function& operator=(function&&) { return *this; } ~function(); }; diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp index b650cf6ed086c..7bd93ee4ba8f3 100644 --- a/clang/test/Sema/warn-lifetime-safety.cpp +++ b/clang/test/Sema/warn-lifetime-safety.cpp @@ -2568,6 +2568,25 @@ std::function<void()> copy_function() { return f2; // expected-note {{returned here}} } +std::function<void()> copy_assign() { + int x; + std::function<void()> f = [&x]() { (void)x; }; // expected-warning {{address of stack memory is returned later}} + std::function<void()> f2 = []() {}; + f2 = f; + return f2; // expected-note {{returned here}} +} + +// FIXME: False negative. std::move's lifetimebound handling in +// `handleFunctionCall` only flows the outermost origin, missing inner origins +// that carry the lambda's loans. +std::function<void()> move_assign() { + int x; + std::function<void()> f = [&x]() { (void)x; }; // Should warn. + std::function<void()> f2 = []() {}; + f2 = std::move(f); + return f2; +} + std::function<void()> reassign_safe_then_unsafe() { static int safe = 1; int local = 2; >From 4127ef03f34e6fadc4e2ee1491eff5a260533885 Mon Sep 17 00:00:00 2001 From: Zhijie Wang <[email protected]> Date: Sun, 12 Apr 2026 18:22:02 -0700 Subject: [PATCH 05/12] Add test function_ref_from_non_capturing_lambda --- clang/test/Sema/warn-lifetime-safety.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp index 7bd93ee4ba8f3..e48e3fa38f526 100644 --- a/clang/test/Sema/warn-lifetime-safety.cpp +++ b/clang/test/Sema/warn-lifetime-safety.cpp @@ -2626,4 +2626,15 @@ std::function<void()> reassign_lambda_to_functor() { return f; } +struct [[gsl::Pointer]] function_ref { + template <typename Callable> + function_ref(Callable &&callable [[clang::lifetimebound]]) : ref(callable) {} + void (*ref)(); +}; + +function_ref function_ref_from_non_capturing_lambda() { + return []() {}; // expected-warning {{address of stack memory is returned later}} \ + // expected-note {{returned here}} +} + } // namespace callable_wrappers >From 26479172e21203bf2f124f685d1a160c2b983c3e Mon Sep 17 00:00:00 2001 From: Zhijie Wang <[email protected]> Date: Sun, 12 Apr 2026 20:59:02 -0700 Subject: [PATCH 06/12] add test --- clang/test/Sema/warn-lifetime-safety.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp index e48e3fa38f526..ac4ef9cd8e49f 100644 --- a/clang/test/Sema/warn-lifetime-safety.cpp +++ b/clang/test/Sema/warn-lifetime-safety.cpp @@ -2637,4 +2637,9 @@ function_ref function_ref_from_non_capturing_lambda() { // expected-note {{returned here}} } +void assign_non_capturing_to_function_ref(function_ref &r) { + r = []() {}; + (void)r; +} + } // namespace callable_wrappers >From d7c9b492f925e2abeefb09fa0c657cc348aa6576 Mon Sep 17 00:00:00 2001 From: Zhijie Wang <[email protected]> Date: Sun, 12 Apr 2026 21:19:04 -0700 Subject: [PATCH 07/12] update doc --- clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h index d375e94ea9771..b70fecd5ab1d1 100644 --- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h +++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h @@ -55,8 +55,7 @@ class Fact { OriginEscapes, /// An origin is invalidated (e.g. vector resized). InvalidateOrigin, - /// All loans are cleared from an origin (e.g., assigning a callable without - /// tracked origins to std::function). + /// All loans of an origin are cleared. KillOrigin, }; @@ -319,6 +318,8 @@ class TestPointFact : public Fact { const OriginManager &) const override; }; +/// All loans are cleared from an origin (e.g., assigning a callable without +/// tracked origins to std::function). class KillOriginFact : public Fact { OriginID OID; >From 6c4867b6ad4005ae613d6c34a0b7a8a4cba7b72b Mon Sep 17 00:00:00 2001 From: Zhijie Wang <[email protected]> Date: Sun, 12 Apr 2026 21:33:31 -0700 Subject: [PATCH 08/12] update test --- clang/test/Sema/warn-lifetime-safety.cpp | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp index ac4ef9cd8e49f..2a2291600c27b 100644 --- a/clang/test/Sema/warn-lifetime-safety.cpp +++ b/clang/test/Sema/warn-lifetime-safety.cpp @@ -2632,14 +2632,10 @@ struct [[gsl::Pointer]] function_ref { void (*ref)(); }; -function_ref function_ref_from_non_capturing_lambda() { - return []() {}; // expected-warning {{address of stack memory is returned later}} \ - // expected-note {{returned here}} -} - void assign_non_capturing_to_function_ref(function_ref &r) { - r = []() {}; - (void)r; + r = []() {}; // expected-warning {{object whose reference is captured does not live long enough}} \ + // expected-note {{destroyed here}} + (void)r; // expected-note {{later used here}} } } // namespace callable_wrappers >From aea1bacd391b80c2afa6144ffbc4b5575e855ef8 Mon Sep 17 00:00:00 2001 From: Zhijie Wang <[email protected]> Date: Sun, 12 Apr 2026 22:40:28 -0700 Subject: [PATCH 09/12] update test --- clang/test/Sema/warn-lifetime-safety.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp index 2a2291600c27b..c1ea383ce92ee 100644 --- a/clang/test/Sema/warn-lifetime-safety.cpp +++ b/clang/test/Sema/warn-lifetime-safety.cpp @@ -2626,16 +2626,24 @@ std::function<void()> reassign_lambda_to_functor() { return f; } +} // namespace callable_wrappers + +namespace GH126600 { +// https://github.com/llvm/llvm-project/issues/126600 struct [[gsl::Pointer]] function_ref { template <typename Callable> function_ref(Callable &&callable [[clang::lifetimebound]]) : ref(callable) {} void (*ref)(); }; +// FIXME: The lifetimebound annotation tracks the outer callable object's +// storage rather than what the callable captures. A mechanism like +// lifetimebound(2) could enable tracking inner lifetimes, which would +// avoid this warning for non-capturing lambdas. void assign_non_capturing_to_function_ref(function_ref &r) { r = []() {}; // expected-warning {{object whose reference is captured does not live long enough}} \ // expected-note {{destroyed here}} (void)r; // expected-note {{later used here}} } -} // namespace callable_wrappers +} // namespace GH126600 >From 1d7aa1c8351296c60958388a4df4d2d546a7e91d Mon Sep 17 00:00:00 2001 From: Zhijie Wang <[email protected]> Date: Mon, 13 Apr 2026 22:18:23 -0700 Subject: [PATCH 10/12] add tests --- .../warn-lifetime-safety-dangling-field.cpp | 13 +++++++++++ .../warn-lifetime-safety-invalidations.cpp | 12 ++++++++++ .../Sema/warn-lifetime-safety-noescape.cpp | 8 +++++++ .../Sema/warn-lifetime-safety-suggestions.cpp | 23 +++++++++++++++++++ clang/test/Sema/warn-lifetime-safety.cpp | 14 ++++++++++- 5 files changed, 69 insertions(+), 1 deletion(-) diff --git a/clang/test/Sema/warn-lifetime-safety-dangling-field.cpp b/clang/test/Sema/warn-lifetime-safety-dangling-field.cpp index 79b0183ed91ec..2afcd4a3dd97b 100644 --- a/clang/test/Sema/warn-lifetime-safety-dangling-field.cpp +++ b/clang/test/Sema/warn-lifetime-safety-dangling-field.cpp @@ -196,3 +196,16 @@ struct DerivedWithCtor : BaseWithPointer { } }; } // namespace DanglingPointerFieldInBaseClass + +namespace callable_wrappers { + +struct HasCallback { + std::function<void()> callback; // expected-note {{this field dangles}} + + void set_callback() { + int local; + callback = [&local]() { (void)local; }; // expected-warning {{address of stack memory escapes to a field}} + } +}; + +} // namespace callable_wrappers diff --git a/clang/test/Sema/warn-lifetime-safety-invalidations.cpp b/clang/test/Sema/warn-lifetime-safety-invalidations.cpp index c7f920c03736a..59d70a13bb963 100644 --- a/clang/test/Sema/warn-lifetime-safety-invalidations.cpp +++ b/clang/test/Sema/warn-lifetime-safety-invalidations.cpp @@ -512,3 +512,15 @@ void ref_capture_reassigned_to_safe() { lambda(); // should not warn } } // namespace lambda_capture_invalidation + +namespace callable_wrappers { + +void function_captured_ref_invalidated() { + std::vector<int> v; + v.push_back(1); + std::function<void()> f = [&r = v[0]]() { (void)r; }; // expected-warning {{object whose reference is captured is later invalidated}} + v.push_back(2); // expected-note {{invalidated here}} + (void)f; // expected-note {{later used here}} +} + +} // namespace callable_wrappers diff --git a/clang/test/Sema/warn-lifetime-safety-noescape.cpp b/clang/test/Sema/warn-lifetime-safety-noescape.cpp index f233ec546faa5..4bb57e6b9df95 100644 --- a/clang/test/Sema/warn-lifetime-safety-noescape.cpp +++ b/clang/test/Sema/warn-lifetime-safety-noescape.cpp @@ -191,3 +191,11 @@ MyObj& return_ref_from_noescape_ptr( int* return_spaced_brackets(int* p [ [clang::noescape] /*some comment*/ ]) { // expected-warning {{parameter is marked [[clang::noescape]] but escapes}} return p; // expected-note {{returned here}} } + +namespace callable_wrappers { + +std::function<void()> escape_noescape_via_function(int &x [[clang::noescape]]) { // expected-warning {{parameter is marked [[clang::noescape]] but escapes}} + return [&]() { (void)x; }; // expected-note {{returned here}} +} + +} // namespace callable_wrappers diff --git a/clang/test/Sema/warn-lifetime-safety-suggestions.cpp b/clang/test/Sema/warn-lifetime-safety-suggestions.cpp index b3b13038dc344..d49143448ff14 100644 --- a/clang/test/Sema/warn-lifetime-safety-suggestions.cpp +++ b/clang/test/Sema/warn-lifetime-safety-suggestions.cpp @@ -62,6 +62,21 @@ struct ReturnThisPointer { }; +namespace std { +template<class> class function; +template<class R, class... Args> +class function<R(Args...)> { +public: + template<class F> function(F) {} + function(const function&) {} + function(function&&) {} + template<class F> function& operator=(F) { return *this; } + function& operator=(const function&) { return *this; } + function& operator=(function&&) { return *this; } + ~function(); +}; +} // namespace std + #endif // TEST_HEADER_H //--- test_source.cpp @@ -529,3 +544,11 @@ CaptureRefToBaseView test_ref_to_base_view() { return x; // expected-note {{returned here}} } } // namespace capturing_constructor + +namespace callable_wrappers { + +std::function<void()> return_lambda_capturing_param(int &x) { // expected-warning {{parameter in intra-TU function should be marked [[clang::lifetimebound]]}} + return [&]() { (void)x; }; // expected-note {{param returned here}} +} + +} // namespace callable_wrappers diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp index c1ea383ce92ee..001c9ae5480b7 100644 --- a/clang/test/Sema/warn-lifetime-safety.cpp +++ b/clang/test/Sema/warn-lifetime-safety.cpp @@ -2626,10 +2626,22 @@ std::function<void()> reassign_lambda_to_functor() { return f; } +std::function<void()> capture_lifetimebound_param(int &x [[clang::lifetimebound]]) { + return [&]() { (void)x; }; +} + +void uaf_via_lifetimebound() { + std::function<void()> f = []() {}; + { + int local; + f = capture_lifetimebound_param(local); // expected-warning {{object whose reference is captured does not live long enough}} + } // expected-note {{destroyed here}} + (void)f; // expected-note {{later used here}} +} + } // namespace callable_wrappers namespace GH126600 { -// https://github.com/llvm/llvm-project/issues/126600 struct [[gsl::Pointer]] function_ref { template <typename Callable> function_ref(Callable &&callable [[clang::lifetimebound]]) : ref(callable) {} >From a4dba26a3ce7d62d73eefab119c7236fe04d0a85 Mon Sep 17 00:00:00 2001 From: Zhijie Wang <[email protected]> Date: Mon, 13 Apr 2026 22:27:45 -0700 Subject: [PATCH 11/12] reuse header --- .../Sema/warn-lifetime-safety-suggestions.cpp | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/clang/test/Sema/warn-lifetime-safety-suggestions.cpp b/clang/test/Sema/warn-lifetime-safety-suggestions.cpp index d49143448ff14..c86d96046e771 100644 --- a/clang/test/Sema/warn-lifetime-safety-suggestions.cpp +++ b/clang/test/Sema/warn-lifetime-safety-suggestions.cpp @@ -1,8 +1,8 @@ // RUN: rm -rf %t // RUN: split-file %s %t -// RUN: %clang_cc1 -fsyntax-only -flifetime-safety-inference -fexperimental-lifetime-safety-tu-analysis -Wlifetime-safety-suggestions -Wlifetime-safety -Wno-dangling -I%t -verify %t/test_source.cpp -// RUN: %clang_cc1 -flifetime-safety-inference -fexperimental-lifetime-safety-tu-analysis -Wlifetime-safety-suggestions -Wlifetime-safety -Wno-dangling -I%t -fixit %t/test_source.cpp -// RUN: %clang_cc1 -fsyntax-only -flifetime-safety-inference -fexperimental-lifetime-safety-tu-analysis -Wlifetime-safety-suggestions -Wno-dangling -I%t -Werror=lifetime-safety-suggestions %t/test_source.cpp +// RUN: %clang_cc1 -fsyntax-only -flifetime-safety-inference -fexperimental-lifetime-safety-tu-analysis -Wlifetime-safety-suggestions -Wlifetime-safety -Wno-dangling -I%t -I%S -verify %t/test_source.cpp +// RUN: %clang_cc1 -flifetime-safety-inference -fexperimental-lifetime-safety-tu-analysis -Wlifetime-safety-suggestions -Wlifetime-safety -Wno-dangling -I%t -I%S -fixit %t/test_source.cpp +// RUN: %clang_cc1 -fsyntax-only -flifetime-safety-inference -fexperimental-lifetime-safety-tu-analysis -Wlifetime-safety-suggestions -Wno-dangling -I%t -I%S -Werror=lifetime-safety-suggestions %t/test_source.cpp View definition_before_header(View a); @@ -62,26 +62,12 @@ struct ReturnThisPointer { }; -namespace std { -template<class> class function; -template<class R, class... Args> -class function<R(Args...)> { -public: - template<class F> function(F) {} - function(const function&) {} - function(function&&) {} - template<class F> function& operator=(F) { return *this; } - function& operator=(const function&) { return *this; } - function& operator=(function&&) { return *this; } - ~function(); -}; -} // namespace std - #endif // TEST_HEADER_H //--- test_source.cpp #include "test_header.h" +#include "Inputs/lifetime-analysis.h" View definition_before_header(View a) { return a; // expected-note {{param returned here}} >From a712910329696327b2f6efce8bfd07a591a8f797 Mon Sep 17 00:00:00 2001 From: Zhijie Wang <[email protected]> Date: Mon, 13 Apr 2026 22:41:54 -0700 Subject: [PATCH 12/12] add test: uaf_via_inferred_lifetimebound --- clang/test/Sema/warn-lifetime-safety-suggestions.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/clang/test/Sema/warn-lifetime-safety-suggestions.cpp b/clang/test/Sema/warn-lifetime-safety-suggestions.cpp index c86d96046e771..d2cf1c175eb57 100644 --- a/clang/test/Sema/warn-lifetime-safety-suggestions.cpp +++ b/clang/test/Sema/warn-lifetime-safety-suggestions.cpp @@ -537,4 +537,13 @@ std::function<void()> return_lambda_capturing_param(int &x) { // expected-warnin return [&]() { (void)x; }; // expected-note {{param returned here}} } +void uaf_via_inferred_lifetimebound() { + std::function<void()> f = []() {}; + { + int local; + f = return_lambda_capturing_param(local); // expected-warning {{object whose reference is captured does not live long enough}} + } // expected-note {{destroyed here}} + (void)f; // expected-note {{later used here}} +} + } // namespace callable_wrappers _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
