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

via cfe-commits cfe-commits at lists.llvm.org
Wed Apr 29 06:09:02 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/9] [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/9] 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/9] 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/9] 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/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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 {



More information about the cfe-commits mailing list