[clang] 46bbbde - [LifetimeSafety] Add lifetimebound inference for std::make_unique (#191632)
via cfe-commits
cfe-commits at lists.llvm.org
Thu Apr 16 06:06:50 PDT 2026
Author: Utkarsh Saxena
Date: 2026-04-16T18:36:45+05:30
New Revision: 46bbbde1f3c3bf3a00ae19c3157f7425899b1e87
URL: https://github.com/llvm/llvm-project/commit/46bbbde1f3c3bf3a00ae19c3157f7425899b1e87
DIFF: https://github.com/llvm/llvm-project/commit/46bbbde1f3c3bf3a00ae19c3157f7425899b1e87.diff
LOG: [LifetimeSafety] Add lifetimebound inference for std::make_unique (#191632)
Enhanced lifetime safety analysis to support `std::make_unique` by
propagating `lifetimebound` attributes from constructor parameters to
`make_unique` parameters.
- Added special handling for `std::make_unique` in
`inferLifetimeBoundAttribute()` to automatically propagate
`lifetimebound` attributes from the constructed type's constructor
parameters to the corresponding `make_unique` parameters
- Extended GSL owner type handling in assignment operations within
`FactsGenerator::VisitCXXOperatorCallExpr()`
`std::make_unique` is a common factory function that forwards arguments
to constructors. Without this enhancement, lifetime safety analysis
couldn't detect when `make_unique` was being used to create objects with
lifetimebound dependencies, leading to missed warnings about potential
dangling references. This change ensures that lifetime safety analysis
works seamlessly with modern C++ idioms using smart pointer factory
functions.
---
**Current Limitation**: Lifetimebound propagation only occurs when the
constructor parameter is a reference type. This restriction avoids
incorrect loan tracking when value types (pointers, view types like
`string_view`) are passed through forwarding references to
`make_unique`. However, this means some legitimate dangling scenarios
involving value-type parameters are not detected. This limitation
presents an opportunity to experiment with `clang::lifetimebound(2)` to
distinguish between parameter categories and enable more precise
tracking for forwarding references and multi-level pointers in general.
_(AI-assisted with HITL)_
Added:
Modified:
clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
clang/lib/Analysis/LifetimeSafety/Origins.cpp
clang/lib/Sema/SemaAttr.cpp
clang/lib/Sema/SemaTemplateInstantiateDecl.cpp
clang/test/Sema/Inputs/lifetime-analysis.h
clang/test/Sema/warn-lifetime-analysis-nocfg.cpp
clang/test/Sema/warn-lifetime-safety-dangling-field.cpp
clang/test/Sema/warn-lifetime-safety-suggestions.cpp
clang/test/Sema/warn-lifetime-safety.cpp
Removed:
################################################################################
diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index 5adf842e5f6d0..0dee1c2c447cb 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -507,7 +507,8 @@ void FactsGenerator::VisitCXXOperatorCallExpr(const CXXOperatorCallExpr *OCE) {
hasOrigins(OCE->getArg(0)->getType())) {
// Pointer-like types: assignment inherently propagates origins.
QualType LHSTy = OCE->getArg(0)->getType();
- if (LHSTy->isPointerOrReferenceType() || isGslPointerType(LHSTy)) {
+ if (LHSTy->isPointerOrReferenceType() || isGslPointerType(LHSTy) ||
+ isGslOwnerType(LHSTy)) {
handleAssignment(OCE->getArg(0), OCE->getArg(1));
return;
}
diff --git a/clang/lib/Analysis/LifetimeSafety/Origins.cpp b/clang/lib/Analysis/LifetimeSafety/Origins.cpp
index 033cbdd75352c..37bd8a3ccc6bb 100644
--- a/clang/lib/Analysis/LifetimeSafety/Origins.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Origins.cpp
@@ -68,6 +68,7 @@ class LifetimeAnnotatedOriginTypeCollector
}
bool shouldVisitLambdaBody() const { return false; }
+ bool shouldVisitTemplateInstantiations() const { return true; }
const llvm::SmallVector<QualType> &getCollectedTypes() const {
return CollectedTypes;
diff --git a/clang/lib/Sema/SemaAttr.cpp b/clang/lib/Sema/SemaAttr.cpp
index 7c79f954e6743..ffd546138008a 100644
--- a/clang/lib/Sema/SemaAttr.cpp
+++ b/clang/lib/Sema/SemaAttr.cpp
@@ -221,6 +221,20 @@ void Sema::inferGslOwnerPointerAttribute(CXXRecordDecl *Record) {
inferGslPointerAttribute(Record, Record);
}
+// This uses recursion and is only safe for small Stmt (e.g., the body of
+// std::make_unique). Do not use this for other Stmt without addressing the
+// potential for stack exhaustion.
+static const CXXNewExpr *findCXXNewExpr(const Stmt *S) {
+ if (!S)
+ return nullptr;
+ if (const auto *E = dyn_cast<CXXNewExpr>(S))
+ return E;
+ for (const Stmt *Child : S->children())
+ if (const CXXNewExpr *E = findCXXNewExpr(Child))
+ return E;
+ return nullptr;
+}
+
void Sema::inferLifetimeBoundAttribute(FunctionDecl *FD) {
if (FD->getNumParams() == 0)
return;
@@ -250,6 +264,29 @@ void Sema::inferLifetimeBoundAttribute(FunctionDecl *FD) {
}
return;
}
+
+ // Handle std::make_unique to propagate lifetimebound attributes from the
+ // constructed type's constructor to make_unique's parameters by looking
+ // into its body.
+ if (FD->getDeclName().isIdentifier() && FD->getName() == "make_unique") {
+ const FunctionDecl *BodyDecl = nullptr;
+ if (FD->getBody(BodyDecl))
+ if (const CXXNewExpr *NewExpr = findCXXNewExpr(BodyDecl->getBody()))
+ if (const CXXConstructExpr *ConstructExpr = NewExpr->getConstructExpr())
+ if (const CXXConstructorDecl *Ctor = ConstructExpr->getConstructor())
+ for (unsigned I = 0; I < Ctor->getNumParams(); ++I)
+ // Only infer lifetimebound for references. For pointers and
+ // views, the forwarding reference in make_unique would
+ // incorrectly track the lifetime of the local pointer variable
+ // itself, rather than the data it points to.
+ if (I < FD->getNumParams() &&
+ Ctor->getParamDecl(I)->hasAttr<LifetimeBoundAttr>() &&
+ Ctor->getParamDecl(I)->getType()->isReferenceType())
+ FD->getParamDecl(I)->addAttr(LifetimeBoundAttr::CreateImplicit(
+ Context, FD->getLocation()));
+ return;
+ }
+
if (auto *CMD = dyn_cast<CXXMethodDecl>(FD)) {
const auto *CRD = CMD->getParent();
if (!CRD->isInStdNamespace() || !CRD->getIdentifier())
diff --git a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp
index 09c2482168ab7..81f730cec4683 100644
--- a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp
+++ b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp
@@ -5473,6 +5473,8 @@ TemplateDeclInstantiator::InitFunctionInstantiation(FunctionDecl *New,
SemaRef.InstantiateAttrs(TemplateArgs, Definition, New,
LateAttrs, StartingScope);
+ SemaRef.inferLifetimeBoundAttribute(New);
+
return false;
}
@@ -5967,6 +5969,8 @@ void Sema::InstantiateFunctionDefinition(SourceLocation PointOfInstantiation,
// context seems wrong. Investigate more.
ActOnFinishFunctionBody(Function, Body.get(), /*IsInstantiation=*/true);
+ inferLifetimeBoundAttribute(Function);
+
checkReferenceToTULocalFromOtherTU(Function, PointOfInstantiation);
if (PatternDecl->isDependentContext())
diff --git a/clang/test/Sema/Inputs/lifetime-analysis.h b/clang/test/Sema/Inputs/lifetime-analysis.h
index 2b904f88bc475..9806cce15d8f5 100644
--- a/clang/test/Sema/Inputs/lifetime-analysis.h
+++ b/clang/test/Sema/Inputs/lifetime-analysis.h
@@ -196,6 +196,7 @@ using string = basic_string<char>;
template<typename T>
struct unique_ptr {
unique_ptr();
+ explicit unique_ptr(T*);
unique_ptr(unique_ptr<T>&&);
unique_ptr& operator=(unique_ptr<T>&&);
~unique_ptr();
@@ -205,6 +206,11 @@ struct unique_ptr {
T *get() const;
};
+template<typename T, typename... Args>
+unique_ptr<T> make_unique(Args&&... args) {
+ return unique_ptr<T>(new T(args...));
+}
+
template<typename T>
struct optional {
optional();
diff --git a/clang/test/Sema/warn-lifetime-analysis-nocfg.cpp b/clang/test/Sema/warn-lifetime-analysis-nocfg.cpp
index 2b6c2d3d8bdb6..00d45caae3b09 100644
--- a/clang/test/Sema/warn-lifetime-analysis-nocfg.cpp
+++ b/clang/test/Sema/warn-lifetime-analysis-nocfg.cpp
@@ -1,6 +1,6 @@
// RUN: %clang_cc1 -fsyntax-only -Wdangling -Wdangling-field -Wreturn-stack-address -verify %s
// RUN: %clang_cc1 -fsyntax-only -Wlifetime-safety -Wno-dangling -verify=cfg %s
-// RUN: %clang_cc1 -fsyntax-only -flifetime-safety-inference -fexperimental-lifetime-safety-tu-analysis -Wlifetime-safety -Wno-dangling -verify=cfg %s
+// RUN: %clang_cc1 -fsyntax-only -flifetime-safety-inference -fexperimental-lifetime-safety-tu-analysis -Wlifetime-safety -Wno-dangling -verify=cfg,tu %s
#include "Inputs/lifetime-analysis.h"
@@ -174,6 +174,16 @@ void initLocalGslPtrWithTempOwner() {
use(global2, p2); // cfg-note 2 {{later used here}}
}
+struct LifetimeBoundCtor {
+ LifetimeBoundCtor(const MyIntOwner& obj1 [[clang::lifetimebound]]);
+ LifetimeBoundCtor(std::string_view sv [[clang::lifetimebound]]);
+};
+
+auto lifetimebound_make_unique_single_param() {
+ return std::make_unique<LifetimeBoundCtor>(MyIntOwner{}); // tu-warning {{address of stack memory is returned later}} tu-note {{returned here}}
+}
+
+
struct Unannotated {
typedef std::vector<int>::iterator iterator;
diff --git a/clang/test/Sema/warn-lifetime-safety-dangling-field.cpp b/clang/test/Sema/warn-lifetime-safety-dangling-field.cpp
index 2afcd4a3dd97b..5a4de105f217d 100644
--- a/clang/test/Sema/warn-lifetime-safety-dangling-field.cpp
+++ b/clang/test/Sema/warn-lifetime-safety-dangling-field.cpp
@@ -1,4 +1,5 @@
// RUN: %clang_cc1 -fsyntax-only -Wlifetime-safety -Wno-dangling -verify %s
+// RUN: %clang_cc1 -fsyntax-only -fexperimental-lifetime-safety-tu-analysis -Wlifetime-safety -Wno-dangling -verify=expected,tu %s
#include "Inputs/lifetime-analysis.h"
@@ -209,3 +210,19 @@ struct HasCallback {
};
} // namespace callable_wrappers
+
+namespace MakeUnique {
+struct MyObj {};
+
+struct LifetimeBoundCtor {
+ LifetimeBoundCtor(const MyObj& obj [[clang::lifetimebound]]);
+};
+
+struct HasUniquePtrField {
+ std::unique_ptr<LifetimeBoundCtor> field; // tu-note {{this field dangles}}
+
+ void setWithParam(MyObj obj) {
+ field = std::make_unique<LifetimeBoundCtor>(obj); // tu-warning {{address of stack memory escapes to a field}}
+ }
+};
+} // namespace MakeUnique
diff --git a/clang/test/Sema/warn-lifetime-safety-suggestions.cpp b/clang/test/Sema/warn-lifetime-safety-suggestions.cpp
index d2cf1c175eb57..ace6265864684 100644
--- a/clang/test/Sema/warn-lifetime-safety-suggestions.cpp
+++ b/clang/test/Sema/warn-lifetime-safety-suggestions.cpp
@@ -547,3 +547,25 @@ void uaf_via_inferred_lifetimebound() {
}
} // namespace callable_wrappers
+
+namespace make_unique_suggestion {
+
+struct LifetimeBoundCtor {
+ View v;
+ // FIXME: This test fails to propagate the lifetimebound in ctor if this is inferred (instead of the current explicit annotation).
+ LifetimeBoundCtor(const MyObj& obj [[clang::lifetimebound]]): v(obj) {}
+};
+
+std::unique_ptr<LifetimeBoundCtor> create_target(const MyObj& obj) { // expected-warning {{parameter in intra-TU function should be marked [[clang::lifetimebound]]}}
+ return std::make_unique<LifetimeBoundCtor>(obj); // expected-note {{param returned here}}
+}
+
+void test_inference() {
+ std::unique_ptr<LifetimeBoundCtor> ptr;
+ {
+ MyObj obj;
+ ptr = create_target(obj); // expected-warning {{object whose reference is captured does not live long enough}}
+ } // expected-note {{destroyed here}}
+ (void)ptr; // expected-note {{later used here}}
+}
+} // namespace make_unique_suggestion
diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp
index 082db5d5cbf15..fba5523931b44 100644
--- a/clang/test/Sema/warn-lifetime-safety.cpp
+++ b/clang/test/Sema/warn-lifetime-safety.cpp
@@ -925,6 +925,9 @@ void lifetimebound_return_reference() {
struct LifetimeBoundCtor {
LifetimeBoundCtor();
LifetimeBoundCtor(const MyObj& obj [[clang::lifetimebound]]);
+ LifetimeBoundCtor(const MyObj& obj, int); // not lifetimebound ctor.
+ LifetimeBoundCtor(int* p [[clang::lifetimebound]]);
+ LifetimeBoundCtor(std::string_view sv [[clang::lifetimebound]]);
};
void lifetimebound_ctor() {
@@ -963,6 +966,109 @@ void lifetimebound_ctor_static_cast() {
(void)v; // expected-note {{later used here}}
}
+void lifetimebound_make_unique() {
+ std::unique_ptr<LifetimeBoundCtor> ptr;
+ {
+ MyObj obj;
+ ptr = std::make_unique<LifetimeBoundCtor>(obj); // tu-warning {{object whose reference is captured does not live long enough}}
+ } // tu-note {{destroyed here}}
+ (void)ptr; // tu-note {{later used here}}
+}
+
+void non_lifetimebound_make_unique() {
+ std::unique_ptr<LifetimeBoundCtor> ptr;
+ {
+ MyObj obj;
+ // No error as the ctor is not lifetimebound.
+ ptr = std::make_unique<LifetimeBoundCtor>(obj, 0);
+ }
+ (void)ptr;
+}
+
+void lifetimebound_make_unique_temp() {
+ std::unique_ptr<LifetimeBoundCtor> ptr = std::make_unique<LifetimeBoundCtor>(MyObj()); // tu-warning {{object whose reference is captured does not live long enough}} \
+ // tu-note {{destroyed here}}
+ (void)ptr; // tu-note {{later used here}}
+}
+
+// FIXME: make_unique annotation cannot be inferred for pointer param in constructor
+void lifetimebound_make_unique_raw_ptr() {
+ std::unique_ptr<LifetimeBoundCtor> ptr;
+ int x = 0;
+ {
+ int* p = &x;
+ ptr = std::make_unique<LifetimeBoundCtor>(p);
+ }
+ (void)ptr;
+}
+
+// FIXME: make_unique annotation cannot be inferred for view-type param in constructor
+void lifetimebound_make_unique_string_view_local() {
+ std::unique_ptr<LifetimeBoundCtor> ptr;
+ {
+ std::string s;
+ std::string_view sv(s);
+ ptr = std::make_unique<LifetimeBoundCtor>(sv);
+ }
+ (void)ptr;
+}
+
+struct MultiLifetimeBoundCtor {
+ MultiLifetimeBoundCtor(const MyObj& obj1 [[clang::lifetimebound]], const MyObj& obj2);
+ MultiLifetimeBoundCtor(const MyObj& obj1, const MyObj& obj2 [[clang::lifetimebound]], int);
+ MultiLifetimeBoundCtor(const MyObj& obj1 [[clang::lifetimebound]], const MyObj& obj2 [[clang::lifetimebound]], double);
+};
+
+void lifetimebound_make_unique_multi_params() {
+ std::unique_ptr<MultiLifetimeBoundCtor> ptr;
+ MyObj obj_long;
+ {
+ MyObj obj_short;
+ ptr = std::make_unique<MultiLifetimeBoundCtor>(obj_short, obj_long); // tu-warning {{object whose reference is captured does not live long enough}}
+ } // tu-note {{destroyed here}}
+ (void)ptr; // tu-note {{later used here}}
+}
+
+void lifetimebound_make_unique_multi_params2() {
+ std::unique_ptr<MultiLifetimeBoundCtor> ptr;
+ MyObj obj_long;
+ {
+ MyObj obj_short;
+ ptr = std::make_unique<MultiLifetimeBoundCtor>(obj_long, obj_short, 1); // tu-warning {{object whose reference is captured does not live long enough}}
+ } // tu-note {{destroyed here}}
+ (void)ptr; // tu-note {{later used here}}
+}
+
+void lifetimebound_make_unique_multi_params2_no_error_case() {
+ std::unique_ptr<MultiLifetimeBoundCtor> ptr;
+ MyObj obj_long;
+ {
+ MyObj obj_short;
+ ptr = std::make_unique<MultiLifetimeBoundCtor>(obj_short, obj_long, 1);
+ }
+ (void)ptr;
+}
+
+void lifetimebound_make_unique_multi_params3_1() {
+ std::unique_ptr<MultiLifetimeBoundCtor> ptr;
+ MyObj obj_long;
+ {
+ MyObj obj_short;
+ ptr = std::make_unique<MultiLifetimeBoundCtor>(obj_short, obj_long, 1.0); // tu-warning {{object whose reference is captured does not live long enough}}
+ } // tu-note {{destroyed here}}
+ (void)ptr; // tu-note {{later used here}}
+}
+
+void lifetimebound_make_unique_multi_params3_2() {
+ std::unique_ptr<MultiLifetimeBoundCtor> ptr;
+ MyObj obj_long;
+ {
+ MyObj obj_short;
+ ptr = std::make_unique<MultiLifetimeBoundCtor>(obj_long, obj_short, 1.0); // tu-warning {{object whose reference is captured does not live long enough}}
+ } // tu-note {{destroyed here}}
+ (void)ptr; // tu-note {{later used here}}
+}
+
View lifetimebound_return_of_local() {
MyObj stack;
return Identity(stack); // expected-warning {{address of stack memory is returned later}}
@@ -2446,15 +2552,13 @@ std::string_view return_dangling_view_through_owner() {
return sv; // expected-note {{returned here}}
}
-// FIXME: False negative. Move assignment of unique_ptr is not defaulted,
-// so origins from `local` don't propagate to `ups`.
void owner_outlives_lifetimebound_source() {
std::unique_ptr<S> ups;
{
std::string local;
- ups = getUniqueS(local);
- }
- (void)ups; // Should warn.
+ ups = getUniqueS(local); // expected-warning {{object whose reference is captured does not live long enough}}
+ } // expected-note {{destroyed here}}
+ (void)ups; // expected-note {{later used here}}
}
} // namespace track_origins_for_lifetimebound_record_type
More information about the cfe-commits
mailing list