[clang] [LifetimeSafety] Add placement new support (PR #194030)

via cfe-commits cfe-commits at lists.llvm.org
Sat Apr 25 11:03:36 PDT 2026


https://github.com/NeKon69 updated https://github.com/llvm/llvm-project/pull/194030

>From 7b145c85107f012ea687a43022301a99ee55315c Mon Sep 17 00:00:00 2001
From: NeKon69 <nobodqwe at gmail.com>
Date: Fri, 24 Apr 2026 22:16:07 +0300
Subject: [PATCH 1/5] [LifetimeSafety] add placement new support

---
 .../LifetimeSafety/FactsGenerator.cpp         |  48 +++++--
 clang/test/Sema/warn-lifetime-safety.cpp      | 119 ++++++++++++++++--
 2 files changed, 145 insertions(+), 22 deletions(-)

diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index efdb1a1691ae3..ec40ad4803c80 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -332,6 +332,12 @@ void FactsGenerator::VisitCastExpr(const CastExpr *CE) {
   case CK_BuiltinFnToFnPtr:
     // Ignore function-to-pointer decays.
     return;
+  case CK_BitCast:
+    // OriginLists for Src and Dst may differ here. For example when casting
+    // from int** to void*
+    if (Src && Dest && Dest->getLength() == Src->getLength())
+      flow(Dest, Src, /*Kill=*/true);
+    return;
   default:
     return;
   }
@@ -625,20 +631,40 @@ void FactsGenerator::VisitArraySubscriptExpr(const ArraySubscriptExpr *ASE) {
 
 void FactsGenerator::VisitCXXNewExpr(const CXXNewExpr *NE) {
   OriginList *NewList = getOriginsList(*NE);
+  auto FlowTo = [&](OriginList *List) {
+    if (!List || !NE->getInitializer())
+      return;
 
-  const Loan *L = createLoan(FactMgr, NE);
-  CurrentBlockFacts.push_back(
-      FactMgr.createFact<IssueFact>(L->getID(), NewList->getOuterOriginID()));
-
-  NewList = NewList->peelOuterOrigin();
+    // FIXME: OriginList is null for `new[]` initializers. Remove this
+    // `Init` check once array origins are supported.
+    if (OriginList *Init = getOriginsList(*NE->getInitializer()); Init) {
+      flow(List, Init, true);
+    }
+  };
 
-  if (!NewList || !NE->getInitializer())
-    return;
+  // Check if we have a placement new where the second argument is void*, to
+  // avoid flowing from non-pointer parameters, such as std::nothrow.
+  // And that the placement parameter num is 1,
+  // that is to mostly limit to standard library placement new.
+  if (NE->getNumPlacementArgs() == 1) {
+    if (const auto *Arg = NE->getOperatorNew()
+                              ->getParamDecl(1)
+                              ->getType()
+                              ->getAs<PointerType>();
+        Arg && Arg->isVoidPointerType()) {
+      OriginList *PlacementList = getOriginsList(*NE->getPlacementArg(0));
+      CurrentBlockFacts.push_back(FactMgr.createFact<OriginFlowFact>(
+          NewList->getOuterOriginID(), PlacementList->getOuterOriginID(),
+          true));
+    }
+  } else {
+    const Loan *L = createLoan(FactMgr, NE);
+    CurrentBlockFacts.push_back(
+        FactMgr.createFact<IssueFact>(L->getID(), NewList->getOuterOriginID()));
+  }
 
-  // FIXME: OriginList is null for `new[]` initializers. Remove this `Init`
-  // check once array origins are supported.
-  if (OriginList *Init = getOriginsList(*NE->getInitializer()); Init)
-    flow(NewList, Init, true);
+  NewList = NewList->peelOuterOrigin();
+  FlowTo(NewList);
 }
 
 void FactsGenerator::VisitCXXDeleteExpr(const CXXDeleteExpr *DE) {
diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp
index fc2aef6024a42..fcac827949cd0 100644
--- a/clang/test/Sema/warn-lifetime-safety.cpp
+++ b/clang/test/Sema/warn-lifetime-safety.cpp
@@ -2859,17 +2859,6 @@ struct PointerFieldHolder {
   MyObj *Ptr;
 };
 
-// FIXME: https://github.com/llvm/llvm-project/issues/184344
-void placement_new_pointer_field_use_after_scope() {
-  PointerFieldHolder h;
-  PointerFieldHolder *p = &h;
-  {
-    MyObj obj;
-    p = new (&h) PointerFieldHolder{&obj};
-  }
-  (void)p->Ptr->id;
-}
-
 // FIXME: https://github.com/llvm/llvm-project/issues/184344
 void delete_through_pointer_field() {
   PointerFieldHolder h{new MyObj};
@@ -2898,6 +2887,114 @@ void allocate_void_ptr() {
 
 } // namespace new_allocation
 
+namespace placement_new {
+
+void placement_new_int_basic() {
+  int *p;
+  {
+    int storage;
+    p = new (&storage) int; // expected-warning {{object whose reference is captured does not live long enough}}
+  }                         // expected-note {{destroyed here}}
+  (void)*p;                 // expected-note {{later used here}}
+}
+
+void placement_new_view_from_dead_scope() {
+  View storage;
+  View *p = &storage;
+  {
+    MyObj obj;
+    p = new (&storage) View(obj); // expected-warning {{object whose reference is captured does not live long enough}}
+  }                               // expected-note {{destroyed here}}
+  p->use();                       // expected-note {{later used here}}
+}
+
+void placement_new_pointer_from_dead_object() {
+  MyObj *slot = nullptr;
+  MyObj **p = &slot;
+  {
+    MyObj obj;
+    p = new (&slot) MyObj *(&obj); // expected-warning {{object whose reference is captured does not live long enough}}
+  }                                // expected-note {{destroyed here}}
+  (void)**p;                       // expected-note {{later used here}}
+}
+
+void placement_new_array_basic() {
+  int *p;
+  {
+    int storage[2];
+    p = new (&storage) int[2]; // expected-warning {{object whose reference is captured does not live long enough}}
+  }                            // expected-note {{destroyed here}}
+  (void)p[0];                  // expected-note {{later used here}}
+}
+
+void placement_new_array_braces() {
+  int *p;
+  {
+    int storage[2];
+    p = new (&storage) int[2]{}; // expected-warning {{object whose reference is captured does not live long enough}}
+  }                              // expected-note {{destroyed here}}
+  (void)p[0];                    // expected-note {{later used here}}
+}
+
+struct PlacementNewInMethod {
+  View V; // expected-note {{this field dangles}}
+
+  void bad_store_after_placement_new() {
+    {
+      MyObj obj;
+      new (this) PlacementNewInMethod;
+      V = obj; // expected-warning {{address of stack memory escapes to a field}}
+    }
+    V.use();
+  }
+};
+
+// FIXME: Currently does not diagnose. We do not overwrite `storage`'s origins on placement new because we lose them on bitcast from `View*` to `void*`.
+void placement_new_member_call_from_dead_scope() {
+  View *storage = new View;
+  {
+    MyObj obj;
+    new (storage) View(obj);
+  }
+  storage->use();
+}
+
+void placement_new_heap_then_delete_use_after_free() {
+  int *storage = new int(7); // expected-warning {{allocated object does not live long enough}}
+  int *p = new (storage) int(42);
+  delete storage;            // expected-note {{freed here}}
+  (void)*p;                  // expected-note {{later used here}}
+}
+
+int* foo(int* x [[clang::lifetimebound]], int* y [[clang::lifetimebound]]);
+
+void placement_new_delete_result_of_lifetimebound_call() {
+  int *x = new int(1); // expected-warning {{allocated object does not live long enough}}
+  int *y = new int(2); // expected-warning {{allocated object does not live long enough}}
+  int *slot = nullptr;
+  int **p = new (&slot) int *(foo(x, y));
+  delete foo(x, y);    // expected-note 2 {{freed here}}
+  (void)**p;           // expected-note 2 {{later used here}}
+}
+
+
+struct PointerFieldHolder {
+  MyObj *Ptr;
+};
+
+// FIXME: https://github.com/llvm/llvm-project/issues/184344
+void placement_new_pointer_field_use_after_scope() {
+  PointerFieldHolder h;
+  PointerFieldHolder *p = &h;
+  {
+    MyObj obj;
+    p = new (&h) PointerFieldHolder{&obj};
+  }
+  (void)p->Ptr->id;
+}
+
+} // namespace placement_new
+
 namespace method_call_uses_field_origins {
 int GLOBAL_INT;
 std::string GLOBAL_STRING{"123"};

>From fc2f2f99cf27ed69ba48d4cf3cc005fd43edf776 Mon Sep 17 00:00:00 2001
From: NeKon69 <nobodqwe at gmail.com>
Date: Sat, 25 Apr 2026 13:21:55 +0300
Subject: [PATCH 2/5] update tests and impl

---
 .../LifetimeSafety/FactsGenerator.cpp         | 23 ++++++++++---------
 clang/test/Sema/warn-lifetime-safety.cpp      |  8 +++----
 2 files changed, 16 insertions(+), 15 deletions(-)

diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index ec40ad4803c80..f1425b45b7a83 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -631,16 +631,6 @@ void FactsGenerator::VisitArraySubscriptExpr(const ArraySubscriptExpr *ASE) {
 
 void FactsGenerator::VisitCXXNewExpr(const CXXNewExpr *NE) {
   OriginList *NewList = getOriginsList(*NE);
-  auto FlowTo = [&](OriginList *List) {
-    if (!List || !NE->getInitializer())
-      return;
-
-    // FIXME: OriginList is null for `new[]` initializers. Remove this
-    // `Init` check once array origins are supported.
-    if (OriginList *Init = getOriginsList(*NE->getInitializer()); Init) {
-      flow(List, Init, true);
-    }
-  };
 
   // Check if we have a placement new where the second argument is void*, to
   // avoid flowing from non-pointer parameters, such as std::nothrow.
@@ -652,6 +642,10 @@ void FactsGenerator::VisitCXXNewExpr(const CXXNewExpr *NE) {
                               ->getType()
                               ->getAs<PointerType>();
         Arg && Arg->isVoidPointerType()) {
+      // FIXME: Flow from constructor expr to placement arg. To also support
+      // side effect placement new has. e.g.
+      // new(&p) MyObj(...);
+      // Should flow from constrcutor to &p.
       OriginList *PlacementList = getOriginsList(*NE->getPlacementArg(0));
       CurrentBlockFacts.push_back(FactMgr.createFact<OriginFlowFact>(
           NewList->getOuterOriginID(), PlacementList->getOuterOriginID(),
@@ -664,7 +658,14 @@ void FactsGenerator::VisitCXXNewExpr(const CXXNewExpr *NE) {
   }
 
   NewList = NewList->peelOuterOrigin();
-  FlowTo(NewList);
+
+  if (!NewList || !NE->getInitializer())
+    return;
+
+  // FIXME: OriginList is null for `new[]` initializers. Remove this `Init`
+  // check once array origins are supported.
+  if (OriginList *Init = getOriginsList(*NE->getInitializer()); Init)
+    flow(NewList, Init, true);
 }
 
 void FactsGenerator::VisitCXXDeleteExpr(const CXXDeleteExpr *DE) {
diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp
index fcac827949cd0..ef3c54072a822 100644
--- a/clang/test/Sema/warn-lifetime-safety.cpp
+++ b/clang/test/Sema/warn-lifetime-safety.cpp
@@ -2936,20 +2936,20 @@ void placement_new_array_braces() {
   (void)p[0];                    // expected-note {{later used here}}
 }
 
+// FIXME: Currently does not diagnose. We do not overwrite `V`'s origins on placement new because we lose them on bitcast from `View*` to `void*`.
 struct PlacementNewInMethod {
-  View V; // expected-note {{this field dangles}}
+  View V;
 
   void bad_store_after_placement_new() {
     {
       MyObj obj;
-      new (this) PlacementNewInMethod;
-      V = obj; // expected-warning {{address of stack memory escapes to a field}}
+      new (&V) View(obj);
     }
     V.use();
   }
 };
 
-// FIXME: Currently does not diagnose. We do not overwrite `storage`'s origins on placement new because we lose them on bitcast from `View*` to `void*`.
+// FIXME: same false-negative as above
 void placement_new_member_call_from_dead_scope() {
   View *storage = new View;
   {

>From ea20cb21ac9d45364efec34308b908d1b595828d Mon Sep 17 00:00:00 2001
From: NeKon69 <nobodqwe at gmail.com>
Date: Sat, 25 Apr 2026 13:23:25 +0300
Subject: [PATCH 3/5] update comment

---
 clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index f1425b45b7a83..625e7f1aa1be2 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -642,10 +642,10 @@ void FactsGenerator::VisitCXXNewExpr(const CXXNewExpr *NE) {
                               ->getType()
                               ->getAs<PointerType>();
         Arg && Arg->isVoidPointerType()) {
-      // FIXME: Flow from constructor expr to placement arg. To also support
+      // FIXME: Flow from constructor expr to placement arg to also support
       // side effect placement new has. e.g.
       // new(&p) MyObj(...);
-      // Should flow from constrcutor to &p.
+      // Should flow from MyObj(...) to &p.
       OriginList *PlacementList = getOriginsList(*NE->getPlacementArg(0));
       CurrentBlockFacts.push_back(FactMgr.createFact<OriginFlowFact>(
           NewList->getOuterOriginID(), PlacementList->getOuterOriginID(),

>From 459da1dd261960d0dd5975f0c62ee716a61425d0 Mon Sep 17 00:00:00 2001
From: NeKon69 <nobodqwe at gmail.com>
Date: Sat, 25 Apr 2026 14:30:55 +0300
Subject: [PATCH 4/5] complete placement new

---
 clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp | 11 ++++++-----
 clang/test/Sema/warn-lifetime-safety.cpp             | 12 +++++++-----
 2 files changed, 13 insertions(+), 10 deletions(-)

diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index 625e7f1aa1be2..b44b7655e38f3 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -642,11 +642,12 @@ void FactsGenerator::VisitCXXNewExpr(const CXXNewExpr *NE) {
                               ->getType()
                               ->getAs<PointerType>();
         Arg && Arg->isVoidPointerType()) {
-      // FIXME: Flow from constructor expr to placement arg to also support
-      // side effect placement new has. e.g.
-      // new(&p) MyObj(...);
-      // Should flow from MyObj(...) to &p.
-      OriginList *PlacementList = getOriginsList(*NE->getPlacementArg(0));
+      const Expr *PlacementArg = NE->getPlacementArg(0)->IgnoreImpCasts();
+      OriginList *PlacementList =
+          getRValueOrigins(PlacementArg, getOriginsList(*PlacementArg));
+      if (PlacementList->peelOuterOrigin())
+        flow(PlacementList->peelOuterOrigin(),
+             getOriginsList(*NE->getInitializer()), true);
       CurrentBlockFacts.push_back(FactMgr.createFact<OriginFlowFact>(
           NewList->getOuterOriginID(), PlacementList->getOuterOriginID(),
           true));
diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp
index ef3c54072a822..a507d7c058358 100644
--- a/clang/test/Sema/warn-lifetime-safety.cpp
+++ b/clang/test/Sema/warn-lifetime-safety.cpp
@@ -2936,7 +2936,10 @@ void placement_new_array_braces() {
   (void)p[0];                    // expected-note {{later used here}}
 }
 
-// FIXME: Currently does not diagnose. We do not overwrite `V`'s origins on placement new because we lose them on bitcast from `View*` to `void*`.
+// FIXME: Currently `&expr` creates a brand new origin instead of reusing origins
+// from the original expression. Because of that, writes through `&expr` cannot
+// overwrite the original expression's inner storage origins. 
+// Related to https://github.com/llvm/llvm-project/issues/176291
 struct PlacementNewInMethod {
   View V;
 
@@ -2949,14 +2952,13 @@ struct PlacementNewInMethod {
   }
 };
 
-// FIXME: same false-negative as above
 void placement_new_member_call_from_dead_scope() {
   View *storage = new View;
   {
     MyObj obj;
-    new (storage) View(obj);
-  }
-  storage->use();
+    new (storage) View(obj); // expected-warning {{object whose reference is captured does not live long enough}}
+  }                          // expected-note {{destroyed here}}
+  storage->use();            // expected-note {{later used here}}
 }
 
 void placement_new_heap_then_delete_use_after_free() {

>From cf663a13bc279e19cf117698162f48dcbffdee87 Mon Sep 17 00:00:00 2001
From: NeKon69 <nobodqwe at gmail.com>
Date: Sat, 25 Apr 2026 21:03:21 +0300
Subject: [PATCH 5/5] replace checks

---
 clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index b44b7655e38f3..15e0f562e1ba8 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -631,6 +631,7 @@ void FactsGenerator::VisitArraySubscriptExpr(const ArraySubscriptExpr *ASE) {
 
 void FactsGenerator::VisitCXXNewExpr(const CXXNewExpr *NE) {
   OriginList *NewList = getOriginsList(*NE);
+  const Expr *Init = NE->getInitializer();
 
   // Check if we have a placement new where the second argument is void*, to
   // avoid flowing from non-pointer parameters, such as std::nothrow.
@@ -645,9 +646,8 @@ void FactsGenerator::VisitCXXNewExpr(const CXXNewExpr *NE) {
       const Expr *PlacementArg = NE->getPlacementArg(0)->IgnoreImpCasts();
       OriginList *PlacementList =
           getRValueOrigins(PlacementArg, getOriginsList(*PlacementArg));
-      if (PlacementList->peelOuterOrigin())
-        flow(PlacementList->peelOuterOrigin(),
-             getOriginsList(*NE->getInitializer()), true);
+      if (Init)
+        flow(PlacementList->peelOuterOrigin(), getOriginsList(*Init), true);
       CurrentBlockFacts.push_back(FactMgr.createFact<OriginFlowFact>(
           NewList->getOuterOriginID(), PlacementList->getOuterOriginID(),
           true));
@@ -660,7 +660,7 @@ void FactsGenerator::VisitCXXNewExpr(const CXXNewExpr *NE) {
 
   NewList = NewList->peelOuterOrigin();
 
-  if (!NewList || !NE->getInitializer())
+  if (!NewList || !Init)
     return;
 
   // FIXME: OriginList is null for `new[]` initializers. Remove this `Init`



More information about the cfe-commits mailing list