[clang] [clang] Fix NewDeleteLeaks false negative for heap fields (PR #221587)

Peiqi Li via cfe-commits cfe-commits at lists.llvm.org
Tue Sep 8 01:05:45 PDT 2026


https://github.com/voyager-jhk updated https://github.com/llvm/llvm-project/pull/221587

>From bdcca58411d202c007d2ef4fc66279c73995e4e8 Mon Sep 17 00:00:00 2001
From: voyager-jhk <voyager.lpq at gmail.com>
Date: Sun, 6 Sep 2026 23:52:43 +0800
Subject: [PATCH] [clang] Fix NewDeleteLeaks false negative for heap fields

Pointers stored in heap fields are incorrectly considered escaped,
causing leaks to go unreported.

Fixes #214226
---
 .../StaticAnalyzer/Checkers/MallocChecker.cpp | 73 ++++++++++++++++++-
 clang/test/Analysis/NewDeleteLeaks.cpp        | 51 +++++++++++++
 clang/test/Analysis/malloc.c                  | 15 +++-
 clang/test/Analysis/pr22954.c                 |  9 ++-
 4 files changed, 140 insertions(+), 8 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp
index 5ee5601a9319a..b083a44cb767a 100644
--- a/clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp
@@ -272,6 +272,9 @@ MallocUpdateRefState(CheckerContext &C, const Expr *E, ProgramStateRef State,
 
 REGISTER_SET_WITH_PROGRAMSTATE(ReallocSizeZeroSymbols, SymbolRef)
 
+/// Symbols to suppress for the current PSK_EscapeOnBind.
+REGISTER_SET_WITH_PROGRAMSTATE(SuppressEscapeOnBind, SymbolRef)
+
 namespace {
 
 /// The state of 'fromPtr' after reallocation is known to have failed.
@@ -388,8 +391,8 @@ struct DynMemFrontend : virtual public CheckerFrontend, public BT_PROVIDERS... {
 class MallocChecker
     : public CheckerFamily<
           check::DeadSymbols, check::PointerEscape, check::ConstPointerEscape,
-          check::PreStmt<ReturnStmt>, check::EndFunction, check::PreCall,
-          check::PostCall, eval::Call, check::NewAllocator,
+          check::Bind, check::PreStmt<ReturnStmt>, check::EndFunction,
+          check::PreCall, check::PostCall, eval::Call, check::NewAllocator,
           check::PostStmt<BlockExpr>, check::PostObjCMessage, check::Location,
           eval::Assume> {
 public:
@@ -451,6 +454,8 @@ class MallocChecker
                             bool Assumption) const;
   void checkLocation(SVal l, bool isLoad, const Stmt *S,
                      CheckerContext &C) const;
+  void checkBind(SVal Loc, SVal Val, const Stmt *S, bool AtDeclInit,
+                 CheckerContext &C) const;
 
   ProgramStateRef checkPointerEscape(ProgramStateRef State,
                                     const InvalidatedSymbols &Escaped,
@@ -1178,6 +1183,33 @@ class EscapeTrackedCallback final : public SymbolVisitor {
 
   friend class SymbolVisitor;
 };
+
+/// Collect allocated symbols reachable from a bound value.
+class CollectAllocatedOnBindCallback final : public SymbolVisitor {
+  ProgramStateRef State;
+
+public:
+  explicit CollectAllocatedOnBindCallback(ProgramStateRef S)
+      : State(std::move(S)) {}
+  ProgramStateRef getState() const { return State; }
+
+  bool VisitSymbol(SymbolRef Sym) override {
+    if (const RefState *RS = State->get<RegionState>(Sym))
+      if (RS->isAllocated() || RS->isAllocatedOfSizeZero())
+        State = State->add<SuppressEscapeOnBind>(Sym);
+    return true;
+  }
+};
+
+static bool isAllocatedOrZeroAllocated(const RefState *RS) {
+  return RS && (RS->isAllocated() || RS->isAllocatedOfSizeZero());
+}
+
+static ProgramStateRef consumeSuppressEscapeOnBind(ProgramStateRef State) {
+  if (State->get<SuppressEscapeOnBind>().isEmpty())
+    return State;
+  return State->remove<SuppressEscapeOnBind>();
+}
 } // end anonymous namespace
 
 static bool isStandardNew(const FunctionDecl *FD) {
@@ -3893,6 +3925,33 @@ bool MallocChecker::mayFreeAnyEscapedMemoryOrIsModeledExplicitly(
   return false;
 }
 
+void MallocChecker::checkBind(SVal Loc, SVal Val, const Stmt *S,
+                              bool AtDeclInit, CheckerContext &C) const {
+  // Preserve tracking for allocations stored in heap objects that are still
+  // owned by the analyzer. An escaped parent does not provide this guarantee.
+  const MemRegion *MR = Loc.getAsRegion();
+  if (!MR)
+    return;
+
+  ProgramStateRef State = C.getState();
+  if (!MR->hasMemorySpace<HeapSpaceRegion>(State))
+    return;
+
+  const SymbolicRegion *SymBase = MR->getSymbolicBase();
+  if (!SymBase)
+    return;
+
+  const RefState *ParentRS = State->get<RegionState>(SymBase->getSymbol());
+  if (!isAllocatedOrZeroAllocated(ParentRS))
+    return;
+
+  ProgramStateRef NewState =
+      State->scanReachableSymbols<CollectAllocatedOnBindCallback>(Val)
+          .getState();
+  if (NewState != State)
+    C.addTransition(NewState);
+}
+
 ProgramStateRef MallocChecker::checkPointerEscape(ProgramStateRef State,
                                              const InvalidatedSymbols &Escaped,
                                              const CallEvent *Call,
@@ -3919,6 +3978,9 @@ ProgramStateRef MallocChecker::checkPointerEscapeAux(
     ProgramStateRef State, const InvalidatedSymbols &Escaped,
     const CallEvent *Call, PointerEscapeKind Kind,
     bool IsConstPointerEscape) const {
+  // The suppression set only applies to bind escapes.
+  const bool SuppressOnBind = Kind == PSK_EscapeOnBind;
+
   // If we know that the call does not free memory, or we want to process the
   // call later, keep tracking the top level arguments.
   SymbolRef EscapingSymbol = nullptr;
@@ -3926,19 +3988,22 @@ ProgramStateRef MallocChecker::checkPointerEscapeAux(
       !mayFreeAnyEscapedMemoryOrIsModeledExplicitly(Call, State,
                                                     EscapingSymbol) &&
       !EscapingSymbol) {
-    return State;
+    return consumeSuppressEscapeOnBind(State);
   }
 
   for (SymbolRef sym : Escaped) {
     if (EscapingSymbol && EscapingSymbol != sym)
       continue;
 
+    if (SuppressOnBind && State->contains<SuppressEscapeOnBind>(sym))
+      continue;
+
     if (const RefState *RS = State->get<RegionState>(sym))
       if (RS->isAllocated() || RS->isAllocatedOfSizeZero())
         if (!IsConstPointerEscape || checkIfNewOrNewArrayFamily(RS))
           State = State->set<RegionState>(sym, RefState::getEscaped(RS));
   }
-  return State;
+  return consumeSuppressEscapeOnBind(State);
 }
 
 bool MallocChecker::isArgZERO_SIZE_PTR(ProgramStateRef State, CheckerContext &C,
diff --git a/clang/test/Analysis/NewDeleteLeaks.cpp b/clang/test/Analysis/NewDeleteLeaks.cpp
index d9c4b77d1e6fc..1300aba5293eb 100644
--- a/clang/test/Analysis/NewDeleteLeaks.cpp
+++ b/clang/test/Analysis/NewDeleteLeaks.cpp
@@ -251,3 +251,54 @@ void validate_system_header() {
 }
 
 } // namespace protobuf_leak
+
+// Regression test for GH#214226.
+namespace heap_field_bind {
+
+struct Owner {
+  int *member;
+  Owner() : member(new int(42)) {} // expected-note {{Memory is allocated}}
+};
+
+void member_leaked_when_owner_deleted() {
+  Owner *owner = new Owner; // expected-note {{Calling default constructor for 'Owner'}}
+                            // expected-note at -1 {{Returning from default constructor for 'Owner'}}
+  delete owner;
+} // expected-warning {{Potential leak of memory pointed to by field 'member'}}
+// expected-note at -1 {{Potential leak of memory pointed to by field 'member'}}
+
+Owner *member_not_leaked_when_owner_returned() {
+  Owner *owner = new Owner;
+  return owner;
+} // no-warning
+
+void member_released_before_owner() {
+  Owner *owner = new Owner;
+  delete owner->member;
+  delete owner;
+} // no-warning
+
+struct OwnerNoCtor {
+  int *member;
+};
+
+void leak_assigned_member_when_owner_deleted() {
+  OwnerNoCtor *owner = new OwnerNoCtor;
+  owner->member = new int; // expected-note {{Memory is allocated}}
+  delete owner;
+} // expected-warning {{Potential leak of memory pointed to by field 'member'}}
+// expected-note at -1 {{Potential leak of memory pointed to by field 'member'}}
+
+OwnerNoCtor *assigned_member_not_leaked_when_owner_returned() {
+  OwnerNoCtor *owner = new OwnerNoCtor;
+  owner->member = new int;
+  return owner;
+} // no-warning
+
+void escaped_parent_does_not_report_inner(OwnerNoCtor **out) {
+  OwnerNoCtor *owner = new OwnerNoCtor;
+  *out = owner;
+  owner->member = new int;
+} // no-warning
+
+} // namespace heap_field_bind
diff --git a/clang/test/Analysis/malloc.c b/clang/test/Analysis/malloc.c
index 6c3dadfd16021..18a36687a2d7b 100644
--- a/clang/test/Analysis/malloc.c
+++ b/clang/test/Analysis/malloc.c
@@ -1771,7 +1771,20 @@ void testMallocIntoMalloc(void) {
   StructWithPtr *s = malloc(sizeof(StructWithPtr));
   s->memP = malloc(sizeof(int));
   free(s);
-} // FIXME: should warn here
+} // expected-warning {{Potential leak of memory pointed to by field 'memP'}}
+
+void testMallocIntoMallocThenFreeMember(void) {
+  StructWithPtr *s = malloc(sizeof(StructWithPtr));
+  s->memP = malloc(sizeof(int));
+  free(s->memP);
+  free(s);
+} // no-warning
+
+void testMallocIntoEscapedParent(StructWithPtr **out) {
+  StructWithPtr *s = malloc(sizeof(StructWithPtr));
+  *out = s;
+  s->memP = malloc(sizeof(int));
+} // no-warning
 
 int conjure(void);
 void testExtent(void) {
diff --git a/clang/test/Analysis/pr22954.c b/clang/test/Analysis/pr22954.c
index b3910da6c70ab..9b5782b2fd79e 100644
--- a/clang/test/Analysis/pr22954.c
+++ b/clang/test/Analysis/pr22954.c
@@ -380,9 +380,11 @@ int f20(int i) {
   a20[1].s1[3] = 8;
   a20[1].s2 = strdup("world");
   a20[i].s2 = strdup("hola");
-  char input[] = {'a', 'b', 'c', 'd'};
+  char input[] = {'a', 'b', 'c', 'd'}; // expected-warning{{Potential leak of memory pointed to by field 's2'}}\
+  expected-warning{{Potential leak of memory pointed to by field 's2'}}
   memcpy(a20[0].s1, input, 4);
-  clang_analyzer_eval(a20[0].s1[0] == 1); // expected-warning{{UNKNOWN}}
+  clang_analyzer_eval(a20[0].s1[0] == 1); // expected-warning{{UNKNOWN}}\
+  expected-warning{{Potential leak of memory pointed to by field 's2'}}
   clang_analyzer_eval(a20[0].s1[1] == 1); // expected-warning{{UNKNOWN}}
   clang_analyzer_eval(a20[0].s1[2] == 1); // expected-warning{{UNKNOWN}}
   clang_analyzer_eval(a20[0].s1[3] == 1); // expected-warning{{UNKNOWN}}
@@ -422,7 +424,8 @@ int f21(int i) {
   a21[i].s1[3] = 4;
   char input[] = {'a', 'b', 'c', 'd'};
   memcpy(a21[i].s1, input, 4);
-  clang_analyzer_eval(a21[0].s1[0] == 1); // expected-warning{{UNKNOWN}}
+  clang_analyzer_eval(a21[0].s1[0] == 1); // expected-warning{{UNKNOWN}}\
+  expected-warning{{Potential leak of memory pointed to by field 's2'}}
   clang_analyzer_eval(a21[0].s1[1] == 1); // expected-warning{{UNKNOWN}}
   clang_analyzer_eval(a21[0].s1[2] == 1); // expected-warning{{UNKNOWN}}
   clang_analyzer_eval(a21[0].s1[3] == 1); // expected-warning{{UNKNOWN}}



More information about the cfe-commits mailing list