https://github.com/aeft updated https://github.com/llvm/llvm-project/pull/187917
>From 2c86039bf1e5f1e15294318d041053b5b608b395 Mon Sep 17 00:00:00 2001 From: Alex Wang <[email protected]> Date: Sat, 21 Mar 2026 23:53:45 -0700 Subject: [PATCH 1/8] [LifetimeSafety] Track origins for lifetimebound calls returning record types --- .../Analysis/Analyses/LifetimeSafety/Facts.h | 2 +- .../Analyses/LifetimeSafety/FactsGenerator.h | 3 + .../Analyses/LifetimeSafety/Origins.h | 22 +- .../LifetimeSafety/FactsGenerator.cpp | 41 ++- .../LifetimeSafety/LifetimeSafety.cpp | 3 + clang/lib/Analysis/LifetimeSafety/Origins.cpp | 88 ++++++- .../Sema/warn-lifetime-analysis-nocfg.cpp | 20 +- clang/test/Sema/warn-lifetime-safety.cpp | 249 +++++++++++++++++- 8 files changed, 390 insertions(+), 38 deletions(-) diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h index 0f848abd913d3..e821e96527deb 100644 --- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h +++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h @@ -318,7 +318,7 @@ class TestPointFact : public Fact { class FactManager { public: FactManager(const AnalysisDeclContext &AC, const CFG &Cfg) - : OriginMgr(AC.getASTContext(), AC.getDecl()) { + : OriginMgr(AC.getASTContext()) { BlockToFacts.resize(Cfg.getNumBlockIDs()); } diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h index dfcbdc7d73007..775b51afa0237 100644 --- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h +++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h @@ -57,6 +57,9 @@ class FactsGenerator : public ConstStmtVisitor<FactsGenerator> { OriginList *getOriginsList(const ValueDecl &D); OriginList *getOriginsList(const Expr &E); + bool hasOrigins(QualType QT) const; + bool hasOrigins(const Expr *E) const; + void flow(OriginList *Dst, OriginList *Src, bool Kill); void handleAssignment(const Expr *LHSExpr, const Expr *RHSExpr); diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Origins.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Origins.h index 8c638bdcace3f..12148d9b29c9c 100644 --- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Origins.h +++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Origins.h @@ -20,6 +20,7 @@ #include "clang/AST/TypeBase.h" #include "clang/Analysis/Analyses/LifetimeSafety/LifetimeStats.h" #include "clang/Analysis/Analyses/LifetimeSafety/Utils.h" +#include "clang/Analysis/AnalysisDeclContext.h" #include "llvm/Support/raw_ostream.h" namespace clang::lifetimes::internal { @@ -117,15 +118,17 @@ class OriginList { OriginList *InnerList = nullptr; }; -bool hasOrigins(QualType QT); -bool hasOrigins(const Expr *E); bool doesDeclHaveStorage(const ValueDecl *D); /// Manages the creation, storage, and retrieval of origins for pointer-like /// variables and expressions. class OriginManager { public: - explicit OriginManager(ASTContext &AST, const Decl *D); + explicit OriginManager(ASTContext &AST); + + /// Must be called after collectLifetimeboundOriginTypes() to ensure + /// ThisOrigins reflects the complete set of tracked types. + void initializeThisOrigins(const Decl *D); /// Gets or creates the OriginList for a given ValueDecl. /// @@ -155,11 +158,20 @@ class OriginManager { unsigned getNumOrigins() const { return NextOriginID.Value; } + bool hasOrigins(QualType QT) const; + bool hasOrigins(const Expr *E) const; + void dump(OriginID OID, llvm::raw_ostream &OS) const; /// Collects statistics about expressions that lack associated origins. void collectMissingOrigins(Stmt &FunctionBody, LifetimeSafetyStats &LSStats); + /// Pre-scans the function body (and constructor init lists) to discover + /// return types of [[clang::lifetimebound]] calls, registering them for + /// origin tracking. + void collectLifetimeboundOriginTypes(AnalysisDeclContext &AC); + void registerLifetimeboundOriginType(QualType QT); + private: OriginID getNextOriginID() { return NextOriginID++; } @@ -178,6 +190,10 @@ class OriginManager { llvm::DenseMap<const clang::ValueDecl *, OriginList *> DeclToList; llvm::DenseMap<const clang::Expr *, OriginList *> ExprToList; std::optional<OriginList *> ThisOrigins; + /// Types that are not inherently pointer-like but require origin tracking + /// because they are returned from functions with [[clang::lifetimebound]] + /// parameters. + llvm::DenseSet<const Type *> LifetimeboundOriginTypes; }; } // namespace clang::lifetimes::internal diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp index 80a73a2bf687e..03b297c30f008 100644 --- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp +++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp @@ -37,6 +37,14 @@ OriginList *FactsGenerator::getOriginsList(const Expr &E) { return FactMgr.getOriginMgr().getOrCreateList(&E); } +bool FactsGenerator::hasOrigins(QualType QT) const { + return FactMgr.getOriginMgr().hasOrigins(QT); +} + +bool FactsGenerator::hasOrigins(const Expr *E) const { + return FactMgr.getOriginMgr().hasOrigins(E); +} + /// Propagates origin information from Src to Dst through all levels of /// indirection, creating OriginFlowFacts at each level. /// @@ -182,14 +190,13 @@ void FactsGenerator::VisitCXXConstructExpr(const CXXConstructExpr *CCE) { handleGSLPointerConstruction(CCE); return; } - // Implicit copy/move constructors of lambda closures lack - // [[clang::lifetimebound]], so `handleFunctionCall` cannot propagate origins. - // Handle them directly to keep the origin chain intact (e.g., `return - // lambda;` copies the closure). - if (const auto *RD = CCE->getType()->getAsCXXRecordDecl(); - RD && RD->isLambda() && - CCE->getConstructor()->isCopyOrMoveConstructor() && - CCE->getNumArgs() == 1) { + // 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. + if (CCE->getConstructor()->isCopyOrMoveConstructor() && + CCE->getConstructor()->isDefaulted() && CCE->getNumArgs() == 1 && + hasOrigins(CCE->getType())) { const Expr *Arg = CCE->getArg(0); if (OriginList *ArgList = getRValueOrigins(Arg, getOriginsList(*Arg))) { flow(getOriginsList(*CCE), ArgList, /*Kill=*/true); @@ -398,8 +405,20 @@ void FactsGenerator::VisitCXXOperatorCallExpr(const CXXOperatorCallExpr *OCE) { // and are handled separately. if (OCE->getOperator() == OO_Equal && OCE->getNumArgs() == 2 && hasOrigins(OCE->getArg(0)->getType())) { - handleAssignment(OCE->getArg(0), OCE->getArg(1)); - return; + // Pointer-like types: assignment inherently propagates origins. + QualType LHSTy = OCE->getArg(0)->getType(); + if (LHSTy->isPointerOrReferenceType() || isGslPointerType(LHSTy)) { + 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 = + dyn_cast_or_null<CXXMethodDecl>(OCE->getDirectCallee()); + MD && MD->isDefaulted()) { + handleAssignment(OCE->getArg(0), OCE->getArg(1)); + return; + } } ArrayRef Args = {OCE->getArgs(), OCE->getNumArgs()}; @@ -646,7 +665,7 @@ void FactsGenerator::handleFunctionCall(const Expr *Call, auto IsArgLifetimeBound = [FD](unsigned I) -> bool { const ParmVarDecl *PVD = nullptr; if (const auto *Method = dyn_cast<CXXMethodDecl>(FD); - Method && Method->isInstance()) { + Method && Method->isInstance() && !isa<CXXConstructorDecl>(FD)) { if (I == 0) // For the 'this' argument, the attribute is on the method itself. return implicitObjectParamIsLifetimeBound(Method) || diff --git a/clang/lib/Analysis/LifetimeSafety/LifetimeSafety.cpp b/clang/lib/Analysis/LifetimeSafety/LifetimeSafety.cpp index 714f979fa5ee7..56a187202d8fa 100644 --- a/clang/lib/Analysis/LifetimeSafety/LifetimeSafety.cpp +++ b/clang/lib/Analysis/LifetimeSafety/LifetimeSafety.cpp @@ -71,6 +71,9 @@ void LifetimeSafetyAnalysis::run() { FactMgr = std::make_unique<FactManager>(AC, Cfg); + FactMgr->getOriginMgr().collectLifetimeboundOriginTypes(AC); + FactMgr->getOriginMgr().initializeThisOrigins(AC.getDecl()); + FactsGenerator FactGen(*FactMgr, AC); FactGen.run(); diff --git a/clang/lib/Analysis/LifetimeSafety/Origins.cpp b/clang/lib/Analysis/LifetimeSafety/Origins.cpp index 0122f7a734541..2655835317d9b 100644 --- a/clang/lib/Analysis/LifetimeSafety/Origins.cpp +++ b/clang/lib/Analysis/LifetimeSafety/Origins.cpp @@ -18,6 +18,7 @@ #include "clang/AST/TypeBase.h" #include "clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h" #include "clang/Analysis/Analyses/LifetimeSafety/LifetimeStats.h" +#include "clang/Analysis/AnalysisDeclContext.h" #include "llvm/ADT/StringMap.h" namespace clang::lifetimes::internal { @@ -29,10 +30,10 @@ class MissingOriginCollector public: MissingOriginCollector( const llvm::DenseMap<const clang::Expr *, OriginList *> &ExprToOriginList, - LifetimeSafetyStats &LSStats) - : ExprToOriginList(ExprToOriginList), LSStats(LSStats) {} + const OriginManager &OM, LifetimeSafetyStats &LSStats) + : ExprToOriginList(ExprToOriginList), OM(OM), LSStats(LSStats) {} bool VisitExpr(Expr *E) { - if (!hasOrigins(E)) + if (!OM.hasOrigins(E)) return true; // Check if we have an origin for this expression. if (!ExprToOriginList.contains(E)) { @@ -46,13 +47,60 @@ class MissingOriginCollector private: const llvm::DenseMap<const clang::Expr *, OriginList *> &ExprToOriginList; + const OriginManager &OM; LifetimeSafetyStats &LSStats; }; + +class LifetimeboundOriginTypeCollector + : public RecursiveASTVisitor<LifetimeboundOriginTypeCollector> { +public: + LifetimeboundOriginTypeCollector(OriginManager &OM) : OM(OM) {} + + bool VisitCallExpr(const CallExpr *CE) { + if (const auto *FD = CE->getDirectCallee()) + collect(FD, FD->getReturnType()); + return true; + } + + bool VisitCXXConstructExpr(const CXXConstructExpr *CCE) { + collect(CCE->getConstructor(), CCE->getType()); + return true; + } + + bool shouldVisitLambdaBody() const { return false; } + +private: + OriginManager &OM; + + void collect(const FunctionDecl *FD, QualType RetType) { + if (!FD) + return; + FD = getDeclWithMergedLifetimeBoundAttrs(FD); + + if (const auto *MD = dyn_cast<CXXMethodDecl>(FD); + MD && MD->isInstance() && !isa<CXXConstructorDecl>(MD) && + implicitObjectParamIsLifetimeBound(MD)) { + OM.registerLifetimeboundOriginType(RetType); + return; + } + + for (const auto *Param : FD->parameters()) { + if (Param->hasAttr<LifetimeBoundAttr>()) { + OM.registerLifetimeboundOriginType(RetType); + return; + } + } + } +}; + } // namespace -bool hasOrigins(QualType QT) { +bool OriginManager::hasOrigins(QualType QT) const { if (QT->isPointerOrReferenceType() || isGslPointerType(QT)) return true; + if (LifetimeboundOriginTypes.contains( + QT->getCanonicalTypeUnqualified().getTypePtr())) + return true; const auto *RD = QT->getAsCXXRecordDecl(); if (!RD) return false; @@ -70,7 +118,9 @@ bool hasOrigins(QualType QT) { /// /// An expression has origins if: /// - It's a glvalue (has addressable storage), OR -/// - Its type is pointer-like (pointer, reference, or gsl::Pointer) +/// - Its type is pointer-like (pointer, reference, or gsl::Pointer), OR +/// - Its type is registered for origin tracking (e.g., return type of a +/// [[clang::lifetimebound]] function) /// /// Examples: /// - `int x; x` : has origin (glvalue) @@ -78,7 +128,7 @@ bool hasOrigins(QualType QT) { /// - `std::string_view{}` : has 1 origin (prvalue of pointer type) /// - `42` : no origin (prvalue of non-pointer type) /// - `x + y` : (where x, y are int) → no origin (prvalue of non-pointer type) -bool hasOrigins(const Expr *E) { +bool OriginManager::hasOrigins(const Expr *E) const { return E->isGLValue() || hasOrigins(E->getType()); } @@ -99,8 +149,9 @@ bool doesDeclHaveStorage(const ValueDecl *D) { return !D->getType()->isReferenceType(); } -OriginManager::OriginManager(ASTContext &AST, const Decl *D) : AST(AST) { - // Create OriginList for 'this' expr. +OriginManager::OriginManager(ASTContext &AST) : AST(AST) {} + +void OriginManager::initializeThisOrigins(const Decl *D) { const auto *MD = llvm::dyn_cast_or_null<CXXMethodDecl>(D); if (!MD || !MD->isInstance()) return; @@ -232,8 +283,27 @@ const Origin &OriginManager::getOrigin(OriginID ID) const { void OriginManager::collectMissingOrigins(Stmt &FunctionBody, LifetimeSafetyStats &LSStats) { - MissingOriginCollector Collector(this->ExprToList, LSStats); + MissingOriginCollector Collector(this->ExprToList, *this, LSStats); Collector.TraverseStmt(const_cast<Stmt *>(&FunctionBody)); } +void OriginManager::collectLifetimeboundOriginTypes(AnalysisDeclContext &AC) { + LifetimeboundOriginTypeCollector Collector(*this); + if (Stmt *Body = AC.getBody()) + Collector.TraverseStmt(Body); + if (const auto *CD = dyn_cast<CXXConstructorDecl>(AC.getDecl())) + for (const auto *Init : CD->inits()) + Collector.TraverseStmt(Init->getInit()); +} + +void OriginManager::registerLifetimeboundOriginType(QualType QT) { + // TODO: Support [[gsl::Owner]] return types. For now, skip them because they + // change owner origin-list shape and can break GSL construction flow. + if (!QT->getAsCXXRecordDecl() || isGslOwnerType(QT) || hasOrigins(QT)) + return; + + LifetimeboundOriginTypes.insert( + QT->getCanonicalTypeUnqualified().getTypePtr()); +} + } // namespace clang::lifetimes::internal diff --git a/clang/test/Sema/warn-lifetime-analysis-nocfg.cpp b/clang/test/Sema/warn-lifetime-analysis-nocfg.cpp index a725119444e2f..d58f23e4b554c 100644 --- a/clang/test/Sema/warn-lifetime-analysis-nocfg.cpp +++ b/clang/test/Sema/warn-lifetime-analysis-nocfg.cpp @@ -576,12 +576,13 @@ struct FooView { FooView(const Foo& foo [[clang::lifetimebound]]); }; FooView test3(int i, std::optional<Foo> a) { - // FIXME: Detect this using the CFG-based lifetime analysis. - // Origin tracking for non-pointers type retured from lifetimebound fn is missing. - // https://github.com/llvm/llvm-project/issues/163600 if (i) - return *a; // expected-warning {{address of stack memory}} - return a.value(); // expected-warning {{address of stack memory}} + return *a; // expected-warning {{address of stack memory}} \ + // cfg-warning {{address of stack memory is returned later}} \ + // cfg-note {{returned here}} + return a.value(); // expected-warning {{address of stack memory}} \ + // cfg-warning {{address of stack memory is returned later}} \ + // cfg-note {{returned here}} } } // namespace GH93386 @@ -591,11 +592,10 @@ struct UrlAnalyzed { }; std::string StrCat(std::string_view, std::string_view); void test1() { - // FIXME: Detect this using the CFG-based lifetime analysis. - // Origin tracking for non-pointers type retured from lifetimebound fn is missing. - // https://github.com/llvm/llvm-project/issues/163600 - UrlAnalyzed url(StrCat("abc", "bcd")); // expected-warning {{object backing the pointer will be destroyed}} - use(url); + UrlAnalyzed url(StrCat("abc", "bcd")); // expected-warning {{object backing the pointer will be destroyed}} \ + // cfg-warning {{object whose reference is captured does not live long enough}} \ + // cfg-note {{destroyed here}} + use(url); // cfg-note {{later used here}} } std::string_view ReturnStringView(std::string_view abc [[clang::lifetimebound]]); diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp index 76d43445f8636..7119b0d9da832 100644 --- a/clang/test/Sema/warn-lifetime-safety.cpp +++ b/clang/test/Sema/warn-lifetime-safety.cpp @@ -909,7 +909,6 @@ void lifetimebound_return_reference() { (void)*ptr; // expected-note {{later used here}} } -// FIXME: No warning for non gsl::Pointer types. Origin tracking is only supported for pointer types. struct LifetimeBoundCtor { LifetimeBoundCtor(); LifetimeBoundCtor(const MyObj& obj [[clang::lifetimebound]]); @@ -919,9 +918,9 @@ void lifetimebound_ctor() { LifetimeBoundCtor v; { MyObj obj; - v = obj; - } - (void)v; + v = obj; // expected-warning {{object whose reference is captured does not live long enough}} + } // expected-note {{destroyed here}} + (void)v; // expected-note {{later used here}} } View lifetimebound_return_of_local() { @@ -2126,3 +2125,245 @@ void indexing_with_static_operator() { } } // namespace static_call_operator + +namespace track_origins_for_lifetimebound_record_type { + +template <class T> void use(T); + +struct S { + S(); + S(const std::string &s [[clang::lifetimebound]]); + + S return_self_after_registration() const; +}; + +S getS(const std::string &s [[clang::lifetimebound]]); + +void from_free_function() { + S s = getS(std::string("temp")); // expected-warning {{object whose reference is captured does not live long enough}} \ + // expected-note {{destroyed here}} + use(s); // expected-note {{later used here}} +} + +void from_constructor() { + S s(std::string("temp")); // expected-warning {{object whose reference is captured does not live long enough}} \ + // expected-note {{destroyed here}} + use(s); // expected-note {{later used here}} +} + +struct Factory { + S make(const std::string &s [[clang::lifetimebound]]); + static S create(const std::string &s [[clang::lifetimebound]]); + S makeThis() const [[clang::lifetimebound]]; +}; + +void from_method() { + Factory f; + S s = f.make(std::string("temp")); // expected-warning {{object whose reference is captured does not live long enough}} \ + // expected-note {{destroyed here}} + use(s); // expected-note {{later used here}} +} + +void from_static_method() { + S s = Factory::create(std::string("temp")); // expected-warning {{object whose reference is captured does not live long enough}} \ + // expected-note {{destroyed here}} + use(s); // expected-note {{later used here}} +} + +void from_lifetimebound_this_method() { + S value; + { + Factory f; + value = f.makeThis(); // expected-warning {{object whose reference is captured does not live long enough}} + } // expected-note {{destroyed here}} + use(value); // expected-note {{later used here}} +} + +void across_scope() { + S s{}; + { + std::string str{"abc"}; + s = getS(str); // expected-warning {{object whose reference is captured does not live long enough}} + } // expected-note {{destroyed here}} + use(s); // expected-note {{later used here}} +} + +void same_scope() { + std::string str{"abc"}; + S s = getS(str); + use(s); +} + +S copy_propagation() { + std::string str{"abc"}; + S a = getS(str); // expected-warning {{address of stack memory is returned later}} + S b = a; + return b; // expected-note {{returned here}} +} + +void assignment_propagation() { + S a, b; + { + std::string str{"abc"}; + a = getS(str); // expected-warning {{object whose reference is captured does not live long enough}} + b = a; + } // expected-note {{destroyed here}} + use(b); // expected-note {{later used here}} +} + +S getSNoAnnotation(const std::string &s); + +void no_annotation() { + S s = getSNoAnnotation(std::string("temp")); + use(s); +} + +void mix_annotated_and_not() { + S s1 = getS(std::string("temp")); // expected-warning {{object whose reference is captured does not live long enough}} \ + // expected-note {{destroyed here}} + S s2 = getSNoAnnotation(std::string("temp")); + use(s1); // expected-note {{later used here}} + use(s2); +} + +S getS2(const std::string &a [[clang::lifetimebound]], const std::string &b [[clang::lifetimebound]]); + +S multiple_lifetimebound_params() { + std::string str{"abc"}; + S s = getS2(str, std::string("temp")); // expected-warning {{address of stack memory is returned later}} \ + // expected-warning {{object whose reference is captured does not live long enough}} \ + // expected-note {{destroyed here}} + return s; // expected-note {{returned here}} \ + // expected-note {{later used here}} +} + +int getInt(const std::string &s [[clang::lifetimebound]]); + +void primitive_return() { + int i = getInt(std::string("temp")); + use(i); +} + +template <class T> +T make(const std::string &s [[clang::lifetimebound]]); + +void from_template_instantiation() { + S s = make<S>(std::string("temp")); // expected-warning {{object whose reference is captured does not live long enough}} \ + // expected-note {{destroyed here}} + use(s); // expected-note {{later used here}} +} + +struct FieldInitFromLifetimebound { + S value; // function-note {{this field dangles}} + FieldInitFromLifetimebound() : value(getS(std::string("temp"))) {} // function-warning {{address of stack memory escapes to a field}} +}; + +S S::return_self_after_registration() const { + std::string s{"abc"}; + getS(s); + return *this; +} + +struct SWithUserDefinedCopyLikeOps { + SWithUserDefinedCopyLikeOps(); + SWithUserDefinedCopyLikeOps(const std::string &s [[clang::lifetimebound]]) : owned(s), data(s) {} + + SWithUserDefinedCopyLikeOps(const SWithUserDefinedCopyLikeOps &other) : owned("copy"), data(owned) {} + + SWithUserDefinedCopyLikeOps &operator=(const SWithUserDefinedCopyLikeOps &) { + owned = "copy"; + data = owned; + return *this; + } + + std::string owned; + std::string_view data; +}; + +SWithUserDefinedCopyLikeOps getSWithUserDefinedCopyLikeOps(const std::string &s [[clang::lifetimebound]]); + +SWithUserDefinedCopyLikeOps user_defined_copy_ctor_should_not_assume_origin_propagation() { + std::string str{"abc"}; + SWithUserDefinedCopyLikeOps s = getSWithUserDefinedCopyLikeOps(str); + SWithUserDefinedCopyLikeOps copy = s; // Copy is rescued by user-defined copy constructor, so should not warn. + return copy; +} + +void user_defined_assignment_should_not_assume_origin_propagation() { + SWithUserDefinedCopyLikeOps dst; + { + std::string str{"abc"}; + SWithUserDefinedCopyLikeOps src = getSWithUserDefinedCopyLikeOps(str); + dst = src; + } + use(dst); +} + +struct SWithOriginPropagatingCopy { + SWithOriginPropagatingCopy(); + SWithOriginPropagatingCopy(const std::string &s [[clang::lifetimebound]]) : data(s) {} + SWithOriginPropagatingCopy(const SWithOriginPropagatingCopy &other) : data(other.data) {} + std::string_view data; +}; + +SWithOriginPropagatingCopy getSWithOriginPropagatingCopy(const std::string &s [[clang::lifetimebound]]); + +// FIXME: False negative. User-defined copy ctor may propagate origins. +SWithOriginPropagatingCopy user_defined_copy_with_origin_propagation() { + std::string str{"abc"}; + SWithOriginPropagatingCopy s = getSWithOriginPropagatingCopy(str); + SWithOriginPropagatingCopy copy = s; + return copy; // Should warn. +} + +struct DefaultedOuter { + DefaultedOuter(); + DefaultedOuter(const std::string &s [[clang::lifetimebound]]) : inner(s) {} + SWithUserDefinedCopyLikeOps inner; +}; + +DefaultedOuter getDefaultedOuter(const std::string &s [[clang::lifetimebound]]); + +// FIXME: False positive. The defaulted outer copy ctor invokes +// SWithUserDefinedCopyLikeOps's user-defined copy ctor, so `copy` should be +// semantically safe. +DefaultedOuter nested_defaulted_outer_with_user_defined_inner() { + std::string str{"abc"}; + DefaultedOuter o = getDefaultedOuter(str); // expected-warning {{address of stack memory is returned later}} + DefaultedOuter copy = o; + return copy; // expected-note {{returned here}} +} + +std::string_view getSV(S s [[clang::lifetimebound]]); + +// FIXME: False negative. Non-pointer/ref/gsl::Pointer parameter types marked +// [[clang::lifetimebound]] are not registered for origin tracking. +void dangling_view_from_non_pointer_param() { + std::string_view sv; + { + S s; + sv = getSV(s); + } + use(sv); // Should warn. +} + +const S &getRef(const std::string &s [[clang::lifetimebound]]); + +// FIXME: False negative. The analysis tracks the returned reference, +// but loses that information when it is copied into a new `S` object. +S from_ref() { + std::string str{"abc"}; + S s = getRef(str); + return s; // Should warn. +} + +MyObj getMyObj(const MyObj &obj [[clang::lifetimebound]]); + +void gsl_owner_return_does_not_crash() { + MyObj obj; + View v = obj; + getMyObj(obj); + use(v); +} + +} // namespace track_origins_for_lifetimebound_record_type >From e6469c2d5953d6100fde75de5a66acdc36f42a57 Mon Sep 17 00:00:00 2001 From: Alex Wang <[email protected]> Date: Tue, 24 Mar 2026 22:35:39 -0400 Subject: [PATCH 2/8] remove FIXME --- clang/test/Sema/warn-lifetime-safety.cpp | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp index 7119b0d9da832..577f8e56df864 100644 --- a/clang/test/Sema/warn-lifetime-safety.cpp +++ b/clang/test/Sema/warn-lifetime-safety.cpp @@ -2299,6 +2299,14 @@ void user_defined_assignment_should_not_assume_origin_propagation() { use(dst); } +const S &getRef(const std::string &s [[clang::lifetimebound]]); + +S from_ref() { + std::string str{"abc"}; + S s = getRef(str); + return s; +} + struct SWithOriginPropagatingCopy { SWithOriginPropagatingCopy(); SWithOriginPropagatingCopy(const std::string &s [[clang::lifetimebound]]) : data(s) {} @@ -2347,16 +2355,6 @@ void dangling_view_from_non_pointer_param() { use(sv); // Should warn. } -const S &getRef(const std::string &s [[clang::lifetimebound]]); - -// FIXME: False negative. The analysis tracks the returned reference, -// but loses that information when it is copied into a new `S` object. -S from_ref() { - std::string str{"abc"}; - S s = getRef(str); - return s; // Should warn. -} - MyObj getMyObj(const MyObj &obj [[clang::lifetimebound]]); void gsl_owner_return_does_not_crash() { >From ca7edf1bac2090e7b5cc019f059391c29ee32244 Mon Sep 17 00:00:00 2001 From: Alex Wang <[email protected]> Date: Wed, 25 Mar 2026 12:19:40 -0400 Subject: [PATCH 3/8] make collectLifetimeboundOriginTypes and initializeThisOrigins private --- .../Analysis/Analyses/LifetimeSafety/Facts.h | 3 +-- .../Analyses/LifetimeSafety/Origins.h | 20 +++++++-------- .../LifetimeSafety/LifetimeSafety.cpp | 3 --- clang/lib/Analysis/LifetimeSafety/Origins.cpp | 25 +++++++++++++------ 4 files changed, 27 insertions(+), 24 deletions(-) diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h index e821e96527deb..e852f66a3fde2 100644 --- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h +++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h @@ -317,8 +317,7 @@ class TestPointFact : public Fact { class FactManager { public: - FactManager(const AnalysisDeclContext &AC, const CFG &Cfg) - : OriginMgr(AC.getASTContext()) { + FactManager(const AnalysisDeclContext &AC, const CFG &Cfg) : OriginMgr(AC) { BlockToFacts.resize(Cfg.getNumBlockIDs()); } diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Origins.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Origins.h index 12148d9b29c9c..a918e874a6fae 100644 --- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Origins.h +++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Origins.h @@ -124,11 +124,7 @@ bool doesDeclHaveStorage(const ValueDecl *D); /// variables and expressions. class OriginManager { public: - explicit OriginManager(ASTContext &AST); - - /// Must be called after collectLifetimeboundOriginTypes() to ensure - /// ThisOrigins reflects the complete set of tracked types. - void initializeThisOrigins(const Decl *D); + explicit OriginManager(const AnalysisDeclContext &AC); /// Gets or creates the OriginList for a given ValueDecl. /// @@ -166,12 +162,6 @@ class OriginManager { /// Collects statistics about expressions that lack associated origins. void collectMissingOrigins(Stmt &FunctionBody, LifetimeSafetyStats &LSStats); - /// Pre-scans the function body (and constructor init lists) to discover - /// return types of [[clang::lifetimebound]] calls, registering them for - /// origin tracking. - void collectLifetimeboundOriginTypes(AnalysisDeclContext &AC); - void registerLifetimeboundOriginType(QualType QT); - private: OriginID getNextOriginID() { return NextOriginID++; } @@ -181,6 +171,14 @@ class OriginManager { template <typename T> OriginList *buildListForType(QualType QT, const T *Node); + void initializeThisOrigins(const Decl *D); + + /// Pre-scans the function body (and constructor init lists) to discover + /// return types of [[clang::lifetimebound]] calls, registering them for + /// origin tracking. + void collectLifetimeboundOriginTypes(const AnalysisDeclContext &AC); + void registerLifetimeboundOriginType(QualType QT); + ASTContext &AST; OriginID NextOriginID{0}; /// TODO(opt): Profile and evaluate the usefulness of small buffer diff --git a/clang/lib/Analysis/LifetimeSafety/LifetimeSafety.cpp b/clang/lib/Analysis/LifetimeSafety/LifetimeSafety.cpp index 56a187202d8fa..714f979fa5ee7 100644 --- a/clang/lib/Analysis/LifetimeSafety/LifetimeSafety.cpp +++ b/clang/lib/Analysis/LifetimeSafety/LifetimeSafety.cpp @@ -71,9 +71,6 @@ void LifetimeSafetyAnalysis::run() { FactMgr = std::make_unique<FactManager>(AC, Cfg); - FactMgr->getOriginMgr().collectLifetimeboundOriginTypes(AC); - FactMgr->getOriginMgr().initializeThisOrigins(AC.getDecl()); - FactsGenerator FactGen(*FactMgr, AC); FactGen.run(); diff --git a/clang/lib/Analysis/LifetimeSafety/Origins.cpp b/clang/lib/Analysis/LifetimeSafety/Origins.cpp index 2655835317d9b..9e67476174e17 100644 --- a/clang/lib/Analysis/LifetimeSafety/Origins.cpp +++ b/clang/lib/Analysis/LifetimeSafety/Origins.cpp @@ -54,8 +54,6 @@ class MissingOriginCollector class LifetimeboundOriginTypeCollector : public RecursiveASTVisitor<LifetimeboundOriginTypeCollector> { public: - LifetimeboundOriginTypeCollector(OriginManager &OM) : OM(OM) {} - bool VisitCallExpr(const CallExpr *CE) { if (const auto *FD = CE->getDirectCallee()) collect(FD, FD->getReturnType()); @@ -69,8 +67,12 @@ class LifetimeboundOriginTypeCollector bool shouldVisitLambdaBody() const { return false; } + const llvm::SmallVector<QualType> &getCollectedTypes() const { + return CollectedTypes; + } + private: - OriginManager &OM; + llvm::SmallVector<QualType> CollectedTypes; void collect(const FunctionDecl *FD, QualType RetType) { if (!FD) @@ -80,13 +82,13 @@ class LifetimeboundOriginTypeCollector if (const auto *MD = dyn_cast<CXXMethodDecl>(FD); MD && MD->isInstance() && !isa<CXXConstructorDecl>(MD) && implicitObjectParamIsLifetimeBound(MD)) { - OM.registerLifetimeboundOriginType(RetType); + CollectedTypes.push_back(RetType); return; } for (const auto *Param : FD->parameters()) { if (Param->hasAttr<LifetimeBoundAttr>()) { - OM.registerLifetimeboundOriginType(RetType); + CollectedTypes.push_back(RetType); return; } } @@ -149,7 +151,11 @@ bool doesDeclHaveStorage(const ValueDecl *D) { return !D->getType()->isReferenceType(); } -OriginManager::OriginManager(ASTContext &AST) : AST(AST) {} +OriginManager::OriginManager(const AnalysisDeclContext &AC) + : AST(AC.getASTContext()) { + collectLifetimeboundOriginTypes(AC); + initializeThisOrigins(AC.getDecl()); +} void OriginManager::initializeThisOrigins(const Decl *D) { const auto *MD = llvm::dyn_cast_or_null<CXXMethodDecl>(D); @@ -287,13 +293,16 @@ void OriginManager::collectMissingOrigins(Stmt &FunctionBody, Collector.TraverseStmt(const_cast<Stmt *>(&FunctionBody)); } -void OriginManager::collectLifetimeboundOriginTypes(AnalysisDeclContext &AC) { - LifetimeboundOriginTypeCollector Collector(*this); +void OriginManager::collectLifetimeboundOriginTypes( + const AnalysisDeclContext &AC) { + LifetimeboundOriginTypeCollector Collector; if (Stmt *Body = AC.getBody()) Collector.TraverseStmt(Body); if (const auto *CD = dyn_cast<CXXConstructorDecl>(AC.getDecl())) for (const auto *Init : CD->inits()) Collector.TraverseStmt(Init->getInit()); + for (QualType QT : Collector.getCollectedTypes()) + registerLifetimeboundOriginType(QT); } void OriginManager::registerLifetimeboundOriginType(QualType QT) { >From 685157b554e99d7e9d05bd894a7aca526dafa683 Mon Sep 17 00:00:00 2001 From: Alex Wang <[email protected]> Date: Wed, 25 Mar 2026 12:37:09 -0400 Subject: [PATCH 4/8] update comment --- .../include/clang/Analysis/Analyses/LifetimeSafety/Origins.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Origins.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Origins.h index a918e874a6fae..3940c893fca98 100644 --- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Origins.h +++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Origins.h @@ -189,8 +189,8 @@ class OriginManager { llvm::DenseMap<const clang::Expr *, OriginList *> ExprToList; std::optional<OriginList *> ThisOrigins; /// Types that are not inherently pointer-like but require origin tracking - /// because they are returned from functions with [[clang::lifetimebound]] - /// parameters. + /// because of lifetime annotations (e.g., [[clang::lifetimebound]]) on + /// functions that return them. llvm::DenseSet<const Type *> LifetimeboundOriginTypes; }; } // namespace clang::lifetimes::internal >From 12c9bd3d2a679d76ec7fc4725c45a02dabb57b4b Mon Sep 17 00:00:00 2001 From: Alex Wang <[email protected]> Date: Wed, 25 Mar 2026 14:42:02 -0400 Subject: [PATCH 5/8] update test --- clang/test/Sema/warn-lifetime-safety.cpp | 32 ++++++++++++++++++++---- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp index 577f8e56df864..1c461dd960936 100644 --- a/clang/test/Sema/warn-lifetime-safety.cpp +++ b/clang/test/Sema/warn-lifetime-safety.cpp @@ -2135,6 +2135,7 @@ struct S { S(const std::string &s [[clang::lifetimebound]]); S return_self_after_registration() const; + std::string_view getData() const [[clang::lifetimebound]]; }; S getS(const std::string &s [[clang::lifetimebound]]); @@ -2147,8 +2148,8 @@ void from_free_function() { void from_constructor() { S s(std::string("temp")); // expected-warning {{object whose reference is captured does not live long enough}} \ - // expected-note {{destroyed here}} - use(s); // expected-note {{later used here}} + // expected-note {{destroyed here}} + use(s); // expected-note {{later used here}} } struct Factory { @@ -2175,8 +2176,8 @@ void from_lifetimebound_this_method() { { Factory f; value = f.makeThis(); // expected-warning {{object whose reference is captured does not live long enough}} - } // expected-note {{destroyed here}} - use(value); // expected-note {{later used here}} + } // expected-note {{destroyed here}} + use(value); // expected-note {{later used here}} } void across_scope() { @@ -2185,7 +2186,7 @@ void across_scope() { std::string str{"abc"}; s = getS(str); // expected-warning {{object whose reference is captured does not live long enough}} } // expected-note {{destroyed here}} - use(s); // expected-note {{later used here}} + use(s); // expected-note {{later used here}} } void same_scope() { @@ -2239,6 +2240,8 @@ S multiple_lifetimebound_params() { int getInt(const std::string &s [[clang::lifetimebound]]); +// TODO: Diagnose [[clang::lifetimebound]] on functions whose return value +// cannot refer to any object (e.g., returning int or enum). void primitive_return() { int i = getInt(std::string("temp")); use(i); @@ -2364,4 +2367,23 @@ void gsl_owner_return_does_not_crash() { use(v); } +std::unique_ptr<S> getUniqueS(const std::string &s [[clang::lifetimebound]]); + +// FIXME: GSL Owner return types of lifetimebound calls are not yet tracked. +void owner_return_unique_ptr_s() { + auto ptr = getUniqueS(std::string("temp")); + (void)ptr; // Should warn. +} + +// FIXME: The warning here is from the local unique_ptr being destroyed on +// return, not from lifetimebound origin tracking. GSL Owner return types are +// not yet tracked. +std::string_view return_dangling_view_through_owner() { + std::string local; + auto ups = getUniqueS(local); + S* s = ups.get(); // expected-warning {{address of stack memory is returned later}} + std::string_view sv = s->getData(); + return sv; // expected-note {{returned here}} +} + } // namespace track_origins_for_lifetimebound_record_type >From 10da73123e068c57f80fd012102caa8c10c617b2 Mon Sep 17 00:00:00 2001 From: Alex Wang <[email protected]> Date: Wed, 25 Mar 2026 17:26:12 -0400 Subject: [PATCH 6/8] support gsl::owner return type origin tracking --- clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp | 6 +++--- clang/lib/Analysis/LifetimeSafety/Origins.cpp | 4 +--- clang/test/Sema/warn-lifetime-safety.cpp | 11 +++++------ 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp index 03b297c30f008..f0507ebc9f04a 100644 --- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp +++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp @@ -707,9 +707,9 @@ void FactsGenerator::handleFunctionCall(const Expr *Call, ArgList = getRValueOrigins(Args[I], ArgList); } if (isGslOwnerType(Args[I]->getType())) { - // GSL construction creates a view that borrows from arguments. - // This implies flowing origins through the list structure. - flow(CallList, ArgList, KillSrc); + CurrentBlockFacts.push_back(FactMgr.createFact<OriginFlowFact>( + CallList->getOuterOriginID(), ArgList->getOuterOriginID(), + KillSrc)); KillSrc = false; } } else if (shouldTrackPointerImplicitObjectArg(I)) { diff --git a/clang/lib/Analysis/LifetimeSafety/Origins.cpp b/clang/lib/Analysis/LifetimeSafety/Origins.cpp index 9e67476174e17..fc61b1e260eba 100644 --- a/clang/lib/Analysis/LifetimeSafety/Origins.cpp +++ b/clang/lib/Analysis/LifetimeSafety/Origins.cpp @@ -306,9 +306,7 @@ void OriginManager::collectLifetimeboundOriginTypes( } void OriginManager::registerLifetimeboundOriginType(QualType QT) { - // TODO: Support [[gsl::Owner]] return types. For now, skip them because they - // change owner origin-list shape and can break GSL construction flow. - if (!QT->getAsCXXRecordDecl() || isGslOwnerType(QT) || hasOrigins(QT)) + if (!QT->getAsCXXRecordDecl() || hasOrigins(QT)) return; LifetimeboundOriginTypes.insert( diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp index 1c461dd960936..ecc0844b73a9d 100644 --- a/clang/test/Sema/warn-lifetime-safety.cpp +++ b/clang/test/Sema/warn-lifetime-safety.cpp @@ -2360,7 +2360,7 @@ void dangling_view_from_non_pointer_param() { MyObj getMyObj(const MyObj &obj [[clang::lifetimebound]]); -void gsl_owner_return_does_not_crash() { +void gsl_owner_return() { MyObj obj; View v = obj; getMyObj(obj); @@ -2369,15 +2369,14 @@ void gsl_owner_return_does_not_crash() { std::unique_ptr<S> getUniqueS(const std::string &s [[clang::lifetimebound]]); -// FIXME: GSL Owner return types of lifetimebound calls are not yet tracked. void owner_return_unique_ptr_s() { - auto ptr = getUniqueS(std::string("temp")); - (void)ptr; // Should warn. + auto ptr = getUniqueS(std::string("temp")); // expected-warning {{object whose reference is captured does not live long enough}} \ + // expected-note {{destroyed here}} + (void)ptr; // expected-note {{later used here}} } // FIXME: The warning here is from the local unique_ptr being destroyed on -// return, not from lifetimebound origin tracking. GSL Owner return types are -// not yet tracked. +// return. The chain breaks and doesn't trace back to `auto ups = getUniqueS(local)`. std::string_view return_dangling_view_through_owner() { std::string local; auto ups = getUniqueS(local); >From 52b6562b3330794105d7e6dc387954e85f62fa3c Mon Sep 17 00:00:00 2001 From: Zhijie Wang <[email protected]> Date: Wed, 25 Mar 2026 18:19:53 -0400 Subject: [PATCH 7/8] add test --- clang/test/Sema/Inputs/lifetime-analysis.h | 1 + clang/test/Sema/warn-lifetime-safety.cpp | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/clang/test/Sema/Inputs/lifetime-analysis.h b/clang/test/Sema/Inputs/lifetime-analysis.h index 0b6bdaef83f9d..2cb9d010e4743 100644 --- a/clang/test/Sema/Inputs/lifetime-analysis.h +++ b/clang/test/Sema/Inputs/lifetime-analysis.h @@ -191,6 +191,7 @@ template<typename T> struct unique_ptr { unique_ptr(); unique_ptr(unique_ptr<T>&&); + unique_ptr& operator=(unique_ptr<T>&&); ~unique_ptr(); T* release(); T &operator*(); diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp index ecc0844b73a9d..40714077c51e3 100644 --- a/clang/test/Sema/warn-lifetime-safety.cpp +++ b/clang/test/Sema/warn-lifetime-safety.cpp @@ -2385,4 +2385,18 @@ std::string_view return_dangling_view_through_owner() { return sv; // expected-note {{returned here}} } +// FIXME: False negative. The loan on `local` doesn't reach `s->getData()`: +// (1) move assignment is not defaulted, so origins don't propagate to `ups`, +// and (2) even if they did, `ups.get()` connects to the owner's storage +// origin, not the value origin holding the loan. +void owner_outlives_lifetimebound_source() { + std::unique_ptr<S> ups; + { + std::string local; + ups = getUniqueS(local); + } + S* s = ups.get(); + use(s->getData()); // Should warn. +} + } // namespace track_origins_for_lifetimebound_record_type >From dcc72178be8843a77a445145b15bc1a924c89c42 Mon Sep 17 00:00:00 2001 From: Zhijie Wang <[email protected]> Date: Wed, 25 Mar 2026 20:55:38 -0400 Subject: [PATCH 8/8] update comment --- clang/test/Sema/warn-lifetime-safety.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp index 40714077c51e3..699bff3a463bd 100644 --- a/clang/test/Sema/warn-lifetime-safety.cpp +++ b/clang/test/Sema/warn-lifetime-safety.cpp @@ -2238,10 +2238,10 @@ S multiple_lifetimebound_params() { // expected-note {{later used here}} } -int getInt(const std::string &s [[clang::lifetimebound]]); - // TODO: Diagnose [[clang::lifetimebound]] on functions whose return value // cannot refer to any object (e.g., returning int or enum). +int getInt(const std::string &s [[clang::lifetimebound]]); + void primitive_return() { int i = getInt(std::string("temp")); use(i); _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
