[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