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@-1 {{Origin '&n' bound to 'n'}}
-  // expected-note@-2  {{Origin '&n' bound to 'n'}}
+  // expected-note@-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@-1 {{Returning value bound to 'i' that will go out of 
scope}}
   // expected-warning@-2 {{address of stack memory associated with local 
variable 'i' returned}}
-  // expected-note@-3    {{Returning value bound to 'i' that will go out of 
scope}}
+  // expected-note@-3    {{Value's lifetime bound to the lifetime of 'i' here}}
+  // expected-note@-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@-1 {{Returning value bound to 'y' that will go out of 
scope}}
-  // expected-note@-2    {{Returning value bound to 'y' that will go out of 
scope}}
+  // expected-note@-2    {{Lifetime of 'y' ended here}}
 }
 
 int *borrow_from_caller(int *b [[clang::lifetimebound]]) {
@@ -220,11 +220,13 @@ int &dangling_sources_ref() {
   // expected-note@-2 {{'y' initialized here}}
   return multi_param_test_ref(x, y);
   // expected-warning@-1 {{Returning value bound to 'x' that will go out of 
scope}}
-  // expected-note@-2    {{Returning value bound to 'x' that will go out of 
scope}}
-  // expected-warning@-3 {{Returning value bound to 'y' that will go out of 
scope}}
-  // expected-note@-4    {{Returning value bound to 'y' that will go out of 
scope}}
-  // expected-warning@-5 {{reference to stack memory associated with local 
variable 'x' returned}}
-  // expected-warning@-6 {{reference to stack memory associated with local 
variable 'y' returned}}
+  // expected-warning@-2 {{Returning value bound to 'y' that will go out of 
scope}}
+  // expected-warning@-3 {{reference to stack memory associated with local 
variable 'x' returned}}
+  // expected-warning@-4 {{reference to stack memory associated with local 
variable 'y' returned}}
+  // expected-note@-5    {{Value's lifetime bound to the lifetime of 'x' here}}
+  // expected-note@-6    {{Value's lifetime bound to the lifetime of 'y' here}}
+  // expected-note@-7    {{Lifetime of 'x' ended here}}
+  // expected-note@-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@-1 {{Returning value bound to 'x' that will go out of 
scope}}
-  // expected-note@-2    {{Returning value bound to 'x' that will go out of 
scope}}
-  // expected-warning@-3 {{reference to stack memory associated with local 
variable 'x' returned}}
+  // expected-warning@-2 {{reference to stack memory associated with local 
variable 'x' returned}}
+  // expected-note@-3    {{Value's lifetime bound to the lifetime of 'x' here}}
+  // expected-note@-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@-1 {{Returning value bound to 'x' that will go out of 
scope}}
-  // expected-note@-2    {{Returning value bound to 'x' that will go out of 
scope}}
-  // expected-warning@-3 {{Returning value bound to 'y' that will go out of 
scope}}
-  // expected-note@-4    {{Returning value bound to 'y' that will go out of 
scope}}
+  // expected-note@-2    {{Value's lifetime bound to the lifetime of 'x' here}}
+  // expected-note@-3    {{Lifetime of 'x' ended here}}
+  // expected-warning@-4 {{Returning value bound to 'y' that will go out of 
scope}}
+  // expected-note@-5    {{Value's lifetime bound to the lifetime of 'y' here}}
+  // expected-note@-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@-1 {{Returning value bound to 'x' that will go out of 
scope}}
-  // expected-note@-2    {{Returning value bound to 'x' that will go out of 
scope}}
+  // expected-note@-2    {{Value's lifetime bound to the lifetime of 'x' here}}
+  // expected-note@-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@-1 {{Returning value bound to 's' that will go out of 
scope}}
-  // expected-warning@-2 {{Address of stack memory associated with local 
variable 's' returned}}
-  // expected-note@-3    {{Address of stack memory associated with local 
variable 's' returned to caller}}
-  // expected-warning@-4 {{address of stack memory associated with local 
variable 's' returned}}
+  // expected-warning@-2 {{Address of stack memory associated with local 
variable 's' returned to caller}}
+  // expected-warning@-3 {{address of stack memory associated with local 
variable 's' returned}}
+  // expected-note@-4    {{Address of stack memory associated with local 
variable 's' returned to caller}}
+  // expected-note@-5    {{Value's lifetime bound to the lifetime of 's' here}}
+  // expected-note@-6    {{Lifetime of 's' ended here}}
 }
 
 int *danglingParam(S param) {
   return param.get();
   // expected-warning@-1 {{Returning value bound to 'param' that will go out 
of scope}}
-  // expected-note@-2    {{Returning value bound to 'param' that will go out 
of scope}}
-  // expected-warning@-3 {{Address of stack memory associated with local 
variable 'param' returned}}
+  // expected-warning@-2 {{Address of stack memory associated with local 
variable 'param' returned to caller}}
+  // expected-warning@-3 {{address of stack memory associated with parameter 
'param' returned}}
   // expected-note@-4    {{Address of stack memory associated with local 
variable 'param' returned to caller}}
-  // expected-warning@-5 {{address of stack memory associated with parameter 
'param' returned}}
+  // expected-note@-5    {{Value's lifetime bound to the lifetime of 'param' 
here}}
+  // expected-note@-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@-1 {{Returning value bound to 'pair' that will go out of 
scope}}
-  // expected-note@-2    {{Returning value bound to 'pair' that will go out of 
scope}}
-  // expected-warning@-3 {{Address of stack memory associated with local 
variable 'pair' returned to caller}}
+  // expected-warning@-2 {{Address of stack memory associated with local 
variable 'pair' returned to caller}}
+  // expected-warning@-3 {{address of stack memory associated with local 
variable 'pair' returned}}
   // expected-note@-4    {{Address of stack memory associated with local 
variable 'pair' returned to caller}}
-  // expected-warning@-5 {{address of stack memory associated with local 
variable 'pair' returned}}
+  // expected-note@-5    {{Value's lifetime bound to the lifetime of 'pair' 
here}}
+  // expected-note@-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@-1 {{Returning value bound to 'derived' that will go out 
of scope}}
-  // expected-note@-2    {{Returning value bound to 'derived' that will go out 
of scope}}
-  // expected-warning@-3 {{Address of stack memory associated with local 
variable 'derived' returned to caller}}
+  // expected-warning@-2 {{Address of stack memory associated with local 
variable 'derived' returned to caller}}
+  // expected-warning@-3 {{address of stack memory associated with local 
variable 'derived' returned}}
   // expected-note@-4    {{Address of stack memory associated with local 
variable 'derived' returned to caller}}
-  // expected-warning@-5 {{address of stack memory associated with local 
variable 'derived' returned}}
+  // expected-note@-5    {{Value's lifetime bound to the lifetime of 'derived' 
here}}
+  // expected-note@-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@-1 {{Returning value bound to 'outer' that will go out 
of scope}}
-  // expected-note@-2    {{Returning value bound to 'outer' that will go out 
of scope}} 
-  // expected-warning@-3 {{Address of stack memory associated with local 
variable 'outer' returned to caller}}
+  // expected-warning@-2 {{Address of stack memory associated with local 
variable 'outer' returned to caller}}
+  // expected-warning@-3 {{address of stack memory associated with local 
variable 'outer' returned}}
   // expected-note@-4    {{Address of stack memory associated with local 
variable 'outer' returned to caller}}
-  // expected-warning@-5 {{address of stack memory associated with local 
variable 'outer' returned}}
+  // expected-note@-5    {{Value's lifetime bound to the lifetime of 'outer' 
here}}
+  // expected-note@-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@-1 {{Returning value bound to 'buf' that will go out of 
scope}}
-  // expected-note@-2    {{Returning value bound to 'buf' that will go out of 
scope}}
-  // expected-warning@-3 {{Address of stack memory associated with local 
variable 'buf' returned to caller}}
+  // expected-warning@-2 {{Address of stack memory associated with local 
variable 'buf' returned to caller}}
+  // expected-warning@-3 {{address of stack memory associated with local 
variable 'buf' returned}}
   // expected-note@-4    {{Address of stack memory associated with local 
variable 'buf' returned to caller}}
-  // expected-warning@-5 {{address of stack memory associated with local 
variable 'buf' returned}}
+  // expected-note@-5    {{Value's lifetime bound to the lifetime of 'buf' 
here}}
+  // expected-note@-6    {{Lifetime of 'buf' ended here}}
 }
 
 // FIXME: Heap allocated memory regions are not yet handled by the lifetime 
checkers.


        
_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to