[clang] [analyzer] Improve dangling value tracking in DanglingPtrDeref (PR #211818)

Benedek Kaibas via cfe-commits cfe-commits at lists.llvm.org
Fri Jul 24 07:59:30 PDT 2026


https://github.com/benedekaibas created https://github.com/llvm/llvm-project/pull/211818

Improve dangling value tracking in the `DanglingPtrDeref` checker by adding `trackExpressionValue`. The report with this change now tracks the dangling value and shows where the value originated from. Currently the checker only points at the destruction and use sites which does not explain the full picture for the user.

>From 28b8e2424a5e3c947765457f2de5f2bf937d100b Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas01 at allegheny.edu>
Date: Thu, 23 Jul 2026 15:01:21 +0200
Subject: [PATCH 1/3] [analyzer] Match dangling subobjects by their base region
 in DanglingPtrDeref

---
 .../Checkers/DanglingPtrDeref.cpp             | 14 +++--
 .../Checkers/LifetimeModeling.cpp             |  2 +-
 clang/test/Analysis/dangling-ptr-deref.cpp    | 53 +++++++++++++++++++
 3 files changed, 65 insertions(+), 4 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/DanglingPtrDeref.cpp b/clang/lib/StaticAnalyzer/Checkers/DanglingPtrDeref.cpp
index ff2087e1db933..41fcc68f8f109 100644
--- a/clang/lib/StaticAnalyzer/Checkers/DanglingPtrDeref.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/DanglingPtrDeref.cpp
@@ -66,13 +66,21 @@ void DanglingPtrDeref::checkPostCall(const CallEvent &Call,
   }
 }
 
+static std::string getRegionName(const MemRegion *Reg) {
+  // FIXME: Once the checker supports heap allocation, more region kinds
+  // should be handled to produce the correct descriptive name.
+  if (const std::string RegName = Reg->getDescriptiveName(); !RegName.empty())
+    return RegName;
+  return "the region";
+}
+
 void DanglingPtrDeref::reportUseAfterScope(const MemRegion *Region,
                                            ExplodedNode *N,
                                            CheckerContext &C) const {
   auto BR = std::make_unique<PathSensitiveBugReport>(
       BugMsg,
-      (llvm::Twine("Use of '") + Region->getString() +
-       "' after its lifetime ended."),
+      (llvm::Twine("Use of ") + getRegionName(Region) +
+       " after its lifetime ended."),
       N);
   BR->addVisitor<DanglingPtrDerefBRVisitor>(Region);
   C.emitReport(std::move(BR));
@@ -99,7 +107,7 @@ DanglingPtrDerefBRVisitor::VisitNode(const ExplodedNode *N,
       S, BRC.getSourceManager(), N->getStackFrame());
   return std::make_shared<PathDiagnosticEventPiece>(
       Pos,
-      (llvm::Twine("'") + SourceRegion->getString() + "' is destroyed here")
+      (llvm::Twine() + getRegionName(SourceRegion) + " is destroyed here")
           .str(),
       true);
 }
diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeModeling.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeModeling.cpp
index 2b6f5dae4243f..ef0b1cb264b18 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeModeling.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeModeling.cpp
@@ -73,7 +73,7 @@ std::vector<const MemRegion *> lifetime_modeling::getDanglingRegionsAfterReturn(
 
 bool lifetime_modeling::isDeallocated(ProgramStateRef State,
                                       const MemRegion *Region) {
-  return State->contains<DeallocatedSourceSet>(Region);
+  return State->contains<DeallocatedSourceSet>(Region->getBaseRegion());
 }
 
 static ProgramStateRef bindSource(ProgramStateRef State, SVal RetVal,
diff --git a/clang/test/Analysis/dangling-ptr-deref.cpp b/clang/test/Analysis/dangling-ptr-deref.cpp
index e661eff9ccbb6..9deda4b2502a4 100644
--- a/clang/test/Analysis/dangling-ptr-deref.cpp
+++ b/clang/test/Analysis/dangling-ptr-deref.cpp
@@ -112,3 +112,56 @@ void inlined_callee_single_report() {
   // expected-note at -1 {{Calling 'deref_param'}}
   (void)r;
 }
+
+struct MyBuffer {
+  char buffer[8];
+};
+
+void member_subregion_dangling_deref() {
+  const char *p = nullptr;
+  {
+    struct MyBuffer tmp_buffer = {};
+    p = tmp_buffer.buffer;
+  }
+  // expected-note at -1 {{'tmp_buffer.buffer[0]' is destroyed here}}
+  char c = *p;
+  // expected-warning at -1 {{Use of 'tmp_buffer.buffer[0]' after its lifetime ended}}
+  // expected-note at -2    {{Use of 'tmp_buffer.buffer[0]' after its lifetime ended}}
+  (void)c;
+}
+
+void opaque(const char *);
+
+void passing_dangling_to_call() {
+  const char *p = nullptr;
+  {
+    struct MyBuffer tmp_buffer = {};
+    p = tmp_buffer.buffer;
+  }
+  // expected-note at -1 {{'tmp_buffer.buffer[0]' is destroyed here}}
+  opaque(p);
+  // expected-warning at -1 {{Use of 'tmp_buffer.buffer[0]' after its lifetime ended}}
+  // expected-note at -2    {{Use of 'tmp_buffer.buffer[0]' after its lifetime ended}}
+}
+
+void member_subregion_alive_deref() {
+  {
+    struct MyBuffer tmp_buffer = {};
+    const char *p = tmp_buffer.buffer;
+    opaque(p); //   no-warning
+    char c = *p; // no-warning
+    (void)c;
+  }
+}
+
+void arr_elem_subreg_dangling_deref() {
+  int *ptr = nullptr;
+  {
+    int local_arr[5];
+    ptr = &local_arr[1];
+  }
+  // expected-note at -1 {{'local_arr[1]' is destroyed here}}
+  *ptr = 7;
+  // expected-warning at -1 {{Use of 'local_arr[1]' after its lifetime ended}}
+  // expected-note at -2    {{Use of 'local_arr[1]' after its lifetime ended}}
+}

>From 2cdca2d740aca62e6b947b6afffb22e1ba3b7f57 Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas01 at allegheny.edu>
Date: Thu, 23 Jul 2026 23:55:54 +0200
Subject: [PATCH 2/3] Resolve nits.

---
 .../Checkers/DanglingPtrDeref.cpp             |  3 +-
 clang/test/Analysis/dangling-ptr-deref.cpp    | 61 +++++++++++++++++--
 2 files changed, 56 insertions(+), 8 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/DanglingPtrDeref.cpp b/clang/lib/StaticAnalyzer/Checkers/DanglingPtrDeref.cpp
index 41fcc68f8f109..d1b9f0fea4044 100644
--- a/clang/lib/StaticAnalyzer/Checkers/DanglingPtrDeref.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/DanglingPtrDeref.cpp
@@ -107,8 +107,7 @@ DanglingPtrDerefBRVisitor::VisitNode(const ExplodedNode *N,
       S, BRC.getSourceManager(), N->getStackFrame());
   return std::make_shared<PathDiagnosticEventPiece>(
       Pos,
-      (llvm::Twine() + getRegionName(SourceRegion) + " is destroyed here")
-          .str(),
+      (getRegionName(SourceRegion) + llvm::Twine(" is destroyed here")).str(),
       true);
 }
 
diff --git a/clang/test/Analysis/dangling-ptr-deref.cpp b/clang/test/Analysis/dangling-ptr-deref.cpp
index 9deda4b2502a4..654b23d0fea64 100644
--- a/clang/test/Analysis/dangling-ptr-deref.cpp
+++ b/clang/test/Analysis/dangling-ptr-deref.cpp
@@ -116,18 +116,20 @@ void inlined_callee_single_report() {
 struct MyBuffer {
   char buffer[8];
 };
+struct MyStruct { int x; };
+struct Inner { int x; };
+struct Outer { struct Inner inner; };
 
-void member_subregion_dangling_deref() {
+char member_subregion_dangling_deref() {
   const char *p = nullptr;
   {
     struct MyBuffer tmp_buffer = {};
     p = tmp_buffer.buffer;
   }
   // expected-note at -1 {{'tmp_buffer.buffer[0]' is destroyed here}}
-  char c = *p;
+  return *p; 
   // expected-warning at -1 {{Use of 'tmp_buffer.buffer[0]' after its lifetime ended}}
   // expected-note at -2    {{Use of 'tmp_buffer.buffer[0]' after its lifetime ended}}
-  (void)c;
 }
 
 void opaque(const char *);
@@ -144,13 +146,12 @@ void passing_dangling_to_call() {
   // expected-note at -2    {{Use of 'tmp_buffer.buffer[0]' after its lifetime ended}}
 }
 
-void member_subregion_alive_deref() {
+char member_subregion_alive_deref() {
   {
     struct MyBuffer tmp_buffer = {};
     const char *p = tmp_buffer.buffer;
     opaque(p); //   no-warning
-    char c = *p; // no-warning
-    (void)c;
+    return *p; //   no-warning
   }
 }
 
@@ -165,3 +166,51 @@ void arr_elem_subreg_dangling_deref() {
   // expected-warning at -1 {{Use of 'local_arr[1]' after its lifetime ended}}
   // expected-note at -2    {{Use of 'local_arr[1]' after its lifetime ended}}
 }
+
+char member_array_elem__dangling_deref() {
+  const char *p = nullptr;
+  {
+    struct MyBuffer tmp_buffer = {};
+    p = tmp_buffer.buffer + 3;
+  }
+  // expected-note at -1 {{'tmp_buffer.buffer[3]' is destroyed here}}
+  return *p;
+  // expected-warning at -1 {{Use of 'tmp_buffer.buffer[3]' after its lifetime ended}}
+  // expected-note at -2    {{Use of 'tmp_buffer.buffer[3]' after its lifetime ended}}
+}
+
+int struct_field_dangling_deref() {
+  int *p = nullptr;
+  {
+    struct MyStruct s = {};
+    p = &s.x;
+  }
+  // expected-note at -1 {{'s.x' is destroyed here}}
+  return *p;
+  // expected-warning at -1 {{Use of 's.x' after its lifetime ended}}
+  // expected-note at -2    {{Use of 's.x' after its lifetime ended}}
+}
+
+int struct_array_element_dangling_deref() {
+  int *p = nullptr;
+  {
+    struct MyStruct arr[4] = {};
+    p = &arr[2].x;
+  }
+  // expected-note at -1 {{'arr[2].x' is destroyed here}}
+  return *p;
+  // expected-warning at -1 {{Use of 'arr[2].x' after its lifetime ended}}
+  // expected-note at -2    {{Use of 'arr[2].x' after its lifetime ended}}
+}
+
+int nested_field_dangling_deref() {
+  int *p = nullptr;
+  {
+    struct Outer o = {};
+    p = &o.inner.x;
+  }
+  // expected-note at -1 {{'o.inner.x' is destroyed here}}
+  return *p;
+  // expected-warning at -1 {{Use of 'o.inner.x' after its lifetime ended}}
+  // expected-note at -2    {{Use of 'o.inner.x' after its lifetime ended}}
+}

>From 5a751fe8482780d9949544ac962fdd4877f7fa1c Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas01 at allegheny.edu>
Date: Fri, 24 Jul 2026 16:53:31 +0200
Subject: [PATCH 3/3] [analyzer] Improve dangling value tracking in
 DanglingPtrDeref

---
 .../Checkers/DanglingPtrDeref.cpp              | 11 ++++++-----
 clang/test/Analysis/dangling-ptr-deref.cpp     | 18 +++++++++---------
 2 files changed, 15 insertions(+), 14 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/DanglingPtrDeref.cpp b/clang/lib/StaticAnalyzer/Checkers/DanglingPtrDeref.cpp
index d1b9f0fea4044..16550bb7007e1 100644
--- a/clang/lib/StaticAnalyzer/Checkers/DanglingPtrDeref.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/DanglingPtrDeref.cpp
@@ -15,8 +15,8 @@ class DanglingPtrDeref : public Checker<check::Location, check::PostCall> {
   void checkLocation(SVal Loc, bool IsLoad, const Stmt *S,
                      CheckerContext &C) const;
   void checkPostCall(const CallEvent &Call, CheckerContext &C) const;
-  void reportUseAfterScope(const MemRegion *Region, ExplodedNode *N,
-                           CheckerContext &C) const;
+  void reportUseAfterScope(const MemRegion *Region, const Stmt *S,
+                           ExplodedNode *N, CheckerContext &C) const;
   const BugType BugMsg{this, "ReportDanglingPtrDeref", "LifetimeBound"};
 };
 
@@ -45,7 +45,7 @@ void DanglingPtrDeref::checkLocation(SVal Loc, bool IsLoad, const Stmt *S,
   if (const MemRegion *LocRegion = Loc.getAsRegion()) {
     if (lifetime_modeling::isDeallocated(State, LocRegion)) {
       if (ExplodedNode *N = C.generateNonFatalErrorNode(State))
-        reportUseAfterScope(LocRegion, N, C);
+        reportUseAfterScope(LocRegion, S, N, C);
     }
   }
 }
@@ -62,7 +62,7 @@ void DanglingPtrDeref::checkPostCall(const CallEvent &Call,
     if (const MemRegion *ArgRegion = Call.getArgSVal(Idx).getAsRegion())
       if (lifetime_modeling::isDeallocated(State, ArgRegion))
         if (ExplodedNode *N = C.generateNonFatalErrorNode())
-          reportUseAfterScope(ArgRegion, N, C);
+          reportUseAfterScope(ArgRegion, Call.getArgExpr(Idx), N, C);
   }
 }
 
@@ -75,7 +75,7 @@ static std::string getRegionName(const MemRegion *Reg) {
 }
 
 void DanglingPtrDeref::reportUseAfterScope(const MemRegion *Region,
-                                           ExplodedNode *N,
+                                           const Stmt *S, ExplodedNode *N,
                                            CheckerContext &C) const {
   auto BR = std::make_unique<PathSensitiveBugReport>(
       BugMsg,
@@ -83,6 +83,7 @@ void DanglingPtrDeref::reportUseAfterScope(const MemRegion *Region,
        " after its lifetime ended."),
       N);
   BR->addVisitor<DanglingPtrDerefBRVisitor>(Region);
+  bugreporter::trackExpressionValue(N, bugreporter::getDerefExpr(S), *BR);
   C.emitReport(std::move(BR));
 }
 
diff --git a/clang/test/Analysis/dangling-ptr-deref.cpp b/clang/test/Analysis/dangling-ptr-deref.cpp
index 654b23d0fea64..890ab279a54aa 100644
--- a/clang/test/Analysis/dangling-ptr-deref.cpp
+++ b/clang/test/Analysis/dangling-ptr-deref.cpp
@@ -4,8 +4,8 @@
 void test_case_one() {
   int *ptr = nullptr;
   {
-    int num = 5;
-    ptr = #
+    int num = 5; // expected-note {{'num' initialized to 5}}
+    ptr = # // expected-note  {{Value assigned to 'ptr'}}
   }
   // expected-note at -1 {{'num' is destroyed here}}
   *ptr = 6;
@@ -17,10 +17,10 @@ void test_case_two() {
   int *ptr_one = nullptr;
   int *ptr_two = nullptr;
   {
-    int n = 1;
-    int m = 2;
-    ptr_one = &n;
-    ptr_two = &m;
+    int n = 1; // expected-note {{'n' initialized to 1}}
+    int m = 2; // expected-note {{'m' initialized to 2}}
+    ptr_one = &n; // expected-note {{Value assigned to 'ptr_one'}}
+    ptr_two = &m; // expected-note {{Value assigned to 'ptr_two'}}
   }
   // expected-note at -1 {{'n' is destroyed here}}
   // expected-note at -2 {{'m' is destroyed here}}
@@ -45,7 +45,7 @@ void test_case_three() {
 void test_case_four() {
   int *ptr = nullptr;
   {
-    int num = 5;
+    int num = 5; // expected-note {{'num' initialized to 5}}
     ptr = #
   }
   // expected-note at -1 {{'num' is destroyed here}}
@@ -75,8 +75,8 @@ void test_case_seven() {
   // expected-note at +3 {{Loop condition is true.  Entering loop body}}
   // expected-note at +2 {{Assuming 'i' is >= 10}}
   // expected-note at +1 {{Loop condition is false. Execution continues on line}}
-  for (int i = 0; i < 10; ++i) {
-    ptr = &i;
+  for (int i = 0; i < 10; ++i) { // expected-note {{'i' initialized to 0}}
+    ptr = &i; // expected-note {{Value assigned to 'ptr'}}
     escape(ptr);
   }
   // expected-note at -1 {{'i' is destroyed here}}



More information about the cfe-commits mailing list