[clang] [LifetimeSafety] Track origins for lifetimebound calls returning record types (PR #187917)
Zhijie Wang via cfe-commits
cfe-commits at lists.llvm.org
Wed Mar 25 17:55:54 PDT 2026
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 <yesterda9 at gmail.com>
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 <yesterda9 at gmail.com>
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 <yesterda9 at gmail.com>
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 <yesterda9 at gmail.com>
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 <yesterda9 at gmail.com>
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 <yesterda9 at gmail.com>
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 <yesterda9 at gmail.com>
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 <yesterda9 at gmail.com>
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);
More information about the cfe-commits
mailing list