[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