Skip to content

Conversation

alejandro-alvarez-sonarsource
Copy link
Contributor

This pull improves the handling of placement new inPointerArith, fixing one family of false positives, and one of negatives:

False Positives

  Buffer buffer;
  int* array = new (&buffer) int[10];
  ++array; // there should be no warning

The code above should flag the memory region buffer as reinterpreted, very much as reinterpret_cast would do. Note that in this particular case the placement new is inlined so the engine can track that *array points to the same region as buffer.

This is no-op if the placement new is opaque.

False Negatives

  Buffer buffer;
  int* array = new (&buffer) int;
  ++array; // there should be a warning

In this case, there is an implicit cast to void* when calling placement new. The memory region was marked as reinterpreted, and therefore later pointer arithmetic will not raise. I have added a condition to not consider a cast to void* as a reinterpretation, as an array of voids does not make much sense.

There are still some limitations, of course. For starters, if a single int is created in place of an array of unsigned char of exactly the same size, it will still be considered as an array. A convoluted example to make the point that I think it makes sense not to raise in this situation is in the test checkPlacementNewSlices.

CPP-6868

@llvmbot llvmbot added clang Clang issues not falling into any other category clang:static analyzer labels Aug 28, 2025
@llvmbot
Copy link
Member

llvmbot commented Aug 28, 2025

@llvm/pr-subscribers-clang

Author: Alejandro Álvarez Ayllón (alejandro-alvarez-sonarsource)

Changes

This pull improves the handling of placement new inPointerArith, fixing one family of false positives, and one of negatives:

False Positives

  Buffer buffer;
  int* array = new (&buffer) int[10];
  ++array; // there should be no warning

The code above should flag the memory region buffer as reinterpreted, very much as reinterpret_cast would do. Note that in this particular case the placement new is inlined so the engine can track that *array points to the same region as buffer.

This is no-op if the placement new is opaque.

False Negatives

  Buffer buffer;
  int* array = new (&buffer) int;
  ++array; // there should be a warning

In this case, there is an implicit cast to void* when calling placement new. The memory region was marked as reinterpreted, and therefore later pointer arithmetic will not raise. I have added a condition to not consider a cast to void* as a reinterpretation, as an array of voids does not make much sense.

There are still some limitations, of course. For starters, if a single int is created in place of an array of unsigned char of exactly the same size, it will still be considered as an array. A convoluted example to make the point that I think it makes sense not to raise in this situation is in the test checkPlacementNewSlices.

CPP-6868


Full diff: https://github.com/llvm/llvm-project/pull/155855.diff

3 Files Affected:

  • (modified) clang/lib/StaticAnalyzer/Checkers/PointerArithChecker.cpp (+31-1)
  • (modified) clang/test/Analysis/PR24184.cpp (+1-1)
  • (modified) clang/test/Analysis/ptr-arith.cpp (+121)
diff --git a/clang/lib/StaticAnalyzer/Checkers/PointerArithChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/PointerArithChecker.cpp
index 30e01e73eb18f..c4a968950e8bc 100644
--- a/clang/lib/StaticAnalyzer/Checkers/PointerArithChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/PointerArithChecker.cpp
@@ -74,6 +74,26 @@ class PointerArithChecker
 
 REGISTER_MAP_WITH_PROGRAMSTATE(RegionState, const MemRegion *, AllocKind)
 
+namespace {
+
+bool isArrayPlacementNew(const CXXNewExpr *NE) {
+  return NE->isArray() && NE->getNumPlacementArgs() > 0;
+}
+
+ProgramStateRef markSuperRegionReinterpreted(ProgramStateRef State,
+                                             const MemRegion *Region) {
+  while (const auto *BaseRegion = dyn_cast<CXXBaseObjectRegion>(Region)) {
+    Region = BaseRegion->getSuperRegion();
+  }
+  if (const auto *ElemRegion = dyn_cast<ElementRegion>(Region)) {
+    State = State->set<RegionState>(ElemRegion->getSuperRegion(),
+                                    AllocKind::Reinterpreted);
+  }
+  return State;
+}
+
+} // namespace
+
 void PointerArithChecker::checkDeadSymbols(SymbolReaper &SR,
                                            CheckerContext &C) const {
   // TODO: intentional leak. Some information is garbage collected too early,
@@ -244,13 +264,23 @@ void PointerArithChecker::checkPostStmt(const CXXNewExpr *NE,
   const MemRegion *Region = AllocedVal.getAsRegion();
   if (!Region)
     return;
+
+  // For array placement-new, mark the original region as reinterpreted
+  if (isArrayPlacementNew(NE)) {
+    State = markSuperRegionReinterpreted(State, Region);
+  }
+
   State = State->set<RegionState>(Region, Kind);
   C.addTransition(State);
 }
 
 void PointerArithChecker::checkPostStmt(const CastExpr *CE,
                                         CheckerContext &C) const {
-  if (CE->getCastKind() != CastKind::CK_BitCast)
+  // Casts to `void*` happen, for instance, on placement new calls.
+  // We consider `void*` not to erase the type information about the underlying
+  // region.
+  if (CE->getCastKind() != CastKind::CK_BitCast ||
+      CE->getType()->isVoidPointerType())
     return;
 
   const Expr *CastedExpr = CE->getSubExpr();
diff --git a/clang/test/Analysis/PR24184.cpp b/clang/test/Analysis/PR24184.cpp
index 172d3e7e33864..c73271b87de98 100644
--- a/clang/test/Analysis/PR24184.cpp
+++ b/clang/test/Analysis/PR24184.cpp
@@ -56,7 +56,7 @@ void fn1_1(void *p1, const void *p2) { p1 != p2; }
 void fn2_1(uint32_t *p1, unsigned char *p2, uint32_t p3) {
   unsigned i = 0;
   for (0; i < p3; i++)
-    fn1_1(p1 + i, p2 + i * 0);
+    fn1_1(p1 + i, p2 + i * 0); // expected-warning {{Pointer arithmetic on non-array variables relies on memory layout, which is dangerous}}
 }
 
 struct A_1 {
diff --git a/clang/test/Analysis/ptr-arith.cpp b/clang/test/Analysis/ptr-arith.cpp
index ec1c75c0c4063..39bb239f51ca7 100644
--- a/clang/test/Analysis/ptr-arith.cpp
+++ b/clang/test/Analysis/ptr-arith.cpp
@@ -165,3 +165,124 @@ void LValueToRValueBitCast_dumps(void *p, char (*array)[8]) {
 unsigned long ptr_arithmetic(void *p) {
   return __builtin_bit_cast(unsigned long, p) + 1; // no-crash
 }
+
+
+void escape(int*);
+
+struct AllocOpaqueFlag {};
+
+void* operator new(unsigned long, void *ptr) noexcept { return ptr; }
+void* operator new(unsigned long, void *ptr, AllocOpaqueFlag const&) noexcept { return ptr; }
+
+void* operator new[](unsigned long, void* ptr) noexcept { return ptr; }
+void* operator new[](unsigned long, void* ptr, AllocOpaqueFlag const&);
+
+struct Buffer {
+  char buf[100];
+  int padding;
+};
+
+void checkPlacementNewArryInObject() {
+  Buffer buffer;
+  int* array = new (&buffer) int[10];
+  escape(array);
+  ++array; // no warning
+  (void)*array;
+}
+
+void checkPlacementNewArrayInObjectOpaque() {
+  Buffer buffer;
+  int* array = new (&buffer, AllocOpaqueFlag{}) int[10];
+  escape(array);
+  ++array; // no warning
+  (void)*array;
+}
+
+void checkPlacementNewArrayInArray() {
+  char buffer[100];
+  int* array = new (buffer) int[10];
+  escape(array);
+  ++array; // no warning
+  (void)*array;
+}
+
+void checkPlacementNewArrayInArrayOpaque() {
+  char buffer[100];
+  int* array = new (buffer, AllocOpaqueFlag{}) int;
+  escape(array);
+  ++array; // no warning
+  (void)*array;
+}
+
+void checkPlacementNewObjectInObject() {
+  Buffer buffer;
+  int* array = new (&buffer) int;
+  escape(array);
+  ++array; // expected-warning{{Pointer arithmetic on non-array variables relies on memory layout, which is dangerous}}
+  (void)*array;
+}
+
+void checkPlacementNewObjectInObjectOpaque() {
+  Buffer buffer;
+  int* array = new (&buffer, AllocOpaqueFlag{}) int;
+  escape(array);
+  ++array;  // expected-warning{{Pointer arithmetic on non-array variables relies on memory layout, which is dangerous}}
+  (void)*array;
+}
+
+void checkPlacementNewObjectInArray() {
+  char buffer[sizeof(int)];
+  int* array = new (buffer) int;
+  escape(array);
+  ++array; // no warning (FN)
+  (void)*array;
+}
+
+void checkPlacementNewObjectInArrayOpaque() {
+  char buffer[sizeof(int)];
+  int* array = new (buffer, AllocOpaqueFlag{}) int;
+  escape(array);
+  ++array; // no warning (FN)
+  (void)*array;
+}
+
+void checkPlacementNewSlices() {
+  const int N = 10;
+  char buffer[sizeof(int) * N] = {0};
+  int *start = new (buffer) int{0};
+  for (int i = 1; i < N; i++) {
+    auto *ptr = new int(buffer[i * sizeof(int)]);
+    *ptr = i;
+  }
+  ++start; // no warning
+  (void*)start;
+}
+
+class BumpAlloc {
+  char *buffer;
+  char *offset;
+public:
+  BumpAlloc(int n): buffer(new char[n]), offset{buffer} {}
+  ~BumpAlloc() {delete [] buffer;}
+
+  void* alloc(unsigned long size) {
+    auto* ptr = offset;
+    offset += size;
+    return ptr;
+  }
+};
+
+void* operator new(unsigned long size, BumpAlloc &ba) {
+  return ba.alloc(size);
+}
+
+void checkPlacementSlab() {
+  BumpAlloc bump{10};
+
+  int *ptr = new (bump) int{0};
+  ++ptr; // no warning
+  (void)*ptr;
+
+  BumpAlloc *why = &bump;
+  ++why; // expected-warning {{Pointer arithmetic on non-array variables relies on memory layout, which is dangerous [alpha.core.PointerArithm]}}
+}

@llvmbot
Copy link
Member

llvmbot commented Aug 28, 2025

@llvm/pr-subscribers-clang-static-analyzer-1

Author: Alejandro Álvarez Ayllón (alejandro-alvarez-sonarsource)

Changes

This pull improves the handling of placement new inPointerArith, fixing one family of false positives, and one of negatives:

False Positives

  Buffer buffer;
  int* array = new (&amp;buffer) int[10];
  ++array; // there should be no warning

The code above should flag the memory region buffer as reinterpreted, very much as reinterpret_cast would do. Note that in this particular case the placement new is inlined so the engine can track that *array points to the same region as buffer.

This is no-op if the placement new is opaque.

False Negatives

  Buffer buffer;
  int* array = new (&amp;buffer) int;
  ++array; // there should be a warning

In this case, there is an implicit cast to void* when calling placement new. The memory region was marked as reinterpreted, and therefore later pointer arithmetic will not raise. I have added a condition to not consider a cast to void* as a reinterpretation, as an array of voids does not make much sense.

There are still some limitations, of course. For starters, if a single int is created in place of an array of unsigned char of exactly the same size, it will still be considered as an array. A convoluted example to make the point that I think it makes sense not to raise in this situation is in the test checkPlacementNewSlices.

CPP-6868


Full diff: https://github.com/llvm/llvm-project/pull/155855.diff

3 Files Affected:

  • (modified) clang/lib/StaticAnalyzer/Checkers/PointerArithChecker.cpp (+31-1)
  • (modified) clang/test/Analysis/PR24184.cpp (+1-1)
  • (modified) clang/test/Analysis/ptr-arith.cpp (+121)
diff --git a/clang/lib/StaticAnalyzer/Checkers/PointerArithChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/PointerArithChecker.cpp
index 30e01e73eb18f..c4a968950e8bc 100644
--- a/clang/lib/StaticAnalyzer/Checkers/PointerArithChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/PointerArithChecker.cpp
@@ -74,6 +74,26 @@ class PointerArithChecker
 
 REGISTER_MAP_WITH_PROGRAMSTATE(RegionState, const MemRegion *, AllocKind)
 
+namespace {
+
+bool isArrayPlacementNew(const CXXNewExpr *NE) {
+  return NE->isArray() && NE->getNumPlacementArgs() > 0;
+}
+
+ProgramStateRef markSuperRegionReinterpreted(ProgramStateRef State,
+                                             const MemRegion *Region) {
+  while (const auto *BaseRegion = dyn_cast<CXXBaseObjectRegion>(Region)) {
+    Region = BaseRegion->getSuperRegion();
+  }
+  if (const auto *ElemRegion = dyn_cast<ElementRegion>(Region)) {
+    State = State->set<RegionState>(ElemRegion->getSuperRegion(),
+                                    AllocKind::Reinterpreted);
+  }
+  return State;
+}
+
+} // namespace
+
 void PointerArithChecker::checkDeadSymbols(SymbolReaper &SR,
                                            CheckerContext &C) const {
   // TODO: intentional leak. Some information is garbage collected too early,
@@ -244,13 +264,23 @@ void PointerArithChecker::checkPostStmt(const CXXNewExpr *NE,
   const MemRegion *Region = AllocedVal.getAsRegion();
   if (!Region)
     return;
+
+  // For array placement-new, mark the original region as reinterpreted
+  if (isArrayPlacementNew(NE)) {
+    State = markSuperRegionReinterpreted(State, Region);
+  }
+
   State = State->set<RegionState>(Region, Kind);
   C.addTransition(State);
 }
 
 void PointerArithChecker::checkPostStmt(const CastExpr *CE,
                                         CheckerContext &C) const {
-  if (CE->getCastKind() != CastKind::CK_BitCast)
+  // Casts to `void*` happen, for instance, on placement new calls.
+  // We consider `void*` not to erase the type information about the underlying
+  // region.
+  if (CE->getCastKind() != CastKind::CK_BitCast ||
+      CE->getType()->isVoidPointerType())
     return;
 
   const Expr *CastedExpr = CE->getSubExpr();
diff --git a/clang/test/Analysis/PR24184.cpp b/clang/test/Analysis/PR24184.cpp
index 172d3e7e33864..c73271b87de98 100644
--- a/clang/test/Analysis/PR24184.cpp
+++ b/clang/test/Analysis/PR24184.cpp
@@ -56,7 +56,7 @@ void fn1_1(void *p1, const void *p2) { p1 != p2; }
 void fn2_1(uint32_t *p1, unsigned char *p2, uint32_t p3) {
   unsigned i = 0;
   for (0; i < p3; i++)
-    fn1_1(p1 + i, p2 + i * 0);
+    fn1_1(p1 + i, p2 + i * 0); // expected-warning {{Pointer arithmetic on non-array variables relies on memory layout, which is dangerous}}
 }
 
 struct A_1 {
diff --git a/clang/test/Analysis/ptr-arith.cpp b/clang/test/Analysis/ptr-arith.cpp
index ec1c75c0c4063..39bb239f51ca7 100644
--- a/clang/test/Analysis/ptr-arith.cpp
+++ b/clang/test/Analysis/ptr-arith.cpp
@@ -165,3 +165,124 @@ void LValueToRValueBitCast_dumps(void *p, char (*array)[8]) {
 unsigned long ptr_arithmetic(void *p) {
   return __builtin_bit_cast(unsigned long, p) + 1; // no-crash
 }
+
+
+void escape(int*);
+
+struct AllocOpaqueFlag {};
+
+void* operator new(unsigned long, void *ptr) noexcept { return ptr; }
+void* operator new(unsigned long, void *ptr, AllocOpaqueFlag const&) noexcept { return ptr; }
+
+void* operator new[](unsigned long, void* ptr) noexcept { return ptr; }
+void* operator new[](unsigned long, void* ptr, AllocOpaqueFlag const&);
+
+struct Buffer {
+  char buf[100];
+  int padding;
+};
+
+void checkPlacementNewArryInObject() {
+  Buffer buffer;
+  int* array = new (&buffer) int[10];
+  escape(array);
+  ++array; // no warning
+  (void)*array;
+}
+
+void checkPlacementNewArrayInObjectOpaque() {
+  Buffer buffer;
+  int* array = new (&buffer, AllocOpaqueFlag{}) int[10];
+  escape(array);
+  ++array; // no warning
+  (void)*array;
+}
+
+void checkPlacementNewArrayInArray() {
+  char buffer[100];
+  int* array = new (buffer) int[10];
+  escape(array);
+  ++array; // no warning
+  (void)*array;
+}
+
+void checkPlacementNewArrayInArrayOpaque() {
+  char buffer[100];
+  int* array = new (buffer, AllocOpaqueFlag{}) int;
+  escape(array);
+  ++array; // no warning
+  (void)*array;
+}
+
+void checkPlacementNewObjectInObject() {
+  Buffer buffer;
+  int* array = new (&buffer) int;
+  escape(array);
+  ++array; // expected-warning{{Pointer arithmetic on non-array variables relies on memory layout, which is dangerous}}
+  (void)*array;
+}
+
+void checkPlacementNewObjectInObjectOpaque() {
+  Buffer buffer;
+  int* array = new (&buffer, AllocOpaqueFlag{}) int;
+  escape(array);
+  ++array;  // expected-warning{{Pointer arithmetic on non-array variables relies on memory layout, which is dangerous}}
+  (void)*array;
+}
+
+void checkPlacementNewObjectInArray() {
+  char buffer[sizeof(int)];
+  int* array = new (buffer) int;
+  escape(array);
+  ++array; // no warning (FN)
+  (void)*array;
+}
+
+void checkPlacementNewObjectInArrayOpaque() {
+  char buffer[sizeof(int)];
+  int* array = new (buffer, AllocOpaqueFlag{}) int;
+  escape(array);
+  ++array; // no warning (FN)
+  (void)*array;
+}
+
+void checkPlacementNewSlices() {
+  const int N = 10;
+  char buffer[sizeof(int) * N] = {0};
+  int *start = new (buffer) int{0};
+  for (int i = 1; i < N; i++) {
+    auto *ptr = new int(buffer[i * sizeof(int)]);
+    *ptr = i;
+  }
+  ++start; // no warning
+  (void*)start;
+}
+
+class BumpAlloc {
+  char *buffer;
+  char *offset;
+public:
+  BumpAlloc(int n): buffer(new char[n]), offset{buffer} {}
+  ~BumpAlloc() {delete [] buffer;}
+
+  void* alloc(unsigned long size) {
+    auto* ptr = offset;
+    offset += size;
+    return ptr;
+  }
+};
+
+void* operator new(unsigned long size, BumpAlloc &ba) {
+  return ba.alloc(size);
+}
+
+void checkPlacementSlab() {
+  BumpAlloc bump{10};
+
+  int *ptr = new (bump) int{0};
+  ++ptr; // no warning
+  (void)*ptr;
+
+  BumpAlloc *why = &bump;
+  ++why; // expected-warning {{Pointer arithmetic on non-array variables relies on memory layout, which is dangerous [alpha.core.PointerArithm]}}
+}

Copy link
Contributor

@steakhal steakhal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks sweet. I did the first round, I'll let others to finish the rest while I'm on vacation


void checkPlacementNewArryInObject() {
Buffer buffer;
int* array = new (&buffer) int[10];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I guess, alignment requirements are not checked. Thats sad, but We could extend this checker later.

This is a common mistake I think.

@NagyDonat
Copy link
Contributor

NagyDonat commented Aug 29, 2025

This change itself seems to be correct, but I have doubts about the overall viability of the implementation of this alpha checker.

In particular I don't like that it maintains its independent model about things that are modeled more accurately by other checkers. For example it recognizes a few allocation functions instead of relying on DynamicMemoryModeling (aka MallocChecker.cpp) which provides more accurate modeling (e.g. recognizes more functions).

Based on this impression I was planning to discard the existing implementation and reimplement the functionality of this alpha checker in a more principled fashion. (I expect that I would be able to do this later this year.) However I'm happy to abandon this plan if you think that this checker is salvageable – and I would be especially grateful if you have plans for stabilizing it and bringing it out of alpha state.

@alejandro-alvarez-sonarsource @steakhal What are your high-level plans for this checker? Is this patch just an ad-hoc bugfix, or part of a more systemic cleanup?

@steakhal
Copy link
Contributor

steakhal commented Sep 2, 2025

This change itself seems to be correct, but I have doubts about the overall viability of the implementation of this alpha checker.

In particular I don't like that it maintains its independent model about things that are modeled more accurately by other checkers. For example it recognizes a few allocation functions instead of relying on DynamicMemoryModeling (aka MallocChecker.cpp) which provides more accurate modeling (e.g. recognizes more functions).

This is a great point. I don't have the context if this they could be merged in a meaningful way, but we should think about it.

Based on this impression I was planning to discard the existing implementation and reimplement the functionality of this alpha checker in a more principled fashion. (I expect that I would be able to do this later this year.) However I'm happy to abandon this plan if you think that this checker is salvageable – and I would be especially grateful if you have plans for stabilizing it and bringing it out of alpha state.

@alejandro-alvarez-sonarsource @steakhal What are your high-level plans for this checker? Is this patch just an ad-hoc bugfix, or part of a more systemic cleanup?

What you say makes sense. Ideally, alpha checkers should either graduate or get scrapped.
AFAIK, we didn't have plans for changing the structure of the code in any major ways. This is a one-off bug fix to cover some blind spots with this checker as a low hanging fruit.
I'd say, to unblock this progress, if the patch looks good, let's go with it and in the future we shall think about the future of this checker long term.

@steakhal steakhal requested a review from NagyDonat September 2, 2025 07:06
@alejandro-alvarez-sonarsource
Copy link
Contributor Author

Sorry for the delay in answering, I was waiting for @steakhal to be back to double check with him, but he already answered with what I had anyways in mind.
Indeed it is a one-off fix. I may revisit the checker soon-ish, but I have no long-term plans besides incremental changes until I see I have to.

@NagyDonat
Copy link
Contributor

I'd say, to unblock this progress, if the patch looks good, let's go with it and in the future we shall think about the future of this checker long term.

As this commit is already written, I don't have any reason against merging it. (I didn't do a through review yet, but I can happily do so if you want.)

My concerns about the long-term future of this alpha checker are only relevant for additional improvements: before investing additional work into ad-hoc improvements, we should discuss the long-term viability of this checker because it would be unfortunate to waste work in a dead end checker. (I'm not sure that this is a dead end; I just have suspicions about it.)

Copy link
Contributor

@necto necto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have approved this fix downstream

@necto necto merged commit 80d4e24 into llvm:main Sep 5, 2025
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:static analyzer clang Clang issues not falling into any other category
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants