[clang] 6a7f6a0 - [analyzer] Implement BugReporterVisitor for UseAfterLifetimeEnd to trace lifetime source binding (#207052)

via cfe-commits cfe-commits at lists.llvm.org
Mon Aug 3 04:50:38 PDT 2026


Author: Benedek Kaibas
Date: 2026-08-03T13:50:34+02:00
New Revision: 6a7f6a02bb95a75c615cde3194ad77d776b869c5

URL: https://github.com/llvm/llvm-project/commit/6a7f6a02bb95a75c615cde3194ad77d776b869c5
DIFF: https://github.com/llvm/llvm-project/commit/6a7f6a02bb95a75c615cde3194ad77d776b869c5.diff

LOG: [analyzer] Implement BugReporterVisitor for UseAfterLifetimeEnd to trace lifetime source binding (#207052)

Currently the `UseAfterLifetimeEnd` checker can emit warnings, but those
warnings cannot clearly describe to which annotated parameter the return
value is actually bound. When multiple parameters are annotated, it is
unclear which one the return value is bound to. Using
`BugReporterVisitor` to trace back the nodes and emit a note that
explains where the lifetime of the annotated parameter (the source)
ended can be helpful for users.

***NOTE***: This PR is built on #205951. It should only be merged after
#205951 is merged.

Consider the following case: 

```cpp
#include <stddef.h>

class Arena {
  char buf[128];
  char *buffer = buf;
  size_t offset = 0;

public:
  void *allocate(size_t size) [[clang::lifetimebound]] {
    void *p = buffer + offset;
    offset += size;
    return p;
  }
  void reset() {offset = 0;}
};

void *arena_dangling() {
  Arena arena;
  void *p = arena.allocate(128);
  arena.reset();
  return p; // arena goes out of scope therefore p dangles
}
```

The `UseAfterLifetimeEnd` checker correctly detects this error and emits
path notes that trace where the value was bound and where its lifetime
ends:

```text
temp.cpp:21:3: warning: Returning value bound to 'arena' that will go out of scope [alpha.cplusplus.UseAfterLifetimeEnd]
   21 |   return p;
      |   ^~~~~~~~
temp.cpp:19:13: note: Value bound to 'arena' here
   19 |   void *p = arena.allocate(128);
      |             ^~~~~~~~~~~~~~~~~~~
temp.cpp:21:3: note: Lifetime of 'arena' ended here
   21 |   return p;
      |   ^~~~~~~~
1 warning generated.
```

The motivating example comes from here:
https://discourse.llvm.org/t/clang-static-analyzer-gsoc-2025-teach-the-clang-static-analyzer-to-understand-lifetime-annotations/84487/41?u=bkaibas01

Added: 
    

Modified: 
    clang/lib/StaticAnalyzer/Checkers/LifetimeModeling.cpp
    clang/lib/StaticAnalyzer/Checkers/LifetimeModeling.h
    clang/lib/StaticAnalyzer/Checkers/UseAfterLifetimeEnd.cpp
    clang/test/Analysis/lifetime-bound.cpp

Removed: 
    


################################################################################
diff  --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeModeling.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeModeling.cpp
index 4b6d76a09575a..7b9fb7acb21ab 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeModeling.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeModeling.cpp
@@ -71,6 +71,11 @@ std::vector<const MemRegion *> lifetime_modeling::getDanglingRegionsAfterReturn(
   return Regions;
 }
 
+bool lifetime_modeling::isBoundToLifetimeSource(ProgramStateRef State,
+                                                SVal Val) {
+  return State->get<LifetimeBoundMap>(Val) != nullptr;
+}
+
 bool lifetime_modeling::isDeallocated(ProgramStateRef State,
                                       const MemRegion *Region) {
   return State->contains<DeallocatedSourceSet>(Region->getBaseRegion());

diff  --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeModeling.h b/clang/lib/StaticAnalyzer/Checkers/LifetimeModeling.h
index 1e2ef8f7810ac..8d6c8e4882d1c 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeModeling.h
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeModeling.h
@@ -16,6 +16,9 @@ getDanglingRegionsAfterReturn(SVal Source, ProgramStateRef State,
 /// Returns true if the underlying MemRegion is deallocated.
 bool isDeallocated(ProgramStateRef State, const MemRegion *Region);
 
+/// Returns true if \p Val is a key in the LifetimeBoundMap.
+bool isBoundToLifetimeSource(ProgramStateRef State, SVal Val);
+
 /// Returns the descriptive name of the memory region or a placeholder if a
 /// descriptive name cannot be constructed for it.
 std::string getRegionName(const MemRegion *Reg);

diff  --git a/clang/lib/StaticAnalyzer/Checkers/UseAfterLifetimeEnd.cpp b/clang/lib/StaticAnalyzer/Checkers/UseAfterLifetimeEnd.cpp
index 0f320eb910930..a9065352adae6 100644
--- a/clang/lib/StaticAnalyzer/Checkers/UseAfterLifetimeEnd.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/UseAfterLifetimeEnd.cpp
@@ -15,8 +15,52 @@ class UseAfterLifetimeEnd : public Checker<check::EndFunction> {
   const BugType BugMsg{this, "UseAfterLifetimeEnd", "LifetimeBound"};
 };
 
+class UseAfterLifetimeEndBRVisitor : public BugReporterVisitor {
+  SVal BoundVal;
+  const MemRegion *SourceRegion;
+
+public:
+  explicit UseAfterLifetimeEndBRVisitor(SVal Val, const MemRegion *Source)
+      : BoundVal(Val), SourceRegion(Source) {}
+
+  void Profile(llvm::FoldingSetNodeID &ID) const override {
+    static int X = 0;
+    ID.AddPointer(&X);
+    BoundVal.Profile(ID);
+    SourceRegion->Profile(ID);
+  }
+
+  PathDiagnosticPieceRef VisitNode(const ExplodedNode *N,
+                                   BugReporterContext &BRC,
+                                   PathSensitiveBugReport &BR) override;
+  PathDiagnosticPieceRef getEndPath(const ExplodedNode *N,
+                                    BugReporterContext &BRC,
+                                    PathSensitiveBugReport &BR) override;
+  PathDiagnosticPieceRef createSourcePiece(const ExplodedNode *N,
+                                           BugReporterContext &BRC,
+                                           StringRef Message) const;
+};
+
 } // namespace
 
+static const Expr *getLifetimeBoundArg(const Expr *RetExpr) {
+  const CallExpr *Expr = dyn_cast_or_null<CallExpr>(RetExpr);
+  if (!Expr)
+    return nullptr;
+  const FunctionDecl *FD = Expr->getDirectCallee();
+  if (!FD)
+    return nullptr;
+
+  for (const ParmVarDecl *PVD : FD->parameters()) {
+    if (PVD->hasAttr<LifetimeBoundAttr>()) {
+      unsigned Idx = PVD->getFunctionScopeIndex();
+      if (Idx < Expr->getNumArgs())
+        return Expr->getArg(Idx);
+    }
+  }
+  return nullptr;
+}
+
 void UseAfterLifetimeEnd::checkEndFunction(const ReturnStmt *RS,
                                            CheckerContext &C) const {
   if (!RS)
@@ -43,6 +87,12 @@ void UseAfterLifetimeEnd::checkEndFunction(const ReturnStmt *RS,
   }
 }
 
+static SourceRange getRegionDeclRange(const MemRegion *Source) {
+  if (const auto *VR = dyn_cast_or_null<VarRegion>(Source))
+    return VR->getDecl()->getSourceRange();
+  return SourceRange();
+}
+
 void UseAfterLifetimeEnd::reportDanglingSource(const MemRegion *Source,
                                                SVal RetVal, ExplodedNode *N,
                                                CheckerContext &C) const {
@@ -51,10 +101,68 @@ void UseAfterLifetimeEnd::reportDanglingSource(const MemRegion *Source,
       (llvm::Twine("Returning value bound to ") +
        lifetime_modeling::getRegionName(Source) + " that will go out of scope"),
       N);
+
+  if (SourceRange Range = getRegionDeclRange(Source); Range.isValid())
+    BR->addRange(Range);
+
+  BR->addVisitor<UseAfterLifetimeEndBRVisitor>(RetVal, Source);
   bugreporter::trackStoredValue(RetVal, Source, *BR);
   C.emitReport(std::move(BR));
 }
 
+PathDiagnosticPieceRef UseAfterLifetimeEndBRVisitor::createSourcePiece(
+    const ExplodedNode *N, BugReporterContext &BRC, StringRef Message) const {
+  const Stmt *S = N->getStmtForDiagnostics();
+  if (!S)
+    return nullptr;
+
+  const Expr *RetExpr = dyn_cast_or_null<Expr>(S);
+  const Expr *Arg = getLifetimeBoundArg(RetExpr);
+
+  PathDiagnosticLocation Pos;
+
+  Pos = PathDiagnosticLocation(Arg ? Arg : S, BRC.getSourceManager(),
+                               N->getStackFrame());
+
+  auto Note = std::make_shared<PathDiagnosticEventPiece>(Pos, Message, true);
+  if (SourceRange Range = getRegionDeclRange(SourceRegion); Range.isValid())
+    Note->addRange(Range);
+
+  return Note;
+}
+
+PathDiagnosticPieceRef
+UseAfterLifetimeEndBRVisitor::VisitNode(const ExplodedNode *N,
+                                        BugReporterContext &BRC,
+                                        PathSensitiveBugReport &BR) {
+  const ExplodedNode *Pred = N->getFirstPred();
+  if (!Pred)
+    return nullptr;
+
+  if (!lifetime_modeling::isBoundToLifetimeSource(N->getState(), BoundVal) ||
+      lifetime_modeling::isBoundToLifetimeSource(Pred->getState(), BoundVal))
+    return nullptr;
+
+  auto Piece = createSourcePiece(
+      N, BRC,
+      (llvm::Twine("Value's lifetime bound to the lifetime of ") +
+       lifetime_modeling::getRegionName(SourceRegion) + " here")
+          .str());
+  return Piece;
+}
+
+PathDiagnosticPieceRef
+UseAfterLifetimeEndBRVisitor::getEndPath(const ExplodedNode *N,
+                                         BugReporterContext &BRC,
+                                         PathSensitiveBugReport &BR) {
+  auto Piece = createSourcePiece(
+      N, BRC,
+      (llvm::Twine("Lifetime of ") +
+       lifetime_modeling::getRegionName(SourceRegion) + " ended here")
+          .str());
+  return Piece;
+}
+
 void ento::registerUseAfterLifetimeEnd(CheckerManager &Mgr) {
   Mgr.registerChecker<UseAfterLifetimeEnd>();
 }

diff  --git a/clang/test/Analysis/lifetime-bound.cpp b/clang/test/Analysis/lifetime-bound.cpp
index ef8ffeb87a8dd..06c6ee9a4dd3a 100644
--- a/clang/test/Analysis/lifetime-bound.cpp
+++ b/clang/test/Analysis/lifetime-bound.cpp
@@ -1,7 +1,6 @@
 // RUN: %clang_analyze_cc1 -analyzer-checker=core,alpha.cplusplus.UseAfterLifetimeEnd,debug.DebugLifetimeModeling \
 // RUN:   -analyzer-config cfg-lifetime=true -analyzer-output=text -verify %s
-// RUN: %clang_analyze_cc1 -analyzer-checker=core,alpha.cplusplus.UseAfterLifetimeEnd,debug.DebugLifetimeModeling \
-// RUN:   -analyzer-config c++-container-inlining=false -analyzer-config cfg-lifetime=true -analyzer-output=text -verify %s
+
 struct A {};
 
 struct Pair {
@@ -100,7 +99,7 @@ void caller_five() {
 
   clang_analyzer_dumpLifetimeOriginsOf(s);
   // expected-warning at -1 {{Origin '&n' bound to 'n'}}
-  // expected-note at -2  {{Origin '&n' bound to 'n'}}
+  // expected-note at -2    {{Origin '&n' bound to 'n'}}
 }
 
 // Free function with both annotated and non-annotated parameters.
@@ -179,19 +178,20 @@ int *test_func(int *p [[clang::lifetimebound]]);
 
 
 int *direct_return() {
-  int i = 5; //expected-note {{'i' initialized here}}
+  int i = 5; // expected-note {{'i' initialized here}}
   return test_func(&i);
   // expected-warning at -1 {{Returning value bound to 'i' that will go out of scope}}
   // expected-warning at -2 {{address of stack memory associated with local variable 'i' returned}}
-  // expected-note at -3    {{Returning value bound to 'i' that will go out of scope}}
+  // expected-note at -3    {{Value's lifetime bound to the lifetime of 'i' here}}
+  // expected-note at -4    {{Lifetime of 'i' ended here}}
 }
 
 int *variable_return() {
   int y = 5; // expected-note {{'y' initialized here}}
-  int *p = test_func(&y);
+  int *p = test_func(&y); // expected-note {{Value's lifetime bound to the lifetime of 'y' here}}
   return p;
   // expected-warning at -1 {{Returning value bound to 'y' that will go out of scope}}
-  // expected-note at -2    {{Returning value bound to 'y' that will go out of scope}}
+  // expected-note at -2    {{Lifetime of 'y' ended here}}
 }
 
 int *borrow_from_caller(int *b [[clang::lifetimebound]]) {
@@ -220,11 +220,13 @@ int &dangling_sources_ref() {
   // expected-note at -2 {{'y' initialized here}}
   return multi_param_test_ref(x, y);
   // expected-warning at -1 {{Returning value bound to 'x' that will go out of scope}}
-  // expected-note at -2    {{Returning value bound to 'x' that will go out of scope}}
-  // expected-warning at -3 {{Returning value bound to 'y' that will go out of scope}}
-  // expected-note at -4    {{Returning value bound to 'y' that will go out of scope}}
-  // expected-warning at -5 {{reference to stack memory associated with local variable 'x' returned}}
-  // expected-warning at -6 {{reference to stack memory associated with local variable 'y' returned}}
+  // expected-warning at -2 {{Returning value bound to 'y' that will go out of scope}}
+  // expected-warning at -3 {{reference to stack memory associated with local variable 'x' returned}}
+  // expected-warning at -4 {{reference to stack memory associated with local variable 'y' returned}}
+  // expected-note at -5    {{Value's lifetime bound to the lifetime of 'x' here}}
+  // expected-note at -6    {{Value's lifetime bound to the lifetime of 'y' here}}
+  // expected-note at -7    {{Lifetime of 'x' ended here}}
+  // expected-note at -8    {{Lifetime of 'y' ended here}}
 }
 
 // Return value bound to annotated parameters (no dangling sources).
@@ -234,11 +236,12 @@ int &no_dangling_sources_ref(int &a [[clang::lifetimebound]], int &b [[clang::li
 
 // Return value bound to annotated parameters (one dangling source).
 int &one_dangling_source_ref(int &a [[clang::lifetimebound]]) {
-  int x = 1; // expected-note {{'x' initialized here}}
+  int x = 1; // expected-note {{'x' initialized here}} 
   return multi_param_test_ref(a, x);
   // expected-warning at -1 {{Returning value bound to 'x' that will go out of scope}}
-  // expected-note at -2    {{Returning value bound to 'x' that will go out of scope}}
-  // expected-warning at -3 {{reference to stack memory associated with local variable 'x' returned}}
+  // expected-warning at -2 {{reference to stack memory associated with local variable 'x' returned}}
+  // expected-note at -3    {{Value's lifetime bound to the lifetime of 'x' here}}
+  // expected-note at -4    {{Lifetime of 'x' ended here}}
 }
 
 int *multi_param_test_ptr(int *a [[clang::lifetimebound]], int *b [[clang::lifetimebound]]);
@@ -252,9 +255,11 @@ int *dangling_sources_ptr() {
   int *y_ptr = &y;
   return multi_param_test_ptr(x_ptr, y_ptr);
   // expected-warning at -1 {{Returning value bound to 'x' that will go out of scope}}
-  // expected-note at -2    {{Returning value bound to 'x' that will go out of scope}}
-  // expected-warning at -3 {{Returning value bound to 'y' that will go out of scope}}
-  // expected-note at -4    {{Returning value bound to 'y' that will go out of scope}}
+  // expected-note at -2    {{Value's lifetime bound to the lifetime of 'x' here}}
+  // expected-note at -3    {{Lifetime of 'x' ended here}}
+  // expected-warning at -4 {{Returning value bound to 'y' that will go out of scope}}
+  // expected-note at -5    {{Value's lifetime bound to the lifetime of 'y' here}}
+  // expected-note at -6    {{Lifetime of 'y' ended here}}
 }
 
 // Return value bound to annotated parameters (no dangling sources).
@@ -268,7 +273,8 @@ int *one_dangling_source_ptr(int *a [[clang::lifetimebound]]) {
   int *x_ptr = &x;
   return multi_param_test_ptr(a, x_ptr);
   // expected-warning at -1 {{Returning value bound to 'x' that will go out of scope}}
-  // expected-note at -2    {{Returning value bound to 'x' that will go out of scope}}
+  // expected-note at -2    {{Value's lifetime bound to the lifetime of 'x' here}}
+  // expected-note at -3    {{Lifetime of 'x' ended here}}
 }
 
 struct S {
@@ -290,20 +296,23 @@ void outer() {
 
 int *danglingLocal() {
   S s; // expected-note {{'s' initialized here}}
-  return s.get(); // expected-note {{Returning value bound to 's' that will go out of scope}}
+  return s.get();
   // expected-warning at -1 {{Returning value bound to 's' that will go out of scope}}
-  // expected-warning at -2 {{Address of stack memory associated with local variable 's' returned}}
-  // expected-note at -3    {{Address of stack memory associated with local variable 's' returned to caller}}
-  // expected-warning at -4 {{address of stack memory associated with local variable 's' returned}}
+  // expected-warning at -2 {{Address of stack memory associated with local variable 's' returned to caller}}
+  // expected-warning at -3 {{address of stack memory associated with local variable 's' returned}}
+  // expected-note at -4    {{Address of stack memory associated with local variable 's' returned to caller}}
+  // expected-note at -5    {{Value's lifetime bound to the lifetime of 's' here}}
+  // expected-note at -6    {{Lifetime of 's' ended here}}
 }
 
 int *danglingParam(S param) {
   return param.get();
   // expected-warning at -1 {{Returning value bound to 'param' that will go out of scope}}
-  // expected-note at -2    {{Returning value bound to 'param' that will go out of scope}}
-  // expected-warning at -3 {{Address of stack memory associated with local variable 'param' returned}}
+  // expected-warning at -2 {{Address of stack memory associated with local variable 'param' returned to caller}}
+  // expected-warning at -3 {{address of stack memory associated with parameter 'param' returned}}
   // expected-note at -4    {{Address of stack memory associated with local variable 'param' returned to caller}}
-  // expected-warning at -5 {{address of stack memory associated with parameter 'param' returned}}
+  // expected-note at -5    {{Value's lifetime bound to the lifetime of 'param' here}}
+  // expected-note at -6    {{Lifetime of 'param' ended here}}
 }
 
 int *getFieldPtr(Pair &p [[clang::lifetimebound]]) { return &p.a; }
@@ -312,10 +321,11 @@ int *field_subobject_dangling() {
   Pair pair{3, 5}; // expected-note {{'pair' initialized here}}
   return getFieldPtr(pair);
   // expected-warning at -1 {{Returning value bound to 'pair' that will go out of scope}}
-  // expected-note at -2    {{Returning value bound to 'pair' that will go out of scope}}
-  // expected-warning at -3 {{Address of stack memory associated with local variable 'pair' returned to caller}}
+  // expected-warning at -2 {{Address of stack memory associated with local variable 'pair' returned to caller}}
+  // expected-warning at -3 {{address of stack memory associated with local variable 'pair' returned}}
   // expected-note at -4    {{Address of stack memory associated with local variable 'pair' returned to caller}}
-  // expected-warning at -5 {{address of stack memory associated with local variable 'pair' returned}}
+  // expected-note at -5    {{Value's lifetime bound to the lifetime of 'pair' here}}
+  // expected-note at -6    {{Lifetime of 'pair' ended here}}
 }
 
 int *getBasePtr(Derived &d [[clang::lifetimebound]]) {
@@ -326,10 +336,11 @@ int *base_subobject_dangling() {
   Derived derived{}; // expected-note {{'derived' initialized here}}
   return getBasePtr(derived);
   // expected-warning at -1 {{Returning value bound to 'derived' that will go out of scope}}
-  // expected-note at -2    {{Returning value bound to 'derived' that will go out of scope}}
-  // expected-warning at -3 {{Address of stack memory associated with local variable 'derived' returned to caller}}
+  // expected-warning at -2 {{Address of stack memory associated with local variable 'derived' returned to caller}}
+  // expected-warning at -3 {{address of stack memory associated with local variable 'derived' returned}}
   // expected-note at -4    {{Address of stack memory associated with local variable 'derived' returned to caller}}
-  // expected-warning at -5 {{address of stack memory associated with local variable 'derived' returned}}
+  // expected-note at -5    {{Value's lifetime bound to the lifetime of 'derived' here}}
+  // expected-note at -6    {{Lifetime of 'derived' ended here}}
 }
 
 int *getNestedFieldPtr(Outer &o [[clang::lifetimebound]]) {
@@ -340,10 +351,11 @@ int *nested_subobject_dangling() {
   Outer outer{}; // expected-note {{'outer' initialized here}}
   return getNestedFieldPtr(outer);
   // expected-warning at -1 {{Returning value bound to 'outer' that will go out of scope}}
-  // expected-note at -2    {{Returning value bound to 'outer' that will go out of scope}} 
-  // expected-warning at -3 {{Address of stack memory associated with local variable 'outer' returned to caller}}
+  // expected-warning at -2 {{Address of stack memory associated with local variable 'outer' returned to caller}}
+  // expected-warning at -3 {{address of stack memory associated with local variable 'outer' returned}}
   // expected-note at -4    {{Address of stack memory associated with local variable 'outer' returned to caller}}
-  // expected-warning at -5 {{address of stack memory associated with local variable 'outer' returned}}
+  // expected-note at -5    {{Value's lifetime bound to the lifetime of 'outer' here}}
+  // expected-note at -6    {{Lifetime of 'outer' ended here}}
 }
 
 int *getArrayElementPtr(Buffer &b [[clang::lifetimebound]]) {
@@ -354,10 +366,11 @@ int *array_member_subobject_dangling() {
   Buffer buf{}; // expected-note {{'buf' initialized here}}
   return getArrayElementPtr(buf);
   // expected-warning at -1 {{Returning value bound to 'buf' that will go out of scope}}
-  // expected-note at -2    {{Returning value bound to 'buf' that will go out of scope}}
-  // expected-warning at -3 {{Address of stack memory associated with local variable 'buf' returned to caller}}
+  // expected-warning at -2 {{Address of stack memory associated with local variable 'buf' returned to caller}}
+  // expected-warning at -3 {{address of stack memory associated with local variable 'buf' returned}}
   // expected-note at -4    {{Address of stack memory associated with local variable 'buf' returned to caller}}
-  // expected-warning at -5 {{address of stack memory associated with local variable 'buf' returned}}
+  // expected-note at -5    {{Value's lifetime bound to the lifetime of 'buf' here}}
+  // expected-note at -6    {{Lifetime of 'buf' ended here}}
 }
 
 // FIXME: Heap allocated memory regions are not yet handled by the lifetime checkers.


        


More information about the cfe-commits mailing list