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

via cfe-commits cfe-commits at lists.llvm.org
Wed Apr 29 07:57:18 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 01/10] [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 02/10] 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 03/10] 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 04/10] 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 05/10] 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`

>From cbd49e0278c9347ce580aa9208bcf0a0679083e2 Mon Sep 17 00:00:00 2001
From: NeKon69 <nobodqwe at gmail.com>
Date: Sat, 25 Apr 2026 21:23:29 +0300
Subject: [PATCH 06/10] add comments explaining flow

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

diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index 15e0f562e1ba8..212e4cd9a36f5 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -643,11 +643,18 @@ void FactsGenerator::VisitCXXNewExpr(const CXXNewExpr *NE) {
                               ->getType()
                               ->getAs<PointerType>();
         Arg && Arg->isVoidPointerType()) {
+      // Use the placement argument before the implicit conversion to void*, so
+      // inner origins are still available.
       const Expr *PlacementArg = NE->getPlacementArg(0)->IgnoreImpCasts();
+      // If the placement argument is a glvalue (such as DeclRefExpr), skip its
+      // outer storage origin so the list starts with the pointer origin.
       OriginList *PlacementList =
           getRValueOrigins(PlacementArg, getOriginsList(*PlacementArg));
+      // Placement new constructs the pointee of the placement pointer.
       if (Init)
         flow(PlacementList->peelOuterOrigin(), getOriginsList(*Init), true);
+      // The pointer returned by placement new comes from the placement
+      // argument.
       CurrentBlockFacts.push_back(FactMgr.createFact<OriginFlowFact>(
           NewList->getOuterOriginID(), PlacementList->getOuterOriginID(),
           true));

>From 0fdf10f8ed04c6c197a124a877d6effb35f9b16e Mon Sep 17 00:00:00 2001
From: NeKon69 <nobodqwe at gmail.com>
Date: Sat, 25 Apr 2026 22:43:09 +0300
Subject: [PATCH 07/10] rename variables

---
 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 212e4cd9a36f5..d598dde81a52b 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -672,8 +672,8 @@ void FactsGenerator::VisitCXXNewExpr(const CXXNewExpr *NE) {
 
   // 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);
+  if (OriginList *InitList = getOriginsList(*Init); InitList)
+    flow(NewList, InitList, true);
 }
 
 void FactsGenerator::VisitCXXDeleteExpr(const CXXDeleteExpr *DE) {

>From 7ef4df706f1267d979b102f8e1902d59e752c117 Mon Sep 17 00:00:00 2001
From: NeKon69 <nobodqwe at gmail.com>
Date: Tue, 28 Apr 2026 16:23:31 +0300
Subject: [PATCH 08/10] add a comment explaining current limitation and a few
 more failing tests

---
 .../LifetimeSafety/FactsGenerator.cpp         |  8 ++
 clang/test/Sema/warn-lifetime-safety.cpp      | 94 +++++++++++++++++++
 2 files changed, 102 insertions(+)

diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index d598dde81a52b..d55131381de39 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -643,6 +643,14 @@ void FactsGenerator::VisitCXXNewExpr(const CXXNewExpr *NE) {
                               ->getType()
                               ->getAs<PointerType>();
         Arg && Arg->isVoidPointerType()) {
+      // FIXME: This assumes the placement argument is a direct glvalue pointer
+      // expression with an origin list shaped like
+      // storage -> pointer value -> pointee object.
+      // The code below overwrites the pointee object origin. Since origin flow
+      // is one way, non-direct placement argument forms such as `storage.get()`
+      // or `&storage` need separate handling to find the actual storage object
+      // whose origins should be overwritten.
+
       // Use the placement argument before the implicit conversion to void*, so
       // inner origins are still available.
       const Expr *PlacementArg = NE->getPlacementArg(0)->IgnoreImpCasts();
diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp
index a507d7c058358..03a321cd215be 100644
--- a/clang/test/Sema/warn-lifetime-safety.cpp
+++ b/clang/test/Sema/warn-lifetime-safety.cpp
@@ -2961,6 +2961,100 @@ void placement_new_member_call_from_dead_scope() {
   storage->use();            // expected-note {{later used here}}
 }
 
+struct ViewPointerFieldHolder {
+  View *Ptr;
+};
+
+// FIXME: Repeated field accesses do not share stable field origins.
+void placement_new_pointer_field_from_dead_scope() {
+  ViewPointerFieldHolder h{new View};
+  {
+    MyObj obj;
+    new (h.Ptr) View(obj);
+  }
+  h.Ptr->use();
+}
+
+// FIXME: Repeated array element accesses do not share stable element origins.
+void placement_new_array_subscript_from_dead_scope() {
+  View *slots[1] = {new View};
+  {
+    MyObj obj;
+    new (slots[0]) View(obj);
+  }
+  slots[0]->use();
+}
+
+// FIXME: Writes through references do not update the referred pointer's origins.
+void placement_new_pointer_reference_from_dead_scope() {
+  View *storage = new View;
+  View *&ref = storage;
+  {
+    MyObj obj;
+    new (ref) View(obj);
+  }
+  storage->use();
+}
+
+// FIXME: Writing through a conditional glvalue is not propagated to either arm.
+void placement_new_conditional_pointer_from_dead_scope(bool flag) {
+  View *x = new View;
+  View *y = new View;
+  {
+    MyObj obj;
+    new (flag ? x : y) View(obj);
+  }
+  x->use();
+  y->use();
+}
+
+View *identity(View *p [[clang::lifetimebound]]);
+
+// FIXME: Function call results are not mapped back to the argument they expose.
+void placement_new_function_pointer_from_dead_scope() {
+  View *storage = new View;
+  {
+    MyObj obj;
+    new (identity(storage)) View(obj);
+  }
+  storage->use();
+}
+
+struct ViewStorage {
+  View Storage;
+  View *get() [[clang::lifetimebound]];
+};
+
+// FIXME: Placement-new does not write back through the lifetimebound result of `get()`.
+void placement_new_member_function_pointer_from_dead_scope() {
+  ViewStorage storage;
+  {
+    MyObj obj;
+    new (storage.get()) View(obj);
+  }
+  storage.Storage.use();
+}
+
+// FIXME: Address-of placement arguments are not resolved to the addressed object.
+void placement_new_addressof_from_dead_scope() {
+  View storage;
+  {
+    MyObj obj;
+    new (&storage) View(obj);
+  }
+  storage.use();
+}
+
+// FIXME: Explicit casts to `void *` are not ignored for placement arguments.
+void placement_new_explicit_void_cast_from_dead_scope() {
+  View *storage = new View;
+  {
+    MyObj obj;
+    new ((void *)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);

>From 2c20253e10b4fc9b0a4704a57fd70cd035b7e1d7 Mon Sep 17 00:00:00 2001
From: NeKon69 <nobodqwe at gmail.com>
Date: Wed, 29 Apr 2026 16:08:31 +0300
Subject: [PATCH 09/10] add another failing test and fix the code for arrays

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

diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index d55131381de39..f21405f5bb7ce 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -658,14 +658,17 @@ void FactsGenerator::VisitCXXNewExpr(const CXXNewExpr *NE) {
       // outer storage origin so the list starts with the pointer origin.
       OriginList *PlacementList =
           getRValueOrigins(PlacementArg, getOriginsList(*PlacementArg));
-      // Placement new constructs the pointee of the placement pointer.
-      if (Init)
+      // If the placement argument only has a glvalue origin, there is no
+      // pointee object origin to overwrite.
+      if (Init && PlacementList)
+        // Placement new constructs the pointee of the placement pointer.
         flow(PlacementList->peelOuterOrigin(), getOriginsList(*Init), true);
       // The pointer returned by placement new comes from the placement
       // argument.
-      CurrentBlockFacts.push_back(FactMgr.createFact<OriginFlowFact>(
-          NewList->getOuterOriginID(), PlacementList->getOuterOriginID(),
-          true));
+      if (PlacementList)
+        CurrentBlockFacts.push_back(FactMgr.createFact<OriginFlowFact>(
+            NewList->getOuterOriginID(), PlacementList->getOuterOriginID(),
+            true));
     }
   } else {
     const Loan *L = createLoan(FactMgr, NE);
diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp
index 03a321cd215be..b7276fb6e0ab5 100644
--- a/clang/test/Sema/warn-lifetime-safety.cpp
+++ b/clang/test/Sema/warn-lifetime-safety.cpp
@@ -3089,6 +3089,15 @@ void placement_new_pointer_field_use_after_scope() {
   (void)p->Ptr->id;
 }
 
+// FIXME: Stripping implicit casts also strips array to pointer decay, leaving only the array glvalue origin and no pointee origin to overwrite. Because of that, origins are not propagated here.
+void placement_new_direct_array_use_after_placement() {
+  char storage[sizeof(std::string)];
+  std::string* str1 = new (storage) std::string{"Old"};
+  auto p1 = str1->c_str();
+  new (storage) std::string{"New"};
+  (void)*p1;
+}
+
 } // namespace placement_new
 
 namespace method_call_uses_field_origins {

>From 3056fe5700cac84e39202acf824c55f68f990774 Mon Sep 17 00:00:00 2001
From: NeKon69 <nobodqwe at gmail.com>
Date: Wed, 29 Apr 2026 17:56:52 +0300
Subject: [PATCH 10/10] move some tests into a separate namespace, do not flow
 into placement argument

---
 .../LifetimeSafety/FactsGenerator.cpp         | 26 ++----
 clang/test/Sema/warn-lifetime-safety.cpp      | 92 +++++++++----------
 2 files changed, 49 insertions(+), 69 deletions(-)

diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index f21405f5bb7ce..40563cad04378 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -643,26 +643,16 @@ void FactsGenerator::VisitCXXNewExpr(const CXXNewExpr *NE) {
                               ->getType()
                               ->getAs<PointerType>();
         Arg && Arg->isVoidPointerType()) {
-      // FIXME: This assumes the placement argument is a direct glvalue pointer
-      // expression with an origin list shaped like
-      // storage -> pointer value -> pointee object.
-      // The code below overwrites the pointee object origin. Since origin flow
-      // is one way, non-direct placement argument forms such as `storage.get()`
-      // or `&storage` need separate handling to find the actual storage object
-      // whose origins should be overwritten.
-
       // Use the placement argument before the implicit conversion to void*, so
       // inner origins are still available.
-      const Expr *PlacementArg = NE->getPlacementArg(0)->IgnoreImpCasts();
-      // If the placement argument is a glvalue (such as DeclRefExpr), skip its
-      // outer storage origin so the list starts with the pointer origin.
-      OriginList *PlacementList =
-          getRValueOrigins(PlacementArg, getOriginsList(*PlacementArg));
-      // If the placement argument only has a glvalue origin, there is no
-      // pointee object origin to overwrite.
-      if (Init && PlacementList)
-        // Placement new constructs the pointee of the placement pointer.
-        flow(PlacementList->peelOuterOrigin(), getOriginsList(*Init), true);
+      const Expr *PlacementArg = NE->getPlacementArg(0);
+      if (const CastExpr *ICE = dyn_cast<CastExpr>(PlacementArg)) {
+        PlacementArg = ICE->getSubExpr();
+      }
+      OriginList *PlacementList = getOriginsList(*PlacementArg);
+      // FIXME: General placement arguments need separate handling to overwrite
+      // the right origins.
+
       // The pointer returned by placement new comes from the placement
       // argument.
       if (PlacementList)
diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp
index b7276fb6e0ab5..30b450c333fbd 100644
--- a/clang/test/Sema/warn-lifetime-safety.cpp
+++ b/clang/test/Sema/warn-lifetime-safety.cpp
@@ -2936,10 +2936,43 @@ void placement_new_array_braces() {
   (void)p[0];                    // expected-note {{later used here}}
 }
 
-// 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
+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
+
+// FIXME: Currently this is not diagnosed because placement new does not overwrite the placement argument's origins.
+namespace placement_new_argument {
 struct PlacementNewInMethod {
   View V;
 
@@ -2956,16 +2989,15 @@ void placement_new_member_call_from_dead_scope() {
   View *storage = new View;
   {
     MyObj obj;
-    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}}
+    new (storage) View(obj);
+  }
+  storage->use();
 }
 
 struct ViewPointerFieldHolder {
   View *Ptr;
 };
 
-// FIXME: Repeated field accesses do not share stable field origins.
 void placement_new_pointer_field_from_dead_scope() {
   ViewPointerFieldHolder h{new View};
   {
@@ -2975,7 +3007,6 @@ void placement_new_pointer_field_from_dead_scope() {
   h.Ptr->use();
 }
 
-// FIXME: Repeated array element accesses do not share stable element origins.
 void placement_new_array_subscript_from_dead_scope() {
   View *slots[1] = {new View};
   {
@@ -2985,7 +3016,6 @@ void placement_new_array_subscript_from_dead_scope() {
   slots[0]->use();
 }
 
-// FIXME: Writes through references do not update the referred pointer's origins.
 void placement_new_pointer_reference_from_dead_scope() {
   View *storage = new View;
   View *&ref = storage;
@@ -2996,7 +3026,6 @@ void placement_new_pointer_reference_from_dead_scope() {
   storage->use();
 }
 
-// FIXME: Writing through a conditional glvalue is not propagated to either arm.
 void placement_new_conditional_pointer_from_dead_scope(bool flag) {
   View *x = new View;
   View *y = new View;
@@ -3010,7 +3039,6 @@ void placement_new_conditional_pointer_from_dead_scope(bool flag) {
 
 View *identity(View *p [[clang::lifetimebound]]);
 
-// FIXME: Function call results are not mapped back to the argument they expose.
 void placement_new_function_pointer_from_dead_scope() {
   View *storage = new View;
   {
@@ -3025,7 +3053,6 @@ struct ViewStorage {
   View *get() [[clang::lifetimebound]];
 };
 
-// FIXME: Placement-new does not write back through the lifetimebound result of `get()`.
 void placement_new_member_function_pointer_from_dead_scope() {
   ViewStorage storage;
   {
@@ -3035,7 +3062,6 @@ void placement_new_member_function_pointer_from_dead_scope() {
   storage.Storage.use();
 }
 
-// FIXME: Address-of placement arguments are not resolved to the addressed object.
 void placement_new_addressof_from_dead_scope() {
   View storage;
   {
@@ -3045,7 +3071,6 @@ void placement_new_addressof_from_dead_scope() {
   storage.use();
 }
 
-// FIXME: Explicit casts to `void *` are not ignored for placement arguments.
 void placement_new_explicit_void_cast_from_dead_scope() {
   View *storage = new View;
   {
@@ -3055,41 +3080,6 @@ void placement_new_explicit_void_cast_from_dead_scope() {
   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;
-}
-
-// FIXME: Stripping implicit casts also strips array to pointer decay, leaving only the array glvalue origin and no pointee origin to overwrite. Because of that, origins are not propagated here.
 void placement_new_direct_array_use_after_placement() {
   char storage[sizeof(std::string)];
   std::string* str1 = new (storage) std::string{"Old"};
@@ -3098,7 +3088,7 @@ void placement_new_direct_array_use_after_placement() {
   (void)*p1;
 }
 
-} // namespace placement_new
+} // namespace placement_new_argument
 
 namespace method_call_uses_field_origins {
 int GLOBAL_INT;



More information about the cfe-commits mailing list