[clang] [llvm] [analyzer] Implemented a base of detecing lifetimebound annotation (PR #200145)

Benedek Kaibas via cfe-commits cfe-commits at lists.llvm.org
Mon Jun 22 10:46:46 PDT 2026


https://github.com/benedekaibas updated https://github.com/llvm/llvm-project/pull/200145

>From ddb2376d5978755ed7a45ce362d1cbaba6efe416 Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Thu, 28 May 2026 11:37:49 +0200
Subject: [PATCH 01/38] Started implementing the lifetime annotation checker.

---
 .../clang/StaticAnalyzer/Checkers/Checkers.td |  4 +
 .../StaticAnalyzer/Checkers/CMakeLists.txt    |  1 +
 .../Checkers/LifetimeAnnotations.cpp          | 73 +++++++++++++++++++
 3 files changed, 78 insertions(+)
 create mode 100644 clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp

diff --git a/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td b/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td
index eca2afbe340a9..85d59fc139728 100644
--- a/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td
+++ b/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td
@@ -788,6 +788,10 @@ def SmartPtrChecker: Checker<"SmartPtr">,
   Dependencies<[SmartPtrModeling]>,
   Documentation<HasDocumentation>;
 
+def LifetimeAnnotations : Checker<"LifetimeAnnotations">,
+  HelpText<"Check for lifetime violations using lifetime annotations">,
+  Documentation<NotDocumented>;
+
 } // end: "alpha.cplusplus"
 
 //===----------------------------------------------------------------------===//
diff --git a/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt b/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt
index 8a0621077b977..3f426186189fa 100644
--- a/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt
+++ b/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt
@@ -55,6 +55,7 @@ add_clang_library(clangStaticAnalyzerCheckers
   IteratorModeling.cpp
   IteratorRangeChecker.cpp
   IvarInvalidationChecker.cpp
+  LifetimeAnnotations.cpp
   LLVMConventionsChecker.cpp
   LocalizationChecker.cpp
   MacOSKeychainAPIChecker.cpp
diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
new file mode 100644
index 0000000000000..54e98b945c7b3
--- /dev/null
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -0,0 +1,73 @@
+#include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h"
+#include "clang/StaticAnalyzer/Core/Checker.h"
+#include "clang/StaticAnalyzer/Core/PathSensitive/CallDescription.h"
+#include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h"
+#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"
+#include <AllocationState.h>
+
+using namespace clang;
+using namespace ento;
+
+REGISTER_MAP_WITH_PROGRAMSTATE(LifetimeBoundMap, const MemRegion *,
+                               const MemRegion *);
+
+class LifetimeAnnotations : public Checker<check::PostCall> {
+public:
+  void checkPostCall(const CallEvent &Call, CheckerContext &C) const;
+  void printState(raw_ostream &Out, ProgramStateRef State, const char *NL,
+                  const char *Sep) const override;
+};
+
+void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
+                                        CheckerContext &C) const {
+  ProgramStateRef State = C.getState();
+
+  const auto *MethodDecl = dyn_cast_if_present<CXXMethodDecl>(Call.getDecl());
+
+  if (!MethodDecl)
+    return;
+
+  unsigned LBParamIdx = MethodDecl->getNumParams();
+  for (unsigned i = 0; i < MethodDecl->getNumParams(); i++) {
+    if (MethodDecl->getParamDecl(i)->hasAttr<LifetimeBoundAttr>()) {
+      LBParamIdx = i;
+      break;
+    }
+  }
+  if (LBParamIdx == MethodDecl->getNumParams())
+    return;
+
+  SVal RetVal = Call.getReturnValue();
+  const MemRegion *RetValRegion = RetVal.getAsRegion();
+  if (!RetValRegion)
+    return;
+
+  SVal ArgVal = Call.getArgSVal(LBParamIdx);
+  const MemRegion *ArgValRegion = ArgVal.getAsRegion();
+  if (!ArgValRegion)
+    return;
+
+  State = State->set<LifetimeBoundMap>(RetValRegion, ArgValRegion);
+  C.addTransition(State);
+}
+
+void LifetimeAnnotations::printState(raw_ostream &Out, ProgramStateRef State,
+                                     const char *NL, const char *Sep) const {
+  auto LBTy = State->get<LifetimeBoundMap>();
+
+  if (!LBTy.isEmpty()) {
+    Out << Sep << "LifetimeBound objects: ";
+
+    for (auto I : LBTy) {
+      Out << I.first << " bound to " << I.second << NL;
+    }
+  }
+}
+
+void ento::registerLifetimeAnnotations(CheckerManager &mgr) {
+  mgr.registerChecker<LifetimeAnnotations>();
+}
+
+bool ento::shouldRegisterLifetimeAnnotations(const CheckerManager &mgr) {
+  return true;
+}

>From 366c901261ff3b734d07d5399657d3d41f9e5d1a Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Sun, 31 May 2026 00:40:48 +0200
Subject: [PATCH 02/38] Addressed mentors feedback.

---
 .../Checkers/LifetimeAnnotations.cpp          | 68 ++++++++++++-------
 1 file changed, 44 insertions(+), 24 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index 54e98b945c7b3..4052e13859041 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -3,12 +3,15 @@
 #include "clang/StaticAnalyzer/Core/PathSensitive/CallDescription.h"
 #include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h"
 #include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"
-#include <AllocationState.h>
+#include "AllocationState.h"
+#include "clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h"
+
 
 using namespace clang;
 using namespace ento;
+using namespace clang::lifetimes;
 
-REGISTER_MAP_WITH_PROGRAMSTATE(LifetimeBoundMap, const MemRegion *,
+REGISTER_MAP_WITH_PROGRAMSTATE(LifetimeBoundMap, SymbolRef,
                                const MemRegion *);
 
 class LifetimeAnnotations : public Checker<check::PostCall> {
@@ -22,45 +25,62 @@ void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
                                         CheckerContext &C) const {
   ProgramStateRef State = C.getState();
 
-  const auto *MethodDecl = dyn_cast_if_present<CXXMethodDecl>(Call.getDecl());
+  const auto *FC = dyn_cast_if_present<AnyFunctionCall>(&Call);
+  if (!FC)
+    return;
 
-  if (!MethodDecl)
+  const FunctionDecl *FD = FC->getDecl();
+  if (!FD)
     return;
 
-  unsigned LBParamIdx = MethodDecl->getNumParams();
-  for (unsigned i = 0; i < MethodDecl->getNumParams(); i++) {
-    if (MethodDecl->getParamDecl(i)->hasAttr<LifetimeBoundAttr>()) {
-      LBParamIdx = i;
+  unsigned LBParamIdx = FD->getNumParams();
+  // FIXME: Use range based for loop instead. Currently that would require
+  // to also change how we create ArgVal which would need a new logic to
+  // be implemented.
+  for (unsigned I = 0, E = FD->getNumParams(); I != E; I++) {
+    if (FD->getParamDecl(I)->hasAttr<LifetimeBoundAttr>()) {
+      LBParamIdx = I;
+      // FIXME: If multiple parameters are annotated this logic would
+      // prevent the analyzer to read after the first parameter.
       break;
     }
   }
-  if (LBParamIdx == MethodDecl->getNumParams())
-    return;
-
   SVal RetVal = Call.getReturnValue();
-  const MemRegion *RetValRegion = RetVal.getAsRegion();
-  if (!RetValRegion)
-    return;
 
-  SVal ArgVal = Call.getArgSVal(LBParamIdx);
-  const MemRegion *ArgValRegion = ArgVal.getAsRegion();
-  if (!ArgValRegion)
+  SymbolRef RetValSym = RetVal.getAsSymbol(/*IncludeBaseRegions=*/true);
+  if(!RetValSym)
     return;
 
-  State = State->set<LifetimeBoundMap>(RetValRegion, ArgValRegion);
+  if (LBParamIdx != FD->getNumParams()) {
+    SVal ArgVal = Call.getArgSVal(LBParamIdx);
+    const MemRegion *ArgValRegion = ArgVal.getAsRegion();
+    // FIXME: if(!ArgValRegion) should be also handled since in those cases
+    // the argument has no region, but still needs to be tracked.
+    if (ArgValRegion)
+        State = State->set<LifetimeBoundMap>(RetValSym, ArgValRegion);
+  }
+
+  if (const auto *IC = dyn_cast<CXXInstanceCall>(&Call)) {
+    if (implicitObjectParamIsLifetimeBound(FD)) {
+      const MemRegion *AttrRegion = IC->getCXXThisVal().getAsRegion();
+
+      if (AttrRegion)
+          State = State->set<LifetimeBoundMap>(RetValSym, AttrRegion);
+    }
+  }
   C.addTransition(State);
 }
 
 void LifetimeAnnotations::printState(raw_ostream &Out, ProgramStateRef State,
                                      const char *NL, const char *Sep) const {
-  auto LBTy = State->get<LifetimeBoundMap>();
+  auto LBVal = State->get<LifetimeBoundMap>();
 
-  if (!LBTy.isEmpty()) {
-    Out << Sep << "LifetimeBound objects: ";
+  if (LBVal.isEmpty())
+    return;
 
-    for (auto I : LBTy) {
-      Out << I.first << " bound to " << I.second << NL;
-    }
+  Out << Sep << "LifetimeBound bindings:" << NL;
+  for (auto&& [RetValSym, ArgValRegion] : LBVal) {
+    Out << " Origin " << RetValSym << " contains Loan " << ArgValRegion << NL;
   }
 }
 

>From 9485add27440249983376c89fc3933d8d75a6c32 Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Fri, 5 Jun 2026 13:33:50 +0200
Subject: [PATCH 03/38] Save the current work that has the test cases and the
 helper function implemented.

---
 ...-AST-matching-to-get-containers-regi.patch | 130 ++++++++++++++++++
 .../Checkers/LifetimeAnnotations.cpp          | 108 +++++++++++++--
 clang/test/Analysis/lifetime-bound.cpp        | 111 +++++++++++++++
 3 files changed, 335 insertions(+), 14 deletions(-)
 create mode 100644 0001-analyzer-Removed-AST-matching-to-get-containers-regi.patch
 create mode 100644 clang/test/Analysis/lifetime-bound.cpp

diff --git a/0001-analyzer-Removed-AST-matching-to-get-containers-regi.patch b/0001-analyzer-Removed-AST-matching-to-get-containers-regi.patch
new file mode 100644
index 0000000000000..450bdda9f70dc
--- /dev/null
+++ b/0001-analyzer-Removed-AST-matching-to-get-containers-regi.patch
@@ -0,0 +1,130 @@
+From 00c5cf9e103a88ad9af285104f878c5f932ad8ff Mon Sep 17 00:00:00 2001
+From: benedekaibas <kaibas.benedek02 at gmail.com>
+Date: Tue, 2 Jun 2026 13:13:46 +0200
+Subject: [PATCH] [analyzer] Removed AST matching to get containers' region and
+ fixed small issues.
+
+---
+ .../StaticAnalyzer/Checkers/MoveChecker.cpp   | 40 +++++++++++--------
+ .../test/Analysis/use-after-move-iterator.cpp | 24 +++++------
+ 2 files changed, 36 insertions(+), 28 deletions(-)
+
+diff --git a/clang/lib/StaticAnalyzer/Checkers/MoveChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/MoveChecker.cpp
+index 47e9c585054a..9ff2e90b618a 100644
+--- a/clang/lib/StaticAnalyzer/Checkers/MoveChecker.cpp
++++ b/clang/lib/StaticAnalyzer/Checkers/MoveChecker.cpp
+@@ -518,31 +518,29 @@ bool MoveChecker::evalCall(const CallEvent &Call, CheckerContext &C) const {
+   if (!StdMoveCall.matches(Call))
+     return false;
+ 
+-  const auto *BeginCall =
+-      dyn_cast<CXXMemberCallExpr>(CE->getArg(0)->IgnoreImpCasts());
+-  if (!BeginCall)
++  const auto *POS = getIteratorPosition(State, Call.getArgSVal(0));
++  if (!POS)
+     return false;
+ 
+-  const Expr *ContainerExpr = BeginCall->getImplicitObjectArgument();
+-  const auto *DRE = dyn_cast<DeclRefExpr>(ContainerExpr->IgnoreImpCasts());
+-  if (!DRE)
++  const MemRegion *ContainerRegion = POS->getContainer();
++  if (!ContainerRegion)
+     return false;
+ 
+-  const auto *VD = dyn_cast<VarDecl>(DRE->getDecl());
+-  if (!VD)
++  const auto *TypedRegion =
++      dyn_cast_if_present<TypedValueRegion>(ContainerRegion);
++  if (!TypedRegion)
+     return false;
+ 
+-  const MemRegion *Region =
+-      State->getLValue(VD, C.getLocationContext()).getAsRegion();
+-  if (!Region)
+-    return false;
++  QualType ObjTy = TypedRegion->getValueType();
+ 
+-  const CXXRecordDecl *RD = ContainerExpr->getType()->getAsCXXRecordDecl();
++  const auto *RD = ObjTy->getAsCXXRecordDecl();
+   if (!RD)
+     return false;
+ 
+-  ObjectKind OK = classifyObject(State, Region, RD);
++  ObjectKind OK = classifyObject(State, ContainerRegion, RD);
+ 
++  // FIXME: Also apply getIteratorPosition from IteratorModeling to recover the
++  // destination region instead of doing AST pattern matching.
+   const auto *BackInsCall = dyn_cast<CallExpr>(CE->getArg(2)->IgnoreImpCasts());
+   if (!BackInsCall)
+     return false;
+@@ -573,7 +571,8 @@ bool MoveChecker::evalCall(const CallEvent &Call, CheckerContext &C) const {
+                                    /*CausesPointerEscape=*/false);
+ 
+   if (shouldBeTracked(OK))
+-    State = State->set<TrackedContentsMap>(Region, RegionState::getMoved());
++    State = State->set<TrackedContentsMap>(ContainerRegion,
++                                           RegionState::getMoved());
+ 
+   C.addTransition(State);
+   return true;
+@@ -736,10 +735,19 @@ void MoveChecker::checkPreCall(const CallEvent &Call, CheckerContext &C) const {
+ 
+     if (const auto *POS = getIteratorPosition(State, Val)) {
+       const MemRegion *ContainerRegion = POS->getContainer();
++      if (!ContainerRegion)
++        return;
++
++      const auto *TypedRegion =
++          dyn_cast_if_present<TypedValueRegion>(ContainerRegion);
++      if (!TypedRegion)
++        return;
+ 
+-      const auto *TypedRegion = cast<TypedValueRegion>(ContainerRegion);
+       QualType ObjTy = TypedRegion->getValueType();
+       const auto *R = ObjTy->getAsCXXRecordDecl();
++      if (!R)
++        return;
++
+       if (State->get<TrackedContentsMap>(ContainerRegion)) {
+         ExplodedNode *N = tryToReportBug(ContainerRegion, R, C, MK_FunCall);
+         if (!N || N->isSink())
+diff --git a/clang/test/Analysis/use-after-move-iterator.cpp b/clang/test/Analysis/use-after-move-iterator.cpp
+index 50dd7e57b42e..2357be3a6bb3 100644
+--- a/clang/test/Analysis/use-after-move-iterator.cpp
++++ b/clang/test/Analysis/use-after-move-iterator.cpp
+@@ -10,20 +10,20 @@
+ // IteratorModeling is enabled.
+ //===----------------------------------------------------------------------===//
+ 
+-void iteratorDerefSource() {
++std::string iteratorDeref(int rng) {
+   std::list<std::string> l1;
+   l1.push_back("l1");
+   std::list<std::string> l2;
+ 
+-  std::move(l1.begin(), l1.end(), std::back_inserter(l2));
+-  *l1.cbegin(); // expected-warning {{Method called on moved-from object 'l1'}}
+-}
+-
+-void iteratorDerefDest() {
+-  std::list<std::string> l1;
+-  l1.push_back("l1");
+-  std::list<std::string> l2;
+-
+-  std::move(l1.begin(), l1.end(), std::back_inserter(l2));
+-  *l2.cbegin(); // no-warning
++  switch (rng) {
++    case 10: {
++      std::move(l1.begin(), l1.end(), std::back_inserter(l2));
++      return *l1.cbegin(); // expected-warning {{Method called on moved-from object 'l1'}}
++    }
++    case 20: {
++      std::move(l1.begin(), l1.end(), std::back_inserter(l2));
++      return *l2.cbegin(); // no-warning: only l1 was invalidated and not l2!
++    }
++  }
++  return 0;
+ }
+-- 
+2.43.0
+
diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index 4052e13859041..1f93d25d7a788 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -3,6 +3,7 @@
 #include "clang/StaticAnalyzer/Core/PathSensitive/CallDescription.h"
 #include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h"
 #include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"
+#include "llvm/Support/raw_ostream.h"
 #include "AllocationState.h"
 #include "clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h"
 
@@ -13,16 +14,30 @@ using namespace clang::lifetimes;
 
 REGISTER_MAP_WITH_PROGRAMSTATE(LifetimeBoundMap, SymbolRef,
                                const MemRegion *);
+REGISTER_MAP_WITH_PROGRAMSTATE(LifetimeBoundMapVal, const MemRegion *, const MemRegion *);
 
-class LifetimeAnnotations : public Checker<check::PostCall> {
+
+class LifetimeAnnotations : public Checker<check::PostCall, eval::Call> {
 public:
   void checkPostCall(const CallEvent &Call, CheckerContext &C) const;
   void printState(raw_ostream &Out, ProgramStateRef State, const char *NL,
                   const char *Sep) const override;
+  bool evalCall(const CallEvent &Call, CheckerContext &C) const;
+  void analyzerLifetimeBound(const CallEvent &Call, const CallExpr *, CheckerContext &C) const;
+
+  const BugType BugMsg{this, "LifetimeAnnotations", "LifetimeBound"};
+};
+
+typedef void (LifetimeAnnotations::*FnCheck)(const CallEvent &Call, const CallExpr *,
+                                              CheckerContext &) const;
+CallDescriptionMap<FnCheck> Callbacks = {
+  {{CDM::SimpleFunc, {"clang_analyzer_lifetime_bound"}},
+    &LifetimeAnnotations::analyzerLifetimeBound},
 };
 
 void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
                                         CheckerContext &C) const {
+  llvm::errs() << "checkPostCall fired" << "\n";
   ProgramStateRef State = C.getState();
 
   const auto *FC = dyn_cast_if_present<AnyFunctionCall>(&Call);
@@ -33,6 +48,8 @@ void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
   if (!FD)
     return;
 
+  SVal RetVal = Call.getReturnValue();
+  SymbolRef RetValSym = RetVal.getAsSymbol(/*IncludeBaseRegions=*/true);
   unsigned LBParamIdx = FD->getNumParams();
   // FIXME: Use range based for loop instead. Currently that would require
   // to also change how we create ArgVal which would need a new logic to
@@ -45,27 +62,34 @@ void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
       break;
     }
   }
-  SVal RetVal = Call.getReturnValue();
-
-  SymbolRef RetValSym = RetVal.getAsSymbol(/*IncludeBaseRegions=*/true);
-  if(!RetValSym)
-    return;
 
   if (LBParamIdx != FD->getNumParams()) {
     SVal ArgVal = Call.getArgSVal(LBParamIdx);
-    const MemRegion *ArgValRegion = ArgVal.getAsRegion();
-    // FIXME: if(!ArgValRegion) should be also handled since in those cases
-    // the argument has no region, but still needs to be tracked.
-    if (ArgValRegion)
+    if (const MemRegion *ArgValRegion = ArgVal.getAsRegion()) {
+      if (RetValSym)
+        llvm::errs() << "RetValSym: ";
+        RetValSym->dump();
+        llvm::errs() << "\n";
         State = State->set<LifetimeBoundMap>(RetValSym, ArgValRegion);
+        llvm::errs() << "State got set with RetValSym" << "\n";
+        C.getState()->dump();
+        llvm::errs() << "\n";
+      if (const MemRegion *RetValRegion = RetVal.getAsRegion())
+        State = State->set<LifetimeBoundMapVal>(RetValRegion, ArgValRegion);
+    }
   }
 
   if (const auto *IC = dyn_cast<CXXInstanceCall>(&Call)) {
+    llvm::errs() << "isCXXThisVal true" << "\n";
     if (implicitObjectParamIsLifetimeBound(FD)) {
-      const MemRegion *AttrRegion = IC->getCXXThisVal().getAsRegion();
-
-      if (AttrRegion)
+      llvm::errs() << "isLifetimeBound true" << "\n";
+      if (const MemRegion *AttrRegion = IC->getCXXThisVal().getAsRegion()) {
+        llvm::errs() << "is AttrRegion non null" << "\n";
+        if (RetValSym)
           State = State->set<LifetimeBoundMap>(RetValSym, AttrRegion);
+        if (const MemRegion *RetValRegion = RetVal.getAsRegion())
+          State = State->set<LifetimeBoundMapVal>(RetValRegion, AttrRegion);
+      }
     }
   }
   C.addTransition(State);
@@ -74,14 +98,70 @@ void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
 void LifetimeAnnotations::printState(raw_ostream &Out, ProgramStateRef State,
                                      const char *NL, const char *Sep) const {
   auto LBVal = State->get<LifetimeBoundMap>();
+  auto LBValTwo = State->get<LifetimeBoundMapVal>();
 
-  if (LBVal.isEmpty())
+  if (LBVal.isEmpty() && LBValTwo.isEmpty())
     return;
 
   Out << Sep << "LifetimeBound bindings:" << NL;
   for (auto&& [RetValSym, ArgValRegion] : LBVal) {
     Out << " Origin " << RetValSym << " contains Loan " << ArgValRegion << NL;
   }
+  for (auto&& [RetVal, ArgValRegion]: LBValTwo) {
+    Out << " Origin " << RetVal << " contains Loan " << ArgValRegion << NL;
+  }
+}
+
+bool LifetimeAnnotations::evalCall(const CallEvent &Call, CheckerContext &C) const {
+
+  const auto *CE = llvm::dyn_cast_if_present<CallExpr>(Call.getOriginExpr());
+  if (!CE)
+    return false;
+
+  const FnCheck *Handler = Callbacks.lookup(Call);
+  if (!Handler)
+    return false;
+
+  (this->*(*Handler))(Call, CE, C);
+  return true;
+  C.addTransition(C.getState());
+}
+
+void LifetimeAnnotations::analyzerLifetimeBound(const CallEvent &Call, const CallExpr *CE, CheckerContext &C) const {
+  llvm::errs() << "\n";
+  llvm::errs() << "lifetime_bound called" << "\n";
+  ProgramStateRef State = C.getState();
+  unsigned int ArgExpr = CE->getNumArgs();
+  if (ArgExpr != 1)
+    return;
+
+  SVal ArgSVal = Call.getArgSVal(0);
+
+  const MemRegion *ArgValRegion = ArgSVal.getAsRegion();
+  SymbolRef ArgSValSym = ArgSVal.getAsSymbol(/*IncludeBaseRegions=*/true);
+
+  llvm::SmallString<128> Str;
+  llvm::raw_svector_ostream OS(Str);
+  ExplodedNode *N = C.generateNonFatalErrorNode();
+  if (!N)
+    return;
+
+  if (ArgSValSym) {
+    if (const auto *ArgValLookFor = State->get<LifetimeBoundMap>(ArgSValSym)) {
+      OS << " Origin " << ArgSValSym << " contains loan " << *ArgValLookFor;
+      auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, OS.str(), N);
+      C.emitReport(std::move(BR));
+      Str.clear();
+    }
+  }
+  if (ArgValRegion) {
+    if (const auto *AttrValLookFor = State->get<LifetimeBoundMapVal>(ArgValRegion)) {
+      OS << " Origin " << ArgValRegion << " bound to " << *AttrValLookFor;
+      auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, OS.str(), N);
+      C.emitReport(std::move(BR));
+      Str.clear();
+    }
+  }
 }
 
 void ento::registerLifetimeAnnotations(CheckerManager &mgr) {
diff --git a/clang/test/Analysis/lifetime-bound.cpp b/clang/test/Analysis/lifetime-bound.cpp
new file mode 100644
index 0000000000000..1cedfe5b0398f
--- /dev/null
+++ b/clang/test/Analysis/lifetime-bound.cpp
@@ -0,0 +1,111 @@
+// RUN: %clang_analyze_cc1 -analyzer-checker=alpha.cplusplus.LifetimeAnnotations \
+// RUN:   -verify %s
+// RUN: %clang_analyze_cc1 -analyzer-checker=alpha.cplusplus.LifetimeAnnotations \
+// RUN:   -analyzer-config c++-container-inlining=false -verify %s
+
+void clang_analyzer_dump(...);
+
+// These are the cases when the result of function calls are MemRegions.
+
+struct A {};
+
+// Ref type parameter annotated case
+struct X {
+  int& choose(int& a [[clang::lifetimebound]]) { return a; }
+};
+
+void clang_analyzer_lifetime_bound(int&);
+
+void caller() {
+  int v = 0;
+  X obj;
+  int& r = obj.choose(v);
+  clang_analyzer_lifetime_bound(r); // expected-warning {{Origin v bound to v}}
+  clang_analyzer_dump(r);
+}
+
+// Obj ref type function return annotated case
+struct Y {
+  A a;
+  A& getA() [[clang::lifetimebound]] { return a; }
+};
+
+void clang_analyzer_lifetime_bound(A& a);
+
+void caller_two() {
+  // Return statement is annotated case.
+  Y y;
+  A& f = y.getA();
+  clang_analyzer_lifetime_bound(f); // expected-warning {{Origin y.a bound to y}}
+  clang_analyzer_dump(f);
+}
+
+// Obj ptr type function return annotated case
+struct Z {
+  A a;
+  A* getA() [[clang::lifetimebound]] { return &a; }
+};
+
+void clang_analyzer_lifetime_bound(A* a);
+
+void caller_three() {
+  Z z;
+  A* func = z.getA();
+  clang_analyzer_lifetime_bound(func); // expected-warning {{Origin z.a bound to z}}
+  clang_analyzer_dump(func);
+}
+
+// Free function with annotated param and ref return
+int& foo(int& num [[clang::lifetimebound]]) { return num; }
+
+void clang_analyzer_lifetime_bound(int&);
+
+void caller_four() {
+  int num = 5;
+  int& s = foo(num);
+  clang_analyzer_lifetime_bound(s); // expected-warning {{Origin num bound to num}}
+  clang_analyzer_dump(s);
+}
+
+// Free function with annotated param and ptr return
+int* boo(int* num [[clang::lifetimebound]]) { return num; }
+
+void clang_analyzer_lifetime_bound(int*);
+
+void caller_five() {
+  int n = 55;
+  int* n_ptr = &n;
+  int* s = boo(n_ptr);
+
+  clang_analyzer_lifetime_bound(s); // expected-warning {{Origin n bound to n}}
+  clang_analyzer_dump(s);
+}
+
+// These are the cases when the result of function calls are SymbolRefs.
+
+// Function returns ptr and has an annotated parameter
+int* foo(int* n [[clang::lifetimebound]]);
+
+void clang_analyzer_lifetime_bound(int*);
+
+void caller_six() {
+  int y = 15;
+  int* y_ptr = &y;
+  auto bind = foo(y_ptr);
+
+  clang_analyzer_lifetime_bound(bind);
+                                       // expected-warning at -1 {{Origin bound to n}}
+                                      // expected-warning at -1 {{Origin contains loan n}}
+  clang_analyzer_dump(bind);
+
+// FIXME: The full warning does look like this:
+// Origin SymRegion{conj_$5{int *, LC1, S847, #1}} bound to n
+// Origin conj_$5{int *, LC1, S847, #1} contains loan n
+// Since the conj sym number and the ID can change across runs I have decided to just include
+// string parts of the error message since that is the only consistent part of the emitted report.
+// This does not apply to the test cases above this test case.
+}
+
+
+// Function returns a reference and has an annotated parameter
+

>From f68a1d5b83cb2ed761a7f6e5c9f8d2f82b61ee20 Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Fri, 5 Jun 2026 16:09:28 +0200
Subject: [PATCH 04/38] Save the current work that has the test cases and the
 helper function implemented + more test cases.

---
 .../Checkers/LifetimeAnnotations.cpp             |  9 ++++++++-
 clang/test/Analysis/lifetime-bound.cpp           | 16 ++++++++++++++++
 2 files changed, 24 insertions(+), 1 deletion(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index 1f93d25d7a788..787698671ae75 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -145,7 +145,7 @@ void LifetimeAnnotations::analyzerLifetimeBound(const CallEvent &Call, const Cal
   ExplodedNode *N = C.generateNonFatalErrorNode();
   if (!N)
     return;
-
+  llvm::errs() << "ArgSValSym: " << (ArgSValSym ? "non-null" : "null") << "\n";
   if (ArgSValSym) {
     if (const auto *ArgValLookFor = State->get<LifetimeBoundMap>(ArgSValSym)) {
       OS << " Origin " << ArgSValSym << " contains loan " << *ArgValLookFor;
@@ -154,7 +154,13 @@ void LifetimeAnnotations::analyzerLifetimeBound(const CallEvent &Call, const Cal
       Str.clear();
     }
   }
+
+  llvm::errs() << "ArgValRegion: " << (ArgValRegion ? "non-null" : "null") << "\n";
   if (ArgValRegion) {
+    llvm::errs() << "\n";
+    llvm::errs() << "ArgValRegion: ";
+    ArgValRegion->dump();
+    llvm::errs() << "\n";
     if (const auto *AttrValLookFor = State->get<LifetimeBoundMapVal>(ArgValRegion)) {
       OS << " Origin " << ArgValRegion << " bound to " << *AttrValLookFor;
       auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, OS.str(), N);
@@ -164,6 +170,7 @@ void LifetimeAnnotations::analyzerLifetimeBound(const CallEvent &Call, const Cal
   }
 }
 
+
 void ento::registerLifetimeAnnotations(CheckerManager &mgr) {
   mgr.registerChecker<LifetimeAnnotations>();
 }
diff --git a/clang/test/Analysis/lifetime-bound.cpp b/clang/test/Analysis/lifetime-bound.cpp
index 1cedfe5b0398f..8cca08aca3f1c 100644
--- a/clang/test/Analysis/lifetime-bound.cpp
+++ b/clang/test/Analysis/lifetime-bound.cpp
@@ -108,4 +108,20 @@ void caller_six() {
 
 
 // Function returns a reference and has an annotated parameter
+int& func(int& some_number [[clang::lifetimebound]]);
+
+void clang_analyzer_lifetime_bound(int&);
+
+void caller_seven() {
+  int f = 15;
+  auto& bind = func(f);
+
+  clang_analyzer_lifetime_bound(bind);
+                                       // expected-warning at -1 {{Origin bound to some_number}}
+                                      // expected-warning at -1 {{Origin contains loan some_number}}
+  clang_analyzer_dump(bind);
+
+// The FIXME about the full warning applies to this text case as well.
+}
+
 

>From 674c40e3669705a2a796508d520db3526a947eae Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Fri, 5 Jun 2026 16:37:42 +0200
Subject: [PATCH 05/38] Removed debugged comments.

---
 .../Checkers/LifetimeAnnotations.cpp          | 20 ++--------------
 clang/test/Analysis/lifetime-bound.cpp        | 24 +++++++++++++++----
 2 files changed, 22 insertions(+), 22 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index 787698671ae75..8baf4b0ba223b 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -67,24 +67,15 @@ void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
     SVal ArgVal = Call.getArgSVal(LBParamIdx);
     if (const MemRegion *ArgValRegion = ArgVal.getAsRegion()) {
       if (RetValSym)
-        llvm::errs() << "RetValSym: ";
-        RetValSym->dump();
-        llvm::errs() << "\n";
         State = State->set<LifetimeBoundMap>(RetValSym, ArgValRegion);
-        llvm::errs() << "State got set with RetValSym" << "\n";
-        C.getState()->dump();
-        llvm::errs() << "\n";
       if (const MemRegion *RetValRegion = RetVal.getAsRegion())
         State = State->set<LifetimeBoundMapVal>(RetValRegion, ArgValRegion);
     }
   }
 
   if (const auto *IC = dyn_cast<CXXInstanceCall>(&Call)) {
-    llvm::errs() << "isCXXThisVal true" << "\n";
     if (implicitObjectParamIsLifetimeBound(FD)) {
-      llvm::errs() << "isLifetimeBound true" << "\n";
       if (const MemRegion *AttrRegion = IC->getCXXThisVal().getAsRegion()) {
-        llvm::errs() << "is AttrRegion non null" << "\n";
         if (RetValSym)
           State = State->set<LifetimeBoundMap>(RetValSym, AttrRegion);
         if (const MemRegion *RetValRegion = RetVal.getAsRegion())
@@ -128,8 +119,7 @@ bool LifetimeAnnotations::evalCall(const CallEvent &Call, CheckerContext &C) con
 }
 
 void LifetimeAnnotations::analyzerLifetimeBound(const CallEvent &Call, const CallExpr *CE, CheckerContext &C) const {
-  llvm::errs() << "\n";
-  llvm::errs() << "lifetime_bound called" << "\n";
+
   ProgramStateRef State = C.getState();
   unsigned int ArgExpr = CE->getNumArgs();
   if (ArgExpr != 1)
@@ -145,7 +135,7 @@ void LifetimeAnnotations::analyzerLifetimeBound(const CallEvent &Call, const Cal
   ExplodedNode *N = C.generateNonFatalErrorNode();
   if (!N)
     return;
-  llvm::errs() << "ArgSValSym: " << (ArgSValSym ? "non-null" : "null") << "\n";
+
   if (ArgSValSym) {
     if (const auto *ArgValLookFor = State->get<LifetimeBoundMap>(ArgSValSym)) {
       OS << " Origin " << ArgSValSym << " contains loan " << *ArgValLookFor;
@@ -155,12 +145,7 @@ void LifetimeAnnotations::analyzerLifetimeBound(const CallEvent &Call, const Cal
     }
   }
 
-  llvm::errs() << "ArgValRegion: " << (ArgValRegion ? "non-null" : "null") << "\n";
   if (ArgValRegion) {
-    llvm::errs() << "\n";
-    llvm::errs() << "ArgValRegion: ";
-    ArgValRegion->dump();
-    llvm::errs() << "\n";
     if (const auto *AttrValLookFor = State->get<LifetimeBoundMapVal>(ArgValRegion)) {
       OS << " Origin " << ArgValRegion << " bound to " << *AttrValLookFor;
       auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, OS.str(), N);
@@ -170,7 +155,6 @@ void LifetimeAnnotations::analyzerLifetimeBound(const CallEvent &Call, const Cal
   }
 }
 
-
 void ento::registerLifetimeAnnotations(CheckerManager &mgr) {
   mgr.registerChecker<LifetimeAnnotations>();
 }
diff --git a/clang/test/Analysis/lifetime-bound.cpp b/clang/test/Analysis/lifetime-bound.cpp
index 8cca08aca3f1c..2acc1734644f2 100644
--- a/clang/test/Analysis/lifetime-bound.cpp
+++ b/clang/test/Analysis/lifetime-bound.cpp
@@ -81,6 +81,8 @@ void caller_five() {
   clang_analyzer_dump(s);
 }
 
+
+
 // These are the cases when the result of function calls are SymbolRefs.
 
 // Function returns ptr and has an annotated parameter
@@ -91,11 +93,11 @@ void clang_analyzer_lifetime_bound(int*);
 void caller_six() {
   int y = 15;
   int* y_ptr = &y;
-  auto bind = foo(y_ptr);
+  auto* bind = foo(y_ptr);
 
   clang_analyzer_lifetime_bound(bind);
-                                       // expected-warning at -1 {{Origin bound to n}}
-                                      // expected-warning at -1 {{Origin contains loan n}}
+                                       // expected-warning at -1 {{Origin bound to y}}
+                                       // expected-warning at -1 {{Origin contains loan y}}
   clang_analyzer_dump(bind);
 
 // FIXME: The full warning does look like this:
@@ -118,10 +120,24 @@ void caller_seven() {
 
   clang_analyzer_lifetime_bound(bind);
                                        // expected-warning at -1 {{Origin bound to some_number}}
-                                      // expected-warning at -1 {{Origin contains loan some_number}}
+                                       // expected-warning at -1 {{Origin contains loan some_number}}
   clang_analyzer_dump(bind);
 
 // The FIXME about the full warning applies to this text case as well.
 }
 
+// Function returns a reference and has two annotated parameters.
+int& f(int& a [[clang::lifetimebound]], int& b [[clang::lifetimebound]]);
+
+void clang_analyzer_lifetime_bound(int&);
+
+void caller_eight() {
+  int first_num = 1;
+  int second_num = 2;
+  auto numbers = f(first_num, second_num);
 
+  clang_analyzer_lifetime_bound(numbers);
+                                          // expected-warning at -1 {{Origin bound to first_num}}
+                                          // expected-warning at -1 {{Origin contains loan first_num}}
+  clang_analyzer_dump(numbers);
+}

>From dc7c4fe09ef95e6b6b7bea294043a8a859f3c480 Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Fri, 5 Jun 2026 18:27:00 +0200
Subject: [PATCH 06/38] Done with TODOs for testing, but they have to be
 cleaned.

---
 .../Checkers/LifetimeAnnotations.cpp          | 10 +++++-----
 clang/test/Analysis/lifetime-bound.cpp        | 20 ++++++++++++++++---
 2 files changed, 22 insertions(+), 8 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index 8baf4b0ba223b..ac7d147b916fe 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -88,17 +88,17 @@ void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
 
 void LifetimeAnnotations::printState(raw_ostream &Out, ProgramStateRef State,
                                      const char *NL, const char *Sep) const {
-  auto LBVal = State->get<LifetimeBoundMap>();
-  auto LBValTwo = State->get<LifetimeBoundMapVal>();
+  auto LBMap = State->get<LifetimeBoundMap>();
+  auto LBMapVal = State->get<LifetimeBoundMapVal>();
 
-  if (LBVal.isEmpty() && LBValTwo.isEmpty())
+  if (LBMap.isEmpty() && LBMapVal.isEmpty())
     return;
 
   Out << Sep << "LifetimeBound bindings:" << NL;
-  for (auto&& [RetValSym, ArgValRegion] : LBVal) {
+  for (auto&& [RetValSym, ArgValRegion] : LBMap) {
     Out << " Origin " << RetValSym << " contains Loan " << ArgValRegion << NL;
   }
-  for (auto&& [RetVal, ArgValRegion]: LBValTwo) {
+  for (auto&& [RetVal, ArgValRegion]: LBMapVal) {
     Out << " Origin " << RetVal << " contains Loan " << ArgValRegion << NL;
   }
 }
diff --git a/clang/test/Analysis/lifetime-bound.cpp b/clang/test/Analysis/lifetime-bound.cpp
index 2acc1734644f2..5029e6589a6be 100644
--- a/clang/test/Analysis/lifetime-bound.cpp
+++ b/clang/test/Analysis/lifetime-bound.cpp
@@ -81,6 +81,20 @@ void caller_five() {
   clang_analyzer_dump(s);
 }
 
+// Free function with both annotated and non-annotated parameters.
+int& fn(int& f, int& s [[clang::lifetimebound]]) { return s; }
+
+void clang_analyzer_lifetime_bound(int&);
+
+void caller_six() {
+  int even = 50;
+  int odd = 55;
+  int& s = fn(even, odd);
+
+  clang_analyzer_lifetime_bound(s); // expected-warning {{Origin odd bound to odd}}
+  clang_analyzer_dump(s);
+}
+
 
 
 // These are the cases when the result of function calls are SymbolRefs.
@@ -90,7 +104,7 @@ int* foo(int* n [[clang::lifetimebound]]);
 
 void clang_analyzer_lifetime_bound(int*);
 
-void caller_six() {
+void caller_seven() {
   int y = 15;
   int* y_ptr = &y;
   auto* bind = foo(y_ptr);
@@ -114,7 +128,7 @@ int& func(int& some_number [[clang::lifetimebound]]);
 
 void clang_analyzer_lifetime_bound(int&);
 
-void caller_seven() {
+void caller_eight() {
   int f = 15;
   auto& bind = func(f);
 
@@ -131,7 +145,7 @@ int& f(int& a [[clang::lifetimebound]], int& b [[clang::lifetimebound]]);
 
 void clang_analyzer_lifetime_bound(int&);
 
-void caller_eight() {
+void caller_nine() {
   int first_num = 1;
   int second_num = 2;
   auto numbers = f(first_num, second_num);

>From 35d12f2af1e91d33cb4746cc58f1b7c9ad607d50 Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Fri, 5 Jun 2026 20:59:28 +0200
Subject: [PATCH 07/38] Finished TODOs for the week.

---
 .../Checkers/LifetimeAnnotations.cpp          |  1 -
 clang/test/Analysis/lifetime-bound.cpp        | 72 ++++++-------------
 2 files changed, 23 insertions(+), 50 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index ac7d147b916fe..12cbf6cfe253f 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -37,7 +37,6 @@ CallDescriptionMap<FnCheck> Callbacks = {
 
 void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
                                         CheckerContext &C) const {
-  llvm::errs() << "checkPostCall fired" << "\n";
   ProgramStateRef State = C.getState();
 
   const auto *FC = dyn_cast_if_present<AnyFunctionCall>(&Call);
diff --git a/clang/test/Analysis/lifetime-bound.cpp b/clang/test/Analysis/lifetime-bound.cpp
index 5029e6589a6be..230f6bd47e804 100644
--- a/clang/test/Analysis/lifetime-bound.cpp
+++ b/clang/test/Analysis/lifetime-bound.cpp
@@ -3,25 +3,25 @@
 // RUN: %clang_analyze_cc1 -analyzer-checker=alpha.cplusplus.LifetimeAnnotations \
 // RUN:   -analyzer-config c++-container-inlining=false -verify %s
 
-void clang_analyzer_dump(...);
+struct A {};
 
-// These are the cases when the result of function calls are MemRegions.
+void clang_analyzer_lifetime_bound(int*);
+void clang_analyzer_lifetime_bound(int&);
+void clang_analyzer_lifetime_bound(A*);
+void clang_analyzer_lifetime_bound(A&);
 
-struct A {};
+// These are the cases when the result of function calls are MemRegions.
 
 // Ref type parameter annotated case
 struct X {
   int& choose(int& a [[clang::lifetimebound]]) { return a; }
 };
 
-void clang_analyzer_lifetime_bound(int&);
-
 void caller() {
   int v = 0;
   X obj;
   int& r = obj.choose(v);
-  clang_analyzer_lifetime_bound(r); // expected-warning {{Origin v bound to v}}
-  clang_analyzer_dump(r);
+  clang_analyzer_lifetime_bound(r); // expected-warning {{bound to v}}
 }
 
 // Obj ref type function return annotated case
@@ -30,14 +30,11 @@ struct Y {
   A& getA() [[clang::lifetimebound]] { return a; }
 };
 
-void clang_analyzer_lifetime_bound(A& a);
-
 void caller_two() {
   // Return statement is annotated case.
   Y y;
   A& f = y.getA();
-  clang_analyzer_lifetime_bound(f); // expected-warning {{Origin y.a bound to y}}
-  clang_analyzer_dump(f);
+  clang_analyzer_lifetime_bound(f); // expected-warning {{bound to y}}
 }
 
 // Obj ptr type function return annotated case
@@ -46,53 +43,41 @@ struct Z {
   A* getA() [[clang::lifetimebound]] { return &a; }
 };
 
-void clang_analyzer_lifetime_bound(A* a);
-
 void caller_three() {
   Z z;
   A* func = z.getA();
-  clang_analyzer_lifetime_bound(func); // expected-warning {{Origin z.a bound to z}}
-  clang_analyzer_dump(func);
+  clang_analyzer_lifetime_bound(func); // expected-warning {{bound to z}}
 }
 
 // Free function with annotated param and ref return
 int& foo(int& num [[clang::lifetimebound]]) { return num; }
 
-void clang_analyzer_lifetime_bound(int&);
-
 void caller_four() {
   int num = 5;
   int& s = foo(num);
-  clang_analyzer_lifetime_bound(s); // expected-warning {{Origin num bound to num}}
-  clang_analyzer_dump(s);
+  clang_analyzer_lifetime_bound(s); // expected-warning {{bound to num}}
 }
 
 // Free function with annotated param and ptr return
 int* boo(int* num [[clang::lifetimebound]]) { return num; }
 
-void clang_analyzer_lifetime_bound(int*);
-
 void caller_five() {
   int n = 55;
   int* n_ptr = &n;
   int* s = boo(n_ptr);
 
-  clang_analyzer_lifetime_bound(s); // expected-warning {{Origin n bound to n}}
-  clang_analyzer_dump(s);
+  clang_analyzer_lifetime_bound(s); // expected-warning {{bound to n}}
 }
 
 // Free function with both annotated and non-annotated parameters.
 int& fn(int& f, int& s [[clang::lifetimebound]]) { return s; }
 
-void clang_analyzer_lifetime_bound(int&);
-
 void caller_six() {
   int even = 50;
   int odd = 55;
   int& s = fn(even, odd);
 
-  clang_analyzer_lifetime_bound(s); // expected-warning {{Origin odd bound to odd}}
-  clang_analyzer_dump(s);
+  clang_analyzer_lifetime_bound(s); // expected-warning {{bound to odd}}
 }
 
 
@@ -102,18 +87,13 @@ void caller_six() {
 // Function returns ptr and has an annotated parameter
 int* foo(int* n [[clang::lifetimebound]]);
 
-void clang_analyzer_lifetime_bound(int*);
-
 void caller_seven() {
   int y = 15;
   int* y_ptr = &y;
   auto* bind = foo(y_ptr);
 
-  clang_analyzer_lifetime_bound(bind);
-                                       // expected-warning at -1 {{Origin bound to y}}
-                                       // expected-warning at -1 {{Origin contains loan y}}
-  clang_analyzer_dump(bind);
-
+  clang_analyzer_lifetime_bound(bind); // expected-warning {{bound to y}}
+                                       // expected-warning at -1 {{contains loan y}}
 // FIXME: The full warning does look like this:
 // Origin SymRegion{conj_$5{int *, LC1, S847, #1}} bound to n
 // Origin conj_$5{int *, LC1, S847, #1} contains loan n
@@ -122,36 +102,30 @@ void caller_seven() {
 // This does not apply to the test cases above this test case.
 }
 
-
 // Function returns a reference and has an annotated parameter
 int& func(int& some_number [[clang::lifetimebound]]);
 
-void clang_analyzer_lifetime_bound(int&);
-
 void caller_eight() {
   int f = 15;
   auto& bind = func(f);
 
-  clang_analyzer_lifetime_bound(bind);
-                                       // expected-warning at -1 {{Origin bound to some_number}}
-                                       // expected-warning at -1 {{Origin contains loan some_number}}
-  clang_analyzer_dump(bind);
-
+  clang_analyzer_lifetime_bound(bind); // expected-warning {{bound to f}}
+                                       // expected-warning at -1 {{contains loan f}}
 // The FIXME about the full warning applies to this text case as well.
 }
 
 // Function returns a reference and has two annotated parameters.
 int& f(int& a [[clang::lifetimebound]], int& b [[clang::lifetimebound]]);
 
-void clang_analyzer_lifetime_bound(int&);
-
 void caller_nine() {
   int first_num = 1;
   int second_num = 2;
-  auto numbers = f(first_num, second_num);
+  int& numbers = f(first_num, second_num);
+
+  clang_analyzer_lifetime_bound(numbers); // expected-warning {{bound to first_num}}
+                                          // expected-warning at -1 {{contains loan first_num}}
 
-  clang_analyzer_lifetime_bound(numbers);
-                                          // expected-warning at -1 {{Origin bound to first_num}}
-                                          // expected-warning at -1 {{Origin contains loan first_num}}
-  clang_analyzer_dump(numbers);
+// FIXME: Currently the callback only iterates until the first annotated parameter which
+// means the second annotation never gets read here. That is a clear bug. It should be fixed
+// in order to analyze all the parameters which are annotated.
 }

>From 756f705ae9cc641da9d26dac00e65c6266b4b701 Mon Sep 17 00:00:00 2001
From: Benedek Kaibas <82393336+benedekaibas at users.noreply.github.com>
Date: Fri, 5 Jun 2026 21:03:21 +0200
Subject: [PATCH 08/38] Removed non-related fie.

---
 ...-AST-matching-to-get-containers-regi.patch | 130 ------------------
 1 file changed, 130 deletions(-)
 delete mode 100644 0001-analyzer-Removed-AST-matching-to-get-containers-regi.patch

diff --git a/0001-analyzer-Removed-AST-matching-to-get-containers-regi.patch b/0001-analyzer-Removed-AST-matching-to-get-containers-regi.patch
deleted file mode 100644
index 450bdda9f70dc..0000000000000
--- a/0001-analyzer-Removed-AST-matching-to-get-containers-regi.patch
+++ /dev/null
@@ -1,130 +0,0 @@
-From 00c5cf9e103a88ad9af285104f878c5f932ad8ff Mon Sep 17 00:00:00 2001
-From: benedekaibas <kaibas.benedek02 at gmail.com>
-Date: Tue, 2 Jun 2026 13:13:46 +0200
-Subject: [PATCH] [analyzer] Removed AST matching to get containers' region and
- fixed small issues.
-
----
- .../StaticAnalyzer/Checkers/MoveChecker.cpp   | 40 +++++++++++--------
- .../test/Analysis/use-after-move-iterator.cpp | 24 +++++------
- 2 files changed, 36 insertions(+), 28 deletions(-)
-
-diff --git a/clang/lib/StaticAnalyzer/Checkers/MoveChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/MoveChecker.cpp
-index 47e9c585054a..9ff2e90b618a 100644
---- a/clang/lib/StaticAnalyzer/Checkers/MoveChecker.cpp
-+++ b/clang/lib/StaticAnalyzer/Checkers/MoveChecker.cpp
-@@ -518,31 +518,29 @@ bool MoveChecker::evalCall(const CallEvent &Call, CheckerContext &C) const {
-   if (!StdMoveCall.matches(Call))
-     return false;
- 
--  const auto *BeginCall =
--      dyn_cast<CXXMemberCallExpr>(CE->getArg(0)->IgnoreImpCasts());
--  if (!BeginCall)
-+  const auto *POS = getIteratorPosition(State, Call.getArgSVal(0));
-+  if (!POS)
-     return false;
- 
--  const Expr *ContainerExpr = BeginCall->getImplicitObjectArgument();
--  const auto *DRE = dyn_cast<DeclRefExpr>(ContainerExpr->IgnoreImpCasts());
--  if (!DRE)
-+  const MemRegion *ContainerRegion = POS->getContainer();
-+  if (!ContainerRegion)
-     return false;
- 
--  const auto *VD = dyn_cast<VarDecl>(DRE->getDecl());
--  if (!VD)
-+  const auto *TypedRegion =
-+      dyn_cast_if_present<TypedValueRegion>(ContainerRegion);
-+  if (!TypedRegion)
-     return false;
- 
--  const MemRegion *Region =
--      State->getLValue(VD, C.getLocationContext()).getAsRegion();
--  if (!Region)
--    return false;
-+  QualType ObjTy = TypedRegion->getValueType();
- 
--  const CXXRecordDecl *RD = ContainerExpr->getType()->getAsCXXRecordDecl();
-+  const auto *RD = ObjTy->getAsCXXRecordDecl();
-   if (!RD)
-     return false;
- 
--  ObjectKind OK = classifyObject(State, Region, RD);
-+  ObjectKind OK = classifyObject(State, ContainerRegion, RD);
- 
-+  // FIXME: Also apply getIteratorPosition from IteratorModeling to recover the
-+  // destination region instead of doing AST pattern matching.
-   const auto *BackInsCall = dyn_cast<CallExpr>(CE->getArg(2)->IgnoreImpCasts());
-   if (!BackInsCall)
-     return false;
-@@ -573,7 +571,8 @@ bool MoveChecker::evalCall(const CallEvent &Call, CheckerContext &C) const {
-                                    /*CausesPointerEscape=*/false);
- 
-   if (shouldBeTracked(OK))
--    State = State->set<TrackedContentsMap>(Region, RegionState::getMoved());
-+    State = State->set<TrackedContentsMap>(ContainerRegion,
-+                                           RegionState::getMoved());
- 
-   C.addTransition(State);
-   return true;
-@@ -736,10 +735,19 @@ void MoveChecker::checkPreCall(const CallEvent &Call, CheckerContext &C) const {
- 
-     if (const auto *POS = getIteratorPosition(State, Val)) {
-       const MemRegion *ContainerRegion = POS->getContainer();
-+      if (!ContainerRegion)
-+        return;
-+
-+      const auto *TypedRegion =
-+          dyn_cast_if_present<TypedValueRegion>(ContainerRegion);
-+      if (!TypedRegion)
-+        return;
- 
--      const auto *TypedRegion = cast<TypedValueRegion>(ContainerRegion);
-       QualType ObjTy = TypedRegion->getValueType();
-       const auto *R = ObjTy->getAsCXXRecordDecl();
-+      if (!R)
-+        return;
-+
-       if (State->get<TrackedContentsMap>(ContainerRegion)) {
-         ExplodedNode *N = tryToReportBug(ContainerRegion, R, C, MK_FunCall);
-         if (!N || N->isSink())
-diff --git a/clang/test/Analysis/use-after-move-iterator.cpp b/clang/test/Analysis/use-after-move-iterator.cpp
-index 50dd7e57b42e..2357be3a6bb3 100644
---- a/clang/test/Analysis/use-after-move-iterator.cpp
-+++ b/clang/test/Analysis/use-after-move-iterator.cpp
-@@ -10,20 +10,20 @@
- // IteratorModeling is enabled.
- //===----------------------------------------------------------------------===//
- 
--void iteratorDerefSource() {
-+std::string iteratorDeref(int rng) {
-   std::list<std::string> l1;
-   l1.push_back("l1");
-   std::list<std::string> l2;
- 
--  std::move(l1.begin(), l1.end(), std::back_inserter(l2));
--  *l1.cbegin(); // expected-warning {{Method called on moved-from object 'l1'}}
--}
--
--void iteratorDerefDest() {
--  std::list<std::string> l1;
--  l1.push_back("l1");
--  std::list<std::string> l2;
--
--  std::move(l1.begin(), l1.end(), std::back_inserter(l2));
--  *l2.cbegin(); // no-warning
-+  switch (rng) {
-+    case 10: {
-+      std::move(l1.begin(), l1.end(), std::back_inserter(l2));
-+      return *l1.cbegin(); // expected-warning {{Method called on moved-from object 'l1'}}
-+    }
-+    case 20: {
-+      std::move(l1.begin(), l1.end(), std::back_inserter(l2));
-+      return *l2.cbegin(); // no-warning: only l1 was invalidated and not l2!
-+    }
-+  }
-+  return 0;
- }
--- 
-2.43.0
-

>From 927a7401e439f3e5a6370e2c7ac5d9b6522dceb0 Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Mon, 8 Jun 2026 12:31:13 +0200
Subject: [PATCH 09/38] Addressed mentors' issues.

---
 .../Checkers/LifetimeAnnotations.cpp           | 14 ++++++--------
 clang/test/Analysis/lifetime-bound.cpp         | 18 ++++--------------
 2 files changed, 10 insertions(+), 22 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index 12cbf6cfe253f..01dbb0c115bc3 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -10,7 +10,6 @@
 
 using namespace clang;
 using namespace ento;
-using namespace clang::lifetimes;
 
 REGISTER_MAP_WITH_PROGRAMSTATE(LifetimeBoundMap, SymbolRef,
                                const MemRegion *);
@@ -29,7 +28,7 @@ class LifetimeAnnotations : public Checker<check::PostCall, eval::Call> {
 };
 
 typedef void (LifetimeAnnotations::*FnCheck)(const CallEvent &Call, const CallExpr *,
-                                              CheckerContext &) const;
+                                              CheckerContext &C) const;
 CallDescriptionMap<FnCheck> Callbacks = {
   {{CDM::SimpleFunc, {"clang_analyzer_lifetime_bound"}},
     &LifetimeAnnotations::analyzerLifetimeBound},
@@ -39,7 +38,7 @@ void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
                                         CheckerContext &C) const {
   ProgramStateRef State = C.getState();
 
-  const auto *FC = dyn_cast_if_present<AnyFunctionCall>(&Call);
+  const auto *FC = dyn_cast<AnyFunctionCall>(&Call);
   if (!FC)
     return;
 
@@ -67,17 +66,17 @@ void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
     if (const MemRegion *ArgValRegion = ArgVal.getAsRegion()) {
       if (RetValSym)
         State = State->set<LifetimeBoundMap>(RetValSym, ArgValRegion);
-      if (const MemRegion *RetValRegion = RetVal.getAsRegion())
+      else if (const MemRegion *RetValRegion = RetVal.getAsRegion())
         State = State->set<LifetimeBoundMapVal>(RetValRegion, ArgValRegion);
     }
   }
 
   if (const auto *IC = dyn_cast<CXXInstanceCall>(&Call)) {
-    if (implicitObjectParamIsLifetimeBound(FD)) {
+    if (clang::lifetimes::implicitObjectParamIsLifetimeBound(FD)) {
       if (const MemRegion *AttrRegion = IC->getCXXThisVal().getAsRegion()) {
         if (RetValSym)
           State = State->set<LifetimeBoundMap>(RetValSym, AttrRegion);
-        if (const MemRegion *RetValRegion = RetVal.getAsRegion())
+        else if (const MemRegion *RetValRegion = RetVal.getAsRegion())
           State = State->set<LifetimeBoundMapVal>(RetValRegion, AttrRegion);
       }
     }
@@ -104,7 +103,7 @@ void LifetimeAnnotations::printState(raw_ostream &Out, ProgramStateRef State,
 
 bool LifetimeAnnotations::evalCall(const CallEvent &Call, CheckerContext &C) const {
 
-  const auto *CE = llvm::dyn_cast_if_present<CallExpr>(Call.getOriginExpr());
+  const auto *CE = dyn_cast_if_present<CallExpr>(Call.getOriginExpr());
   if (!CE)
     return false;
 
@@ -114,7 +113,6 @@ bool LifetimeAnnotations::evalCall(const CallEvent &Call, CheckerContext &C) con
 
   (this->*(*Handler))(Call, CE, C);
   return true;
-  C.addTransition(C.getState());
 }
 
 void LifetimeAnnotations::analyzerLifetimeBound(const CallEvent &Call, const CallExpr *CE, CheckerContext &C) const {
diff --git a/clang/test/Analysis/lifetime-bound.cpp b/clang/test/Analysis/lifetime-bound.cpp
index 230f6bd47e804..1d9b4eabcee04 100644
--- a/clang/test/Analysis/lifetime-bound.cpp
+++ b/clang/test/Analysis/lifetime-bound.cpp
@@ -77,7 +77,7 @@ void caller_six() {
   int odd = 55;
   int& s = fn(even, odd);
 
-  clang_analyzer_lifetime_bound(s); // expected-warning {{bound to odd}}
+  clang_analyzer_lifetime_bound(s); // expected-warning {{Origin odd bound to odd}}
 }
 
 
@@ -92,14 +92,7 @@ void caller_seven() {
   int* y_ptr = &y;
   auto* bind = foo(y_ptr);
 
-  clang_analyzer_lifetime_bound(bind); // expected-warning {{bound to y}}
-                                       // expected-warning at -1 {{contains loan y}}
-// FIXME: The full warning does look like this:
-// Origin SymRegion{conj_$5{int *, LC1, S847, #1}} bound to n
-// Origin conj_$5{int *, LC1, S847, #1} contains loan n
-// Since the conj sym number and the ID can change across runs I have decided to just include
-// string parts of the error message since that is the only consistent part of the emitted report.
-// This does not apply to the test cases above this test case.
+  clang_analyzer_lifetime_bound(bind); // expected-warning {{contains loan y}}
 }
 
 // Function returns a reference and has an annotated parameter
@@ -109,9 +102,7 @@ void caller_eight() {
   int f = 15;
   auto& bind = func(f);
 
-  clang_analyzer_lifetime_bound(bind); // expected-warning {{bound to f}}
-                                       // expected-warning at -1 {{contains loan f}}
-// The FIXME about the full warning applies to this text case as well.
+  clang_analyzer_lifetime_bound(bind); // expected-warning {{contains loan f}}
 }
 
 // Function returns a reference and has two annotated parameters.
@@ -122,8 +113,7 @@ void caller_nine() {
   int second_num = 2;
   int& numbers = f(first_num, second_num);
 
-  clang_analyzer_lifetime_bound(numbers); // expected-warning {{bound to first_num}}
-                                          // expected-warning at -1 {{contains loan first_num}}
+  clang_analyzer_lifetime_bound(numbers); // expected-warning {{contains loan first_num}}
 
 // FIXME: Currently the callback only iterates until the first annotated parameter which
 // means the second annotation never gets read here. That is a clear bug. It should be fixed

>From d3b9a6c1d46d515e5a69b9abb85167328c10443f Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Mon, 8 Jun 2026 12:55:20 +0200
Subject: [PATCH 10/38] Currently LazyCoumpoundVal is not present in any of the
 maps.

---
 clang/test/Analysis/lifetime-bound.cpp | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/clang/test/Analysis/lifetime-bound.cpp b/clang/test/Analysis/lifetime-bound.cpp
index 1d9b4eabcee04..1e5afa9159bd4 100644
--- a/clang/test/Analysis/lifetime-bound.cpp
+++ b/clang/test/Analysis/lifetime-bound.cpp
@@ -119,3 +119,17 @@ void caller_nine() {
 // means the second annotation never gets read here. That is a clear bug. It should be fixed
 // in order to analyze all the parameters which are annotated.
 }
+
+struct View {
+  int* p;
+};
+View makeView(int& x [[clang::lifetimebound]]);
+
+void clang_analyzer_lifetime_bound(View);
+
+void caller_view() {
+  int v = 42;
+  View w = makeView(v);
+  // FIXME: Currently none of the maps cover LazyCompoundVal
+  clang_analyzer_lifetime_bound(w); // no-warning
+}

>From a286dd32765c4bfe9e357e472ccc6f23226ed72a Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Tue, 9 Jun 2026 14:13:05 +0200
Subject: [PATCH 11/38] The checker correctly reads multiple annotated
 parameters.

---
 .../Checkers/LifetimeAnnotations.cpp          | 117 +++++++++++-------
 clang/test/Analysis/lifetime-bound.cpp        |  22 ++--
 2 files changed, 81 insertions(+), 58 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index 01dbb0c115bc3..28f67208c338c 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -1,39 +1,63 @@
+#include "clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h"
 #include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h"
 #include "clang/StaticAnalyzer/Core/Checker.h"
 #include "clang/StaticAnalyzer/Core/PathSensitive/CallDescription.h"
 #include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h"
 #include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"
 #include "llvm/Support/raw_ostream.h"
-#include "AllocationState.h"
-#include "clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h"
-
 
 using namespace clang;
 using namespace ento;
 
-REGISTER_MAP_WITH_PROGRAMSTATE(LifetimeBoundMap, SymbolRef,
-                               const MemRegion *);
-REGISTER_MAP_WITH_PROGRAMSTATE(LifetimeBoundMapVal, const MemRegion *, const MemRegion *);
+struct LifetimeMap {
+  SymbolRef SymRefType;
+  const MemRegion *MemRegType;
+
+  bool operator==(const LifetimeMap &Type) const {
+    return std::tie(SymRefType, MemRegType) ==
+           std::tie(Type.SymRefType, Type.MemRegType);
+  }
+
+  bool operator<(const LifetimeMap &Type) const {
+    return std::tie(SymRefType, MemRegType) <
+           std::tie(Type.SymRefType, Type.MemRegType);
+  }
+
+  void Profile(llvm::FoldingSetNodeID &ID) const {
+    ID.AddPointer(SymRefType);
+    ID.AddPointer(MemRegType);
+  }
+};
+
+REGISTER_SET_WITH_PROGRAMSTATE(LifetimeBoundSet, LifetimeMap)
 
+REGISTER_MAP_WITH_PROGRAMSTATE(LifetimeBoundMapVal, const MemRegion *,
+                               const MemRegion *)
 
+namespace {
 class LifetimeAnnotations : public Checker<check::PostCall, eval::Call> {
 public:
   void checkPostCall(const CallEvent &Call, CheckerContext &C) const;
   void printState(raw_ostream &Out, ProgramStateRef State, const char *NL,
                   const char *Sep) const override;
   bool evalCall(const CallEvent &Call, CheckerContext &C) const;
-  void analyzerLifetimeBound(const CallEvent &Call, const CallExpr *, CheckerContext &C) const;
+  void analyzerLifetimeBound(const CallEvent &Call, const CallExpr *,
+                             CheckerContext &C) const;
 
   const BugType BugMsg{this, "LifetimeAnnotations", "LifetimeBound"};
-};
 
-typedef void (LifetimeAnnotations::*FnCheck)(const CallEvent &Call, const CallExpr *,
-                                              CheckerContext &C) const;
-CallDescriptionMap<FnCheck> Callbacks = {
-  {{CDM::SimpleFunc, {"clang_analyzer_lifetime_bound"}},
-    &LifetimeAnnotations::analyzerLifetimeBound},
+  using FnCheck = void (LifetimeAnnotations::*)(const CallEvent &Call,
+                                                const CallExpr *,
+                                                CheckerContext &C) const;
+
+  const CallDescriptionMap<FnCheck> Callbacks = {
+      {{CDM::SimpleFunc, {"clang_analyzer_lifetime_bound"}},
+       &LifetimeAnnotations::analyzerLifetimeBound},
+  };
 };
 
+} // namespace
+
 void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
                                         CheckerContext &C) const {
   ProgramStateRef State = C.getState();
@@ -48,26 +72,19 @@ void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
 
   SVal RetVal = Call.getReturnValue();
   SymbolRef RetValSym = RetVal.getAsSymbol(/*IncludeBaseRegions=*/true);
-  unsigned LBParamIdx = FD->getNumParams();
-  // FIXME: Use range based for loop instead. Currently that would require
-  // to also change how we create ArgVal which would need a new logic to
-  // be implemented.
-  for (unsigned I = 0, E = FD->getNumParams(); I != E; I++) {
-    if (FD->getParamDecl(I)->hasAttr<LifetimeBoundAttr>()) {
-      LBParamIdx = I;
-      // FIXME: If multiple parameters are annotated this logic would
-      // prevent the analyzer to read after the first parameter.
-      break;
-    }
-  }
 
-  if (LBParamIdx != FD->getNumParams()) {
-    SVal ArgVal = Call.getArgSVal(LBParamIdx);
-    if (const MemRegion *ArgValRegion = ArgVal.getAsRegion()) {
-      if (RetValSym)
-        State = State->set<LifetimeBoundMap>(RetValSym, ArgValRegion);
-      else if (const MemRegion *RetValRegion = RetVal.getAsRegion())
-        State = State->set<LifetimeBoundMapVal>(RetValRegion, ArgValRegion);
+  for (const ParmVarDecl *PVD : FD->parameters()) {
+    if (PVD->hasAttr<LifetimeBoundAttr>()) {
+      unsigned Idx = PVD->getFunctionScopeIndex();
+      SVal Arg = Call.getArgSVal(Idx);
+
+      if (const MemRegion *ArgValRegion = Arg.getAsRegion()) {
+        if (RetValSym)
+          State = State->add<LifetimeBoundSet>(
+              LifetimeMap{RetValSym, ArgValRegion});
+        else if (const MemRegion *RetValRegion = RetVal.getAsRegion())
+          State = State->set<LifetimeBoundMapVal>(RetValRegion, ArgValRegion);
+      }
     }
   }
 
@@ -75,7 +92,8 @@ void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
     if (clang::lifetimes::implicitObjectParamIsLifetimeBound(FD)) {
       if (const MemRegion *AttrRegion = IC->getCXXThisVal().getAsRegion()) {
         if (RetValSym)
-          State = State->set<LifetimeBoundMap>(RetValSym, AttrRegion);
+          State =
+              State->add<LifetimeBoundSet>(LifetimeMap{RetValSym, AttrRegion});
         else if (const MemRegion *RetValRegion = RetVal.getAsRegion())
           State = State->set<LifetimeBoundMapVal>(RetValRegion, AttrRegion);
       }
@@ -86,22 +104,23 @@ void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
 
 void LifetimeAnnotations::printState(raw_ostream &Out, ProgramStateRef State,
                                      const char *NL, const char *Sep) const {
-  auto LBMap = State->get<LifetimeBoundMap>();
+  auto LBMap = State->get<LifetimeBoundSet>();
   auto LBMapVal = State->get<LifetimeBoundMapVal>();
 
   if (LBMap.isEmpty() && LBMapVal.isEmpty())
     return;
 
   Out << Sep << "LifetimeBound bindings:" << NL;
-  for (auto&& [RetValSym, ArgValRegion] : LBMap) {
+  for (auto &&[RetValSym, ArgValRegion] : LBMap) {
     Out << " Origin " << RetValSym << " contains Loan " << ArgValRegion << NL;
   }
-  for (auto&& [RetVal, ArgValRegion]: LBMapVal) {
+  for (auto &&[RetVal, ArgValRegion] : LBMapVal) {
     Out << " Origin " << RetVal << " contains Loan " << ArgValRegion << NL;
   }
 }
 
-bool LifetimeAnnotations::evalCall(const CallEvent &Call, CheckerContext &C) const {
+bool LifetimeAnnotations::evalCall(const CallEvent &Call,
+                                   CheckerContext &C) const {
 
   const auto *CE = dyn_cast_if_present<CallExpr>(Call.getOriginExpr());
   if (!CE)
@@ -115,11 +134,13 @@ bool LifetimeAnnotations::evalCall(const CallEvent &Call, CheckerContext &C) con
   return true;
 }
 
-void LifetimeAnnotations::analyzerLifetimeBound(const CallEvent &Call, const CallExpr *CE, CheckerContext &C) const {
+void LifetimeAnnotations::analyzerLifetimeBound(const CallEvent &Call,
+                                                const CallExpr *CE,
+                                                CheckerContext &C) const {
 
   ProgramStateRef State = C.getState();
-  unsigned int ArgExpr = CE->getNumArgs();
-  if (ArgExpr != 1)
+  unsigned int ArgCount = CE->getNumArgs();
+  if (ArgCount != 1)
     return;
 
   SVal ArgSVal = Call.getArgSVal(0);
@@ -134,16 +155,20 @@ void LifetimeAnnotations::analyzerLifetimeBound(const CallEvent &Call, const Cal
     return;
 
   if (ArgSValSym) {
-    if (const auto *ArgValLookFor = State->get<LifetimeBoundMap>(ArgSValSym)) {
-      OS << " Origin " << ArgSValSym << " contains loan " << *ArgValLookFor;
-      auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, OS.str(), N);
-      C.emitReport(std::move(BR));
-      Str.clear();
+    auto LBSet = State->get<LifetimeBoundSet>();
+    for (const LifetimeMap &Entry : LBSet) {
+      if (Entry.SymRefType == ArgSValSym) {
+        OS << " Origin " << ArgSValSym << " bound to " << Entry.MemRegType;
+        auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, OS.str(), N);
+        C.emitReport(std::move(BR));
+        Str.clear();
+      }
     }
   }
 
   if (ArgValRegion) {
-    if (const auto *AttrValLookFor = State->get<LifetimeBoundMapVal>(ArgValRegion)) {
+    if (const auto *AttrValLookFor =
+            State->get<LifetimeBoundMapVal>(ArgValRegion)) {
       OS << " Origin " << ArgValRegion << " bound to " << *AttrValLookFor;
       auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, OS.str(), N);
       C.emitReport(std::move(BR));
diff --git a/clang/test/Analysis/lifetime-bound.cpp b/clang/test/Analysis/lifetime-bound.cpp
index 1e5afa9159bd4..f9389c88854e9 100644
--- a/clang/test/Analysis/lifetime-bound.cpp
+++ b/clang/test/Analysis/lifetime-bound.cpp
@@ -21,7 +21,7 @@ void caller() {
   int v = 0;
   X obj;
   int& r = obj.choose(v);
-  clang_analyzer_lifetime_bound(r); // expected-warning {{bound to v}}
+  clang_analyzer_lifetime_bound(r); // expected-warning {{Origin v bound to v}}
 }
 
 // Obj ref type function return annotated case
@@ -34,7 +34,7 @@ void caller_two() {
   // Return statement is annotated case.
   Y y;
   A& f = y.getA();
-  clang_analyzer_lifetime_bound(f); // expected-warning {{bound to y}}
+  clang_analyzer_lifetime_bound(f); // expected-warning {{Origin y.a bound to y}}
 }
 
 // Obj ptr type function return annotated case
@@ -46,7 +46,7 @@ struct Z {
 void caller_three() {
   Z z;
   A* func = z.getA();
-  clang_analyzer_lifetime_bound(func); // expected-warning {{bound to z}}
+  clang_analyzer_lifetime_bound(func); // expected-warning {{Origin z.a bound to z}}
 }
 
 // Free function with annotated param and ref return
@@ -55,7 +55,7 @@ int& foo(int& num [[clang::lifetimebound]]) { return num; }
 void caller_four() {
   int num = 5;
   int& s = foo(num);
-  clang_analyzer_lifetime_bound(s); // expected-warning {{bound to num}}
+  clang_analyzer_lifetime_bound(s); // expected-warning {{Origin num bound to num}}
 }
 
 // Free function with annotated param and ptr return
@@ -66,7 +66,7 @@ void caller_five() {
   int* n_ptr = &n;
   int* s = boo(n_ptr);
 
-  clang_analyzer_lifetime_bound(s); // expected-warning {{bound to n}}
+  clang_analyzer_lifetime_bound(s); // expected-warning {{Origin n bound to n}}
 }
 
 // Free function with both annotated and non-annotated parameters.
@@ -92,7 +92,7 @@ void caller_seven() {
   int* y_ptr = &y;
   auto* bind = foo(y_ptr);
 
-  clang_analyzer_lifetime_bound(bind); // expected-warning {{contains loan y}}
+  clang_analyzer_lifetime_bound(bind); // expected-warning-re {{Origin conj_${{[0-9]+}}{int *, LC{{[0-9]+}}, S{{[0-9]+}}, #{{[0-9]+}}} bound to y}}
 }
 
 // Function returns a reference and has an annotated parameter
@@ -102,7 +102,7 @@ void caller_eight() {
   int f = 15;
   auto& bind = func(f);
 
-  clang_analyzer_lifetime_bound(bind); // expected-warning {{contains loan f}}
+  clang_analyzer_lifetime_bound(bind); // expected-warning-re {{Origin conj_${{[0-9]+}}{int &, LC{{[0-9]+}}, S{{[0-9]+}}, #{{[0-9]+}}} bound to f}}
 }
 
 // Function returns a reference and has two annotated parameters.
@@ -113,11 +113,9 @@ void caller_nine() {
   int second_num = 2;
   int& numbers = f(first_num, second_num);
 
-  clang_analyzer_lifetime_bound(numbers); // expected-warning {{contains loan first_num}}
-
-// FIXME: Currently the callback only iterates until the first annotated parameter which
-// means the second annotation never gets read here. That is a clear bug. It should be fixed
-// in order to analyze all the parameters which are annotated.
+  clang_analyzer_lifetime_bound(numbers);
+  // expected-warning-re at -1 {{Origin conj_${{[0-9]+}}{int &, LC{{[0-9]+}}, S{{[0-9]+}}, #{{[0-9]+}}} bound to first_num}}
+  // expected-warning-re at -2 {{Origin conj_${{[0-9]+}}{int &, LC{{[0-9]+}}, S{{[0-9]+}}, #{{[0-9]+}}} bound to second_num}}
 }
 
 struct View {

>From 9a41d92a1dc6a002281bcdb99d97fe0618d4fec9 Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Wed, 10 Jun 2026 12:06:23 +0200
Subject: [PATCH 12/38] [analyzer] Cleaned up the code to format correctly.

---
 .../Checkers/LifetimeAnnotations.cpp          | 29 ++++++++++---------
 1 file changed, 16 insertions(+), 13 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index 28f67208c338c..e8deb2e0d6f55 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -43,6 +43,8 @@ class LifetimeAnnotations : public Checker<check::PostCall, eval::Call> {
   bool evalCall(const CallEvent &Call, CheckerContext &C) const;
   void analyzerLifetimeBound(const CallEvent &Call, const CallExpr *,
                              CheckerContext &C) const;
+  ProgramStateRef bindValues(ProgramStateRef State, SymbolRef RetValSym, SVal RetVal, const MemRegion *Source) const;
+
 
   const BugType BugMsg{this, "LifetimeAnnotations", "LifetimeBound"};
 
@@ -58,6 +60,17 @@ class LifetimeAnnotations : public Checker<check::PostCall, eval::Call> {
 
 } // namespace
 
+ProgramStateRef LifetimeAnnotations::bindValues(ProgramStateRef State, SymbolRef RetValSym, SVal RetVal, const MemRegion *Source) const {
+
+  if (Source) {
+    if (RetValSym)
+      State = State->add<LifetimeBoundSet>(LifetimeMap{RetValSym, Source});
+    else if (const MemRegion *RetValRegion = RetVal.getAsRegion())
+      State = State->set<LifetimeBoundMapVal>(RetValRegion, Source);
+  }
+  return State;
+}
+
 void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
                                         CheckerContext &C) const {
   ProgramStateRef State = C.getState();
@@ -77,25 +90,15 @@ void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
     if (PVD->hasAttr<LifetimeBoundAttr>()) {
       unsigned Idx = PVD->getFunctionScopeIndex();
       SVal Arg = Call.getArgSVal(Idx);
-
-      if (const MemRegion *ArgValRegion = Arg.getAsRegion()) {
-        if (RetValSym)
-          State = State->add<LifetimeBoundSet>(
-              LifetimeMap{RetValSym, ArgValRegion});
-        else if (const MemRegion *RetValRegion = RetVal.getAsRegion())
-          State = State->set<LifetimeBoundMapVal>(RetValRegion, ArgValRegion);
+      if (const MemRegion *ArgValRegion = Arg.getAsRegion())
+        State = bindValues(State, RetValSym, RetVal, ArgValRegion);
       }
     }
-  }
 
   if (const auto *IC = dyn_cast<CXXInstanceCall>(&Call)) {
     if (clang::lifetimes::implicitObjectParamIsLifetimeBound(FD)) {
       if (const MemRegion *AttrRegion = IC->getCXXThisVal().getAsRegion()) {
-        if (RetValSym)
-          State =
-              State->add<LifetimeBoundSet>(LifetimeMap{RetValSym, AttrRegion});
-        else if (const MemRegion *RetValRegion = RetVal.getAsRegion())
-          State = State->set<LifetimeBoundMapVal>(RetValRegion, AttrRegion);
+        State = bindValues(State, RetValSym, RetVal, AttrRegion);
       }
     }
   }

>From 8fc9ed72f56ff6732bd53381f9bc7012d5a16d16 Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Wed, 10 Jun 2026 18:50:35 +0200
Subject: [PATCH 13/38] [analyzer] Rewrote maps and binding.

---
 .../Checkers/LifetimeAnnotations.cpp          | 52 +++++++++++--------
 1 file changed, 31 insertions(+), 21 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index e8deb2e0d6f55..408d869d14e7b 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -29,10 +29,10 @@ struct LifetimeMap {
   }
 };
 
-REGISTER_SET_WITH_PROGRAMSTATE(LifetimeBoundSet, LifetimeMap)
+REGISTER_SET_FACTORY_WITH_PROGRAMSTATE(LifetimeSourceSet, const MemRegion *)
+REGISTER_MAP_WITH_PROGRAMSTATE(LifetimeBoundMap, SymbolRef, LifetimeSourceSet)
 
-REGISTER_MAP_WITH_PROGRAMSTATE(LifetimeBoundMapVal, const MemRegion *,
-                               const MemRegion *)
+REGISTER_MAP_WITH_PROGRAMSTATE(LifetimeBoundMapVal, const MemRegion *, LifetimeSourceSet)
 
 namespace {
 class LifetimeAnnotations : public Checker<check::PostCall, eval::Call> {
@@ -61,16 +61,24 @@ class LifetimeAnnotations : public Checker<check::PostCall, eval::Call> {
 } // namespace
 
 ProgramStateRef LifetimeAnnotations::bindValues(ProgramStateRef State, SymbolRef RetValSym, SVal RetVal, const MemRegion *Source) const {
+  LifetimeSourceSet::Factory &F = State->getStateManager().get_context<LifetimeSourceSet>();
 
-  if (Source) {
-    if (RetValSym)
-      State = State->add<LifetimeBoundSet>(LifetimeMap{RetValSym, Source});
-    else if (const MemRegion *RetValRegion = RetVal.getAsRegion())
-      State = State->set<LifetimeBoundMapVal>(RetValRegion, Source);
+  if (RetValSym) {
+    const LifetimeSourceSet *LBSet = State->get<LifetimeBoundMap>(RetValSym);
+    LifetimeSourceSet Set = LBSet ? *LBSet : F.getEmptySet();
+    Set = F.add(Set, Source);
+    State = State->set<LifetimeBoundMap>(RetValSym, Set);
+  }
+  else if (const MemRegion *RetValRegion = RetVal.getAsRegion()) {
+    const LifetimeSourceSet *LBValSet = State->get<LifetimeBoundMapVal>(RetValRegion);
+    LifetimeSourceSet Set = LBValSet ? *LBValSet : F.getEmptySet();
+    Set = F.add(Set, Source);
+    State = State->set<LifetimeBoundMapVal>(RetValRegion, Set);
   }
   return State;
 }
 
+
 void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
                                         CheckerContext &C) const {
   ProgramStateRef State = C.getState();
@@ -107,7 +115,7 @@ void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
 
 void LifetimeAnnotations::printState(raw_ostream &Out, ProgramStateRef State,
                                      const char *NL, const char *Sep) const {
-  auto LBMap = State->get<LifetimeBoundSet>();
+  auto LBMap = State->get<LifetimeBoundMap>();
   auto LBMapVal = State->get<LifetimeBoundMapVal>();
 
   if (LBMap.isEmpty() && LBMapVal.isEmpty())
@@ -115,10 +123,12 @@ void LifetimeAnnotations::printState(raw_ostream &Out, ProgramStateRef State,
 
   Out << Sep << "LifetimeBound bindings:" << NL;
   for (auto &&[RetValSym, ArgValRegion] : LBMap) {
-    Out << " Origin " << RetValSym << " contains Loan " << ArgValRegion << NL;
+    for (const auto *Region : ArgValRegion)
+      Out << " Origin " << RetValSym << " contains Loan " << Region << NL;
   }
   for (auto &&[RetVal, ArgValRegion] : LBMapVal) {
-    Out << " Origin " << RetVal << " contains Loan " << ArgValRegion << NL;
+    for (const auto *Region : ArgValRegion)
+      Out << " Origin " << RetVal << " contains Loan " << Region << NL;
   }
 }
 
@@ -158,10 +168,9 @@ void LifetimeAnnotations::analyzerLifetimeBound(const CallEvent &Call,
     return;
 
   if (ArgSValSym) {
-    auto LBSet = State->get<LifetimeBoundSet>();
-    for (const LifetimeMap &Entry : LBSet) {
-      if (Entry.SymRefType == ArgSValSym) {
-        OS << " Origin " << ArgSValSym << " bound to " << Entry.MemRegType;
+    if (auto *SourceSet = State->get<LifetimeBoundMap>(ArgSValSym)) {
+      for (const auto *Region : *SourceSet) {
+        OS << " Origin " << ArgSValSym << " bound to " << Region;
         auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, OS.str(), N);
         C.emitReport(std::move(BR));
         Str.clear();
@@ -170,12 +179,13 @@ void LifetimeAnnotations::analyzerLifetimeBound(const CallEvent &Call,
   }
 
   if (ArgValRegion) {
-    if (const auto *AttrValLookFor =
-            State->get<LifetimeBoundMapVal>(ArgValRegion)) {
-      OS << " Origin " << ArgValRegion << " bound to " << *AttrValLookFor;
-      auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, OS.str(), N);
-      C.emitReport(std::move(BR));
-      Str.clear();
+    if (auto *SourceSet = State->get<LifetimeBoundMapVal>(ArgValRegion)) {
+      for (const auto *Region : *SourceSet) {
+        OS << " Origin " << ArgValRegion << " bound to " << Region;
+        auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, OS.str(), N);
+        C.emitReport(std::move(BR));
+        Str.clear();
+      }
     }
   }
 }

>From af61426727214c2f5865c602b995cd3204e086ab Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Wed, 10 Jun 2026 18:59:09 +0200
Subject: [PATCH 14/38] [analyzer] Removed dead code and applied more accurate
 naming for binding function.

---
 .../Checkers/LifetimeAnnotations.cpp          | 32 ++++---------------
 1 file changed, 6 insertions(+), 26 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index 408d869d14e7b..fa80922f5a6f1 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -9,26 +9,6 @@
 using namespace clang;
 using namespace ento;
 
-struct LifetimeMap {
-  SymbolRef SymRefType;
-  const MemRegion *MemRegType;
-
-  bool operator==(const LifetimeMap &Type) const {
-    return std::tie(SymRefType, MemRegType) ==
-           std::tie(Type.SymRefType, Type.MemRegType);
-  }
-
-  bool operator<(const LifetimeMap &Type) const {
-    return std::tie(SymRefType, MemRegType) <
-           std::tie(Type.SymRefType, Type.MemRegType);
-  }
-
-  void Profile(llvm::FoldingSetNodeID &ID) const {
-    ID.AddPointer(SymRefType);
-    ID.AddPointer(MemRegType);
-  }
-};
-
 REGISTER_SET_FACTORY_WITH_PROGRAMSTATE(LifetimeSourceSet, const MemRegion *)
 REGISTER_MAP_WITH_PROGRAMSTATE(LifetimeBoundMap, SymbolRef, LifetimeSourceSet)
 
@@ -122,13 +102,13 @@ void LifetimeAnnotations::printState(raw_ostream &Out, ProgramStateRef State,
     return;
 
   Out << Sep << "LifetimeBound bindings:" << NL;
-  for (auto &&[RetValSym, ArgValRegion] : LBMap) {
-    for (const auto *Region : ArgValRegion)
-      Out << " Origin " << RetValSym << " contains Loan " << Region << NL;
+  for (auto &&[OriginSym, SourceSet] : LBMap) {
+    for (const auto *Region : SourceSet)
+      Out << " Origin " << OriginSym << " contains Loan " << Region << NL;
   }
-  for (auto &&[RetVal, ArgValRegion] : LBMapVal) {
-    for (const auto *Region : ArgValRegion)
-      Out << " Origin " << RetVal << " contains Loan " << Region << NL;
+  for (auto &&[OriginRegion, SourceSet] : LBMapVal) {
+    for (const auto *Region : SourceSet)
+      Out << " Origin " << OriginRegion << " contains Loan " << Region << NL;
   }
 }
 

>From 8dccd0b3a9c4c96cccc809d42e9361c578dad9fa Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Thu, 11 Jun 2026 12:11:27 +0200
Subject: [PATCH 15/38] [analyzer] Started implementing checking and marking
 dead symbols.

---
 clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index fa80922f5a6f1..11c53a1bfa3e7 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -24,6 +24,7 @@ class LifetimeAnnotations : public Checker<check::PostCall, eval::Call> {
   void analyzerLifetimeBound(const CallEvent &Call, const CallExpr *,
                              CheckerContext &C) const;
   ProgramStateRef bindValues(ProgramStateRef State, SymbolRef RetValSym, SVal RetVal, const MemRegion *Source) const;
+  void checkDeadSymbols(SymbolReaper &SymReaper, CheckerContext &C) const;
 
 
   const BugType BugMsg{this, "LifetimeAnnotations", "LifetimeBound"};
@@ -93,6 +94,12 @@ void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
   C.addTransition(State);
 }
 
+void LifetimeAnnotations::checkDeadSymbols(SymbolReaper &SymReaper, CheckerContext &C) const {
+  ProgramStateRef State = C.getState();
+
+  LifetimeBoundMapTy TrackedRegion = State->get<LifetimeBoundMap>();
+}
+
 void LifetimeAnnotations::printState(raw_ostream &Out, ProgramStateRef State,
                                      const char *NL, const char *Sep) const {
   auto LBMap = State->get<LifetimeBoundMap>();

>From 672ad56fcac843b17dfd27cc560bf14af7ddc476 Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Thu, 11 Jun 2026 18:18:07 +0200
Subject: [PATCH 16/38] [analyzer] Implemented dangling borrower detection.

---
 .../Checkers/LifetimeAnnotations.cpp           | 18 +++++++++++++-----
 1 file changed, 13 insertions(+), 5 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index 11c53a1bfa3e7..65dbb51440992 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -24,8 +24,7 @@ class LifetimeAnnotations : public Checker<check::PostCall, eval::Call> {
   void analyzerLifetimeBound(const CallEvent &Call, const CallExpr *,
                              CheckerContext &C) const;
   ProgramStateRef bindValues(ProgramStateRef State, SymbolRef RetValSym, SVal RetVal, const MemRegion *Source) const;
-  void checkDeadSymbols(SymbolReaper &SymReaper, CheckerContext &C) const;
-
+  bool isSourceDangle(const MemRegion *Source, ProgramStateRef State, CheckerContext &C) const;
 
   const BugType BugMsg{this, "LifetimeAnnotations", "LifetimeBound"};
 
@@ -94,10 +93,19 @@ void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
   C.addTransition(State);
 }
 
-void LifetimeAnnotations::checkDeadSymbols(SymbolReaper &SymReaper, CheckerContext &C) const {
-  ProgramStateRef State = C.getState();
+bool LifetimeAnnotations::isSourceDangle(const MemRegion *Source, ProgramStateRef State, CheckerContext &C) const {
+  // Q1: Am I sure I need ProgramStateRef State as a parameter?
+
+  if (const auto *StackSpace = Source->getMemorySpaceAs<StackSpaceRegion>(State)) {
+    const StackFrame *SF = StackSpace->getStackFrame();
+    const StackFrame *CurrentSF = C.getStackFrame();
+    if (SF == CurrentSF || SF->isParentOf(CurrentSF))
+      return false;
+    return false;
+  }
 
-  LifetimeBoundMapTy TrackedRegion = State->get<LifetimeBoundMap>();
+  // Currently return false, but this has to be replaced when the source is a SymRegion instead of a MemRegion
+  return true;
 }
 
 void LifetimeAnnotations::printState(raw_ostream &Out, ProgramStateRef State,

>From b018123507ea31f237d5693ea4f022639e967cec Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Thu, 11 Jun 2026 18:29:48 +0200
Subject: [PATCH 17/38] [analyzer] Start implementing the checkEndFunction.

---
 clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index 65dbb51440992..d0ea7dccfbaf9 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -15,7 +15,7 @@ REGISTER_MAP_WITH_PROGRAMSTATE(LifetimeBoundMap, SymbolRef, LifetimeSourceSet)
 REGISTER_MAP_WITH_PROGRAMSTATE(LifetimeBoundMapVal, const MemRegion *, LifetimeSourceSet)
 
 namespace {
-class LifetimeAnnotations : public Checker<check::PostCall, eval::Call> {
+class LifetimeAnnotations : public Checker<check::PostCall, check::EndFunction, eval::Call> {
 public:
   void checkPostCall(const CallEvent &Call, CheckerContext &C) const;
   void printState(raw_ostream &Out, ProgramStateRef State, const char *NL,
@@ -25,6 +25,7 @@ class LifetimeAnnotations : public Checker<check::PostCall, eval::Call> {
                              CheckerContext &C) const;
   ProgramStateRef bindValues(ProgramStateRef State, SymbolRef RetValSym, SVal RetVal, const MemRegion *Source) const;
   bool isSourceDangle(const MemRegion *Source, ProgramStateRef State, CheckerContext &C) const;
+  void checkEndFunction(const ReturnStmt *RS, CheckerContext &C) const;
 
   const BugType BugMsg{this, "LifetimeAnnotations", "LifetimeBound"};
 

>From f156db2c0769710a14ed55cc51951dba35ff9a4c Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Fri, 12 Jun 2026 11:58:31 +0200
Subject: [PATCH 18/38] [analyzer] Dangling source detection works for
 MemRegions.

---
 .../Checkers/LifetimeAnnotations.cpp               | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index d0ea7dccfbaf9..b18a90598213e 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -109,6 +109,20 @@ bool LifetimeAnnotations::isSourceDangle(const MemRegion *Source, ProgramStateRe
   return true;
 }
 
+void LifetimeAnnotations::checkEndFunction(const ReturnStmt *RS, CheckerContext &C) const {
+  ProgramStateRef State = C.getState();
+  auto LBMapVal = State->get<LifetimeBoundMapVal>();
+  if (LBMapVal.isEmpty())
+    return;
+
+  for (auto&& [OriginRegion, SourceSet] : LBMapVal) {
+    for (const auto *Region : SourceSet) {
+      if (isSourceDangle(Region, State, C) == true)
+        return;
+    }
+  }
+}
+
 void LifetimeAnnotations::printState(raw_ostream &Out, ProgramStateRef State,
                                      const char *NL, const char *Sep) const {
   auto LBMap = State->get<LifetimeBoundMap>();

>From a65e419de9d71173632e46a1e5b46115475bae2b Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Fri, 12 Jun 2026 12:55:38 +0200
Subject: [PATCH 19/38] [analyzer] Debug prints for debugging checkEndFunction.

---
 .../Checkers/LifetimeAnnotations.cpp          | 37 +++++++++++++++----
 1 file changed, 30 insertions(+), 7 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index b18a90598213e..8d4b7b0f0b2aa 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -95,30 +95,53 @@ void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
 }
 
 bool LifetimeAnnotations::isSourceDangle(const MemRegion *Source, ProgramStateRef State, CheckerContext &C) const {
-  // Q1: Am I sure I need ProgramStateRef State as a parameter?
-
+  llvm::errs() << "isSourceDangle called";
   if (const auto *StackSpace = Source->getMemorySpaceAs<StackSpaceRegion>(State)) {
+    llvm::errs() << "isSourceDanle non null";
     const StackFrame *SF = StackSpace->getStackFrame();
     const StackFrame *CurrentSF = C.getStackFrame();
-    if (SF == CurrentSF || SF->isParentOf(CurrentSF))
+    if (SF == CurrentSF || SF->isParentOf(CurrentSF)) {
+      llvm::errs() << "isSourceDangle about to fire";
       return false;
+    }
     return false;
   }
 
-  // Currently return false, but this has to be replaced when the source is a SymRegion instead of a MemRegion
+  // Currently return true, but this has to be replaced when the source is a SymRegion instead of a MemRegion
   return true;
 }
 
 void LifetimeAnnotations::checkEndFunction(const ReturnStmt *RS, CheckerContext &C) const {
   ProgramStateRef State = C.getState();
+  auto LBMap = State->get<LifetimeBoundMap>();
   auto LBMapVal = State->get<LifetimeBoundMapVal>();
-  if (LBMapVal.isEmpty())
+
+  llvm::SmallString<128> Str;
+  llvm::raw_svector_ostream OS(Str);
+  ExplodedNode *N = C.generateErrorNode();
+
+  if (LBMapVal.isEmpty() && LBMapVal.isEmpty())
     return;
 
+  for (auto&& [OriginSym, SourceSet] : LBMapVal) {
+    for (const auto *Region : SourceSet) {
+      if (isSourceDangle(Region, State, C)) {
+        OS << " Returning value bound to a local " << Region << " that will go out of scope.";
+        auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, OS.str(), N);
+        C.emitReport(std::move(BR));
+        Str.clear();
+      }
+    }
+  }
+
   for (auto&& [OriginRegion, SourceSet] : LBMapVal) {
     for (const auto *Region : SourceSet) {
-      if (isSourceDangle(Region, State, C) == true)
-        return;
+      if (isSourceDangle(Region, State, C)) {
+        OS << " Returning value bound to a local " << Region << " that will go out of scope.";
+        auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, OS.str(), N);
+        C.emitReport(std::move(BR));
+        Str.clear();
+      }
     }
   }
 }

>From c4643ef6071ea1c3593267ff8807d34aa31081ed Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Fri, 12 Jun 2026 13:31:58 +0200
Subject: [PATCH 20/38] [analyzer] Removed duplicate call on the
 LifetimeBoundMap.

---
 .../Checkers/LifetimeAnnotations.cpp          | 23 +++++++++++++------
 1 file changed, 16 insertions(+), 7 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index 8d4b7b0f0b2aa..12d9b676589fb 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -62,6 +62,7 @@ ProgramStateRef LifetimeAnnotations::bindValues(ProgramStateRef State, SymbolRef
 
 void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
                                         CheckerContext &C) const {
+  llvm::errs() << "checkPostCall called" << "\n";
   ProgramStateRef State = C.getState();
 
   const auto *FC = dyn_cast<AnyFunctionCall>(&Call);
@@ -95,23 +96,23 @@ void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
 }
 
 bool LifetimeAnnotations::isSourceDangle(const MemRegion *Source, ProgramStateRef State, CheckerContext &C) const {
-  llvm::errs() << "isSourceDangle called";
+  llvm::errs() << "isSourceDangle called" << "\n";
   if (const auto *StackSpace = Source->getMemorySpaceAs<StackSpaceRegion>(State)) {
-    llvm::errs() << "isSourceDanle non null";
+    llvm::errs() << "isSourceDangle non null" << "\n";
     const StackFrame *SF = StackSpace->getStackFrame();
     const StackFrame *CurrentSF = C.getStackFrame();
     if (SF == CurrentSF || SF->isParentOf(CurrentSF)) {
-      llvm::errs() << "isSourceDangle about to fire";
+      llvm::errs() << "isSourceDangle about to fire" << "\n";
       return false;
     }
     return false;
   }
-
   // Currently return true, but this has to be replaced when the source is a SymRegion instead of a MemRegion
   return true;
 }
 
 void LifetimeAnnotations::checkEndFunction(const ReturnStmt *RS, CheckerContext &C) const {
+  llvm::errs() << "checkEndFunction called." << "\n";
   ProgramStateRef State = C.getState();
   auto LBMap = State->get<LifetimeBoundMap>();
   auto LBMapVal = State->get<LifetimeBoundMapVal>();
@@ -119,11 +120,14 @@ void LifetimeAnnotations::checkEndFunction(const ReturnStmt *RS, CheckerContext
   llvm::SmallString<128> Str;
   llvm::raw_svector_ostream OS(Str);
   ExplodedNode *N = C.generateErrorNode();
-
-  if (LBMapVal.isEmpty() && LBMapVal.isEmpty())
+  llvm::errs() << "maps are not empty before check" << "\n";
+  if (LBMap.isEmpty() && LBMapVal.isEmpty())
     return;
+  
+  llvm::errs() << "maps are not empty after null check" << "\n";
 
-  for (auto&& [OriginSym, SourceSet] : LBMapVal) {
+  for (auto&& [OriginSym, SourceSet] : LBMap) {
+    llvm::errs() << "LBMapVal isEmpty: " << LBMap.isEmpty() << "\n";
     for (const auto *Region : SourceSet) {
       if (isSourceDangle(Region, State, C)) {
         OS << " Returning value bound to a local " << Region << " that will go out of scope.";
@@ -131,10 +135,13 @@ void LifetimeAnnotations::checkEndFunction(const ReturnStmt *RS, CheckerContext
         C.emitReport(std::move(BR));
         Str.clear();
       }
+      llvm::errs() << "Checking source: " << Region << " isDangling: "
+             << isSourceDangle(Region, State, C) << "\n";
     }
   }
 
   for (auto&& [OriginRegion, SourceSet] : LBMapVal) {
+    llvm::errs() << "LBMapVal isEmpty: " << LBMapVal.isEmpty() << "\n";
     for (const auto *Region : SourceSet) {
       if (isSourceDangle(Region, State, C)) {
         OS << " Returning value bound to a local " << Region << " that will go out of scope.";
@@ -142,6 +149,8 @@ void LifetimeAnnotations::checkEndFunction(const ReturnStmt *RS, CheckerContext
         C.emitReport(std::move(BR));
         Str.clear();
       }
+      llvm::errs() << "Checking source: " << Region << " isDangling: "
+             << isSourceDangle(Region, State, C) << "\n";
     }
   }
 }

>From d443b306328b7dfe24dd05a4dc271da608843833 Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Fri, 12 Jun 2026 20:54:33 +0200
Subject: [PATCH 21/38] [analyzer] Resolved teh type mismatch in looking up the
 maps.

---
 .../Checkers/LifetimeAnnotations.cpp          | 58 ++++++++++---------
 1 file changed, 32 insertions(+), 26 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index 12d9b676589fb..4543f84e07dc8 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -96,38 +96,43 @@ void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
 }
 
 bool LifetimeAnnotations::isSourceDangle(const MemRegion *Source, ProgramStateRef State, CheckerContext &C) const {
-  llvm::errs() << "isSourceDangle called" << "\n";
+  // This check works for checkEndFunction
   if (const auto *StackSpace = Source->getMemorySpaceAs<StackSpaceRegion>(State)) {
-    llvm::errs() << "isSourceDangle non null" << "\n";
     const StackFrame *SF = StackSpace->getStackFrame();
     const StackFrame *CurrentSF = C.getStackFrame();
-    if (SF == CurrentSF || SF->isParentOf(CurrentSF)) {
-      llvm::errs() << "isSourceDangle about to fire" << "\n";
-      return false;
-    }
-    return false;
+    if (SF == CurrentSF)
+      return true;
   }
-  // Currently return true, but this has to be replaced when the source is a SymRegion instead of a MemRegion
-  return true;
+  // Currently return false, but this has to be replaced when the source is a SymRegion instead of a MemRegion
+  return false;
 }
 
 void LifetimeAnnotations::checkEndFunction(const ReturnStmt *RS, CheckerContext &C) const {
-  llvm::errs() << "checkEndFunction called." << "\n";
   ProgramStateRef State = C.getState();
   auto LBMap = State->get<LifetimeBoundMap>();
   auto LBMapVal = State->get<LifetimeBoundMapVal>();
+  
+  const Expr *RetExpr = RS->getRetValue();
+  if (!RetExpr)
+    return;
+
+  RetExpr = RetExpr->IgnoreParens();
+  SVal RetVal = C.getSVal(RetExpr);
+  const MemRegion *RetValRegion = RetVal.getAsRegion();
+  if (!RetValRegion)
+    return;
 
   llvm::SmallString<128> Str;
   llvm::raw_svector_ostream OS(Str);
-  ExplodedNode *N = C.generateErrorNode();
-  llvm::errs() << "maps are not empty before check" << "\n";
+
   if (LBMap.isEmpty() && LBMapVal.isEmpty())
-    return;
-  
-  llvm::errs() << "maps are not empty after null check" << "\n";
+    return; 
 
   for (auto&& [OriginSym, SourceSet] : LBMap) {
-    llvm::errs() << "LBMapVal isEmpty: " << LBMap.isEmpty() << "\n";
+    ExplodedNode *N = C.generateNonFatalErrorNode();
+    if (!N)
+      return;
+
     for (const auto *Region : SourceSet) {
       if (isSourceDangle(Region, State, C)) {
         OS << " Returning value bound to a local " << Region << " that will go out of scope.";
@@ -135,22 +140,23 @@ void LifetimeAnnotations::checkEndFunction(const ReturnStmt *RS, CheckerContext
         C.emitReport(std::move(BR));
         Str.clear();
       }
-      llvm::errs() << "Checking source: " << Region << " isDangling: "
-             << isSourceDangle(Region, State, C) << "\n";
     }
   }
 
   for (auto&& [OriginRegion, SourceSet] : LBMapVal) {
-    llvm::errs() << "LBMapVal isEmpty: " << LBMapVal.isEmpty() << "\n";
+    ExplodedNode *N = C.generateNonFatalErrorNode();
+    if (!N)
+      return;
+
     for (const auto *Region : SourceSet) {
-      if (isSourceDangle(Region, State, C)) {
-        OS << " Returning value bound to a local " << Region << " that will go out of scope.";
-        auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, OS.str(), N);
-        C.emitReport(std::move(BR));
-        Str.clear();
+      if (OriginRegion == RetValRegion) {
+        if (isSourceDangle(Region, State, C)) {
+          OS << " Returning value bound to a local " << Region << " that will go out of scope.";
+          auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, OS.str(), N);
+          C.emitReport(std::move(BR));
+          Str.clear();
+        }
       }
-      llvm::errs() << "Checking source: " << Region << " isDangling: "
-             << isSourceDangle(Region, State, C) << "\n";
     }
   }
 }

>From 073b313f3706b0c4d253a2f8e949cf57452bc528 Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Fri, 12 Jun 2026 22:02:46 +0200
Subject: [PATCH 22/38] [analyzer] Dangling detection works, but the code is
 ugly, format it into helper functions before pushing to the PR.

---
 .../Checkers/LifetimeAnnotations.cpp          | 60 +++++++++++--------
 1 file changed, 34 insertions(+), 26 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index 4543f84e07dc8..c9ba3e15d5745 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -62,7 +62,6 @@ ProgramStateRef LifetimeAnnotations::bindValues(ProgramStateRef State, SymbolRef
 
 void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
                                         CheckerContext &C) const {
-  llvm::errs() << "checkPostCall called" << "\n";
   ProgramStateRef State = C.getState();
 
   const auto *FC = dyn_cast<AnyFunctionCall>(&Call);
@@ -108,6 +107,9 @@ bool LifetimeAnnotations::isSourceDangle(const MemRegion *Source, ProgramStateRe
 }
 
 void LifetimeAnnotations::checkEndFunction(const ReturnStmt *RS, CheckerContext &C) const {
+  if (!RS)
+    return;
+
   ProgramStateRef State = C.getState();
   auto LBMap = State->get<LifetimeBoundMap>();
   auto LBMapVal = State->get<LifetimeBoundMapVal>();
@@ -118,9 +120,6 @@ void LifetimeAnnotations::checkEndFunction(const ReturnStmt *RS, CheckerContext
 
   RetExpr = RetExpr->IgnoreParens();
   SVal RetVal = C.getSVal(RetExpr);
-  const MemRegion *RetValRegion = RetVal.getAsRegion();
-  if (!RetValRegion)
-    return;
 
   llvm::SmallString<128> Str;
   llvm::raw_svector_ostream OS(Str);
@@ -128,33 +127,42 @@ void LifetimeAnnotations::checkEndFunction(const ReturnStmt *RS, CheckerContext
   if (LBMap.isEmpty() && LBMapVal.isEmpty())
     return; 
 
-  for (auto&& [OriginSym, SourceSet] : LBMap) {
-    ExplodedNode *N = C.generateNonFatalErrorNode();
-    if (!N)
-      return;
+  SymbolRef RetValSym = RetVal.getAsSymbol(/*IncludeBaseRegions=*/true);
+  const MemRegion *RetValRegion = RetVal.getAsRegion();
+  if (!RetValSym && !RetValRegion)
+    return;
 
-    for (const auto *Region : SourceSet) {
-      if (isSourceDangle(Region, State, C)) {
-        OS << " Returning value bound to a local " << Region << " that will go out of scope.";
-        auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, OS.str(), N);
-        C.emitReport(std::move(BR));
-        Str.clear();
+  if (RetValSym) {
+    for (auto&& [OriginSym, SourceSet] : LBMap) {
+      for (const auto *Region : SourceSet) {
+        if (OriginSym == RetValSym) {
+          if (isSourceDangle(Region, State, C)) {
+            ExplodedNode *N = C.generateNonFatalErrorNode();
+            if (!N)
+              return;
+            OS << " Returning value bound to a local " << Region << " that will go out of scope.";
+            auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, OS.str(), N);
+            C.emitReport(std::move(BR));
+            Str.clear();
+          }
+        }
       }
     }
   }
 
-  for (auto&& [OriginRegion, SourceSet] : LBMapVal) {
-    ExplodedNode *N = C.generateNonFatalErrorNode();
-    if (!N)
-      return;
-
-    for (const auto *Region : SourceSet) {
-      if (OriginRegion == RetValRegion) {
-        if (isSourceDangle(Region, State, C)) {
-          OS << " Returning value bound to a local " << Region << " that will go out of scope.";
-          auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, OS.str(), N);
-          C.emitReport(std::move(BR));
-          Str.clear();
+  if (RetValRegion) {
+    for (auto&& [OriginRegion, SourceSet] : LBMapVal) {
+      for (const auto *Region : SourceSet) {
+        if (OriginRegion == RetValRegion) {
+          if (isSourceDangle(Region, State, C)) {
+            ExplodedNode *N = C.generateNonFatalErrorNode();
+            if (!N)
+              return;
+            OS << " Returning value bound to a local " << Region << " that will go out of scope.";
+            auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, OS.str(), N);
+              C.emitReport(std::move(BR));
+              Str.clear();
+          }
         }
       }
     }

>From 394e136e30de2fe4acc2e0e149a74f128f7eafe6 Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Sat, 13 Jun 2026 01:47:01 +0200
Subject: [PATCH 23/38] [analyzer] Core implementation of the checkEndFunction
 done.

---
 .../Checkers/LifetimeAnnotations.cpp          | 130 ++++++++++--------
 1 file changed, 74 insertions(+), 56 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index c9ba3e15d5745..edc9bb2253287 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -12,10 +12,12 @@ using namespace ento;
 REGISTER_SET_FACTORY_WITH_PROGRAMSTATE(LifetimeSourceSet, const MemRegion *)
 REGISTER_MAP_WITH_PROGRAMSTATE(LifetimeBoundMap, SymbolRef, LifetimeSourceSet)
 
-REGISTER_MAP_WITH_PROGRAMSTATE(LifetimeBoundMapVal, const MemRegion *, LifetimeSourceSet)
+REGISTER_MAP_WITH_PROGRAMSTATE(LifetimeBoundMapVal, const MemRegion *,
+                               LifetimeSourceSet)
 
 namespace {
-class LifetimeAnnotations : public Checker<check::PostCall, check::EndFunction, eval::Call> {
+class LifetimeAnnotations
+    : public Checker<check::PostCall, check::EndFunction, eval::Call> {
 public:
   void checkPostCall(const CallEvent &Call, CheckerContext &C) const;
   void printState(raw_ostream &Out, ProgramStateRef State, const char *NL,
@@ -23,8 +25,17 @@ class LifetimeAnnotations : public Checker<check::PostCall, check::EndFunction,
   bool evalCall(const CallEvent &Call, CheckerContext &C) const;
   void analyzerLifetimeBound(const CallEvent &Call, const CallExpr *,
                              CheckerContext &C) const;
-  ProgramStateRef bindValues(ProgramStateRef State, SymbolRef RetValSym, SVal RetVal, const MemRegion *Source) const;
-  bool isSourceDangle(const MemRegion *Source, ProgramStateRef State, CheckerContext &C) const;
+  ProgramStateRef bindValues(ProgramStateRef State, SymbolRef RetValSym,
+                             SVal RetVal, const MemRegion *Source) const;
+  bool isSourceDangle(const MemRegion *Source, ProgramStateRef State,
+                      CheckerContext &C) const;
+  void reportDanglingSource(const MemRegion *Region, ExplodedNode *N,
+                            CheckerContext &C) const;
+
+  template <typename MapTy, typename KeyTy>
+  void checkReturnedBorrower(const MapTy &Map, const KeyTy RetKey,
+                             ProgramStateRef State, ExplodedNode *N,
+                             CheckerContext &C) const;
   void checkEndFunction(const ReturnStmt *RS, CheckerContext &C) const;
 
   const BugType BugMsg{this, "LifetimeAnnotations", "LifetimeBound"};
@@ -41,17 +52,21 @@ class LifetimeAnnotations : public Checker<check::PostCall, check::EndFunction,
 
 } // namespace
 
-ProgramStateRef LifetimeAnnotations::bindValues(ProgramStateRef State, SymbolRef RetValSym, SVal RetVal, const MemRegion *Source) const {
-  LifetimeSourceSet::Factory &F = State->getStateManager().get_context<LifetimeSourceSet>();
+ProgramStateRef LifetimeAnnotations::bindValues(ProgramStateRef State,
+                                                SymbolRef RetValSym,
+                                                SVal RetVal,
+                                                const MemRegion *Source) const {
+  LifetimeSourceSet::Factory &F =
+      State->getStateManager().get_context<LifetimeSourceSet>();
 
   if (RetValSym) {
     const LifetimeSourceSet *LBSet = State->get<LifetimeBoundMap>(RetValSym);
     LifetimeSourceSet Set = LBSet ? *LBSet : F.getEmptySet();
     Set = F.add(Set, Source);
     State = State->set<LifetimeBoundMap>(RetValSym, Set);
-  }
-  else if (const MemRegion *RetValRegion = RetVal.getAsRegion()) {
-    const LifetimeSourceSet *LBValSet = State->get<LifetimeBoundMapVal>(RetValRegion);
+  } else if (const MemRegion *RetValRegion = RetVal.getAsRegion()) {
+    const LifetimeSourceSet *LBValSet =
+        State->get<LifetimeBoundMapVal>(RetValRegion);
     LifetimeSourceSet Set = LBValSet ? *LBValSet : F.getEmptySet();
     Set = F.add(Set, Source);
     State = State->set<LifetimeBoundMapVal>(RetValRegion, Set);
@@ -59,7 +74,6 @@ ProgramStateRef LifetimeAnnotations::bindValues(ProgramStateRef State, SymbolRef
   return State;
 }
 
-
 void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
                                         CheckerContext &C) const {
   ProgramStateRef State = C.getState();
@@ -81,8 +95,8 @@ void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
       SVal Arg = Call.getArgSVal(Idx);
       if (const MemRegion *ArgValRegion = Arg.getAsRegion())
         State = bindValues(State, RetValSym, RetVal, ArgValRegion);
-      }
     }
+  }
 
   if (const auto *IC = dyn_cast<CXXInstanceCall>(&Call)) {
     if (clang::lifetimes::implicitObjectParamIsLifetimeBound(FD)) {
@@ -94,26 +108,47 @@ void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
   C.addTransition(State);
 }
 
-bool LifetimeAnnotations::isSourceDangle(const MemRegion *Source, ProgramStateRef State, CheckerContext &C) const {
-  // This check works for checkEndFunction
-  if (const auto *StackSpace = Source->getMemorySpaceAs<StackSpaceRegion>(State)) {
+bool LifetimeAnnotations::isSourceDangle(const MemRegion *Source,
+                                         ProgramStateRef State,
+                                         CheckerContext &C) const {
+  // FIXME: Currently the checker only focuses on stack MemRegions only since
+  // that is the scope of week 3. Sources without a stack region are not
+  // covered, but should be implemented as well next step.
+  if (const auto *StackSpace =
+          Source->getMemorySpaceAs<StackSpaceRegion>(State)) {
     const StackFrame *SF = StackSpace->getStackFrame();
     const StackFrame *CurrentSF = C.getStackFrame();
-    if (SF == CurrentSF)
+    if (SF == CurrentSF || !SF->isParentOf(CurrentSF))
       return true;
   }
-  // Currently return false, but this has to be replaced when the source is a SymRegion instead of a MemRegion
   return false;
 }
 
-void LifetimeAnnotations::checkEndFunction(const ReturnStmt *RS, CheckerContext &C) const {
+template <typename MapTy, typename KeyTy>
+void LifetimeAnnotations::checkReturnedBorrower(const MapTy &Map,
+                                                const KeyTy RetKey,
+                                                ProgramStateRef State,
+                                                ExplodedNode *N,
+                                                CheckerContext &C) const {
+  for (auto &&[Origin, SourceSet] : Map) {
+    if (Origin == RetKey) {
+      for (const MemRegion *Region : SourceSet) {
+        if (isSourceDangle(Region, State, C))
+          reportDanglingSource(Region, N, C);
+      }
+    }
+  }
+}
+
+void LifetimeAnnotations::checkEndFunction(const ReturnStmt *RS,
+                                           CheckerContext &C) const {
   if (!RS)
     return;
 
   ProgramStateRef State = C.getState();
   auto LBMap = State->get<LifetimeBoundMap>();
   auto LBMapVal = State->get<LifetimeBoundMapVal>();
-  
+
   const Expr *RetExpr = RS->getRetValue();
   if (!RetExpr)
     return;
@@ -121,52 +156,35 @@ void LifetimeAnnotations::checkEndFunction(const ReturnStmt *RS, CheckerContext
   RetExpr = RetExpr->IgnoreParens();
   SVal RetVal = C.getSVal(RetExpr);
 
-  llvm::SmallString<128> Str;
-  llvm::raw_svector_ostream OS(Str);
-
   if (LBMap.isEmpty() && LBMapVal.isEmpty())
-    return; 
+    return;
 
   SymbolRef RetValSym = RetVal.getAsSymbol(/*IncludeBaseRegions=*/true);
   const MemRegion *RetValRegion = RetVal.getAsRegion();
   if (!RetValSym && !RetValRegion)
     return;
 
-  if (RetValSym) {
-    for (auto&& [OriginSym, SourceSet] : LBMap) {
-      for (const auto *Region : SourceSet) {
-        if (OriginSym == RetValSym) {
-          if (isSourceDangle(Region, State, C)) {
-            ExplodedNode *N = C.generateNonFatalErrorNode();
-            if (!N)
-              return;
-            OS << " Returning value bound to a local " << Region << " that will go out of scope.";
-            auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, OS.str(), N);
-            C.emitReport(std::move(BR));
-            Str.clear();
-          }
-        }
-      }
-    }
-  }
+  ExplodedNode *N = C.generateNonFatalErrorNode();
+  if (!N)
+    return;
 
-  if (RetValRegion) {
-    for (auto&& [OriginRegion, SourceSet] : LBMapVal) {
-      for (const auto *Region : SourceSet) {
-        if (OriginRegion == RetValRegion) {
-          if (isSourceDangle(Region, State, C)) {
-            ExplodedNode *N = C.generateNonFatalErrorNode();
-            if (!N)
-              return;
-            OS << " Returning value bound to a local " << Region << " that will go out of scope.";
-            auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, OS.str(), N);
-              C.emitReport(std::move(BR));
-              Str.clear();
-          }
-        }
-      }
-    }
-  }
+  if (RetValSym)
+    checkReturnedBorrower(LBMap, RetValSym, State, N, C);
+
+  if (RetValRegion)
+    checkReturnedBorrower(LBMapVal, RetValRegion, State, N, C);
+}
+
+void LifetimeAnnotations::reportDanglingSource(const MemRegion *Region,
+                                               ExplodedNode *N,
+                                               CheckerContext &C) const {
+  llvm::SmallString<128> Str;
+  llvm::raw_svector_ostream OS(Str);
+
+  OS << " Returning value bound to a local " << Region
+     << " that will go out of scope";
+  auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, OS.str(), N);
+  C.emitReport(std::move(BR));
 }
 
 void LifetimeAnnotations::printState(raw_ostream &Out, ProgramStateRef State,

>From 7393b3c3e9bd741fa4fca11a85e25bd3f0733f7a Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Mon, 15 Jun 2026 18:26:35 +0200
Subject: [PATCH 24/38] [analyzer] Implemented checkDeadSymbols to clean up the
 program state.

---
 .../Checkers/LifetimeAnnotations.cpp          | 31 +++++++++++++++++--
 1 file changed, 29 insertions(+), 2 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index edc9bb2253287..ae73823595633 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -37,6 +37,7 @@ class LifetimeAnnotations
                              ProgramStateRef State, ExplodedNode *N,
                              CheckerContext &C) const;
   void checkEndFunction(const ReturnStmt *RS, CheckerContext &C) const;
+  void checkDeadSymbols(SymbolReaper &SymReaper, CheckerContext &C) const;
 
   const BugType BugMsg{this, "LifetimeAnnotations", "LifetimeBound"};
 
@@ -181,12 +182,38 @@ void LifetimeAnnotations::reportDanglingSource(const MemRegion *Region,
   llvm::SmallString<128> Str;
   llvm::raw_svector_ostream OS(Str);
 
-  OS << " Returning value bound to a local " << Region
-     << " that will go out of scope";
+  OS << "Returning value bound to a local " << Region
+     << "that will go out of scope";
   auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, OS.str(), N);
   C.emitReport(std::move(BR));
 }
 
+void LifetimeAnnotations::checkDeadSymbols(SymbolReaper &SymReaper, CheckerContext &C) const {
+  ProgramStateRef State = C.getState();
+
+  // Get both maps since both of them needs to be cleaned up
+  LifetimeBoundMapTy LBMap = State->get<LifetimeBoundMap>();
+  LifetimeBoundMapValTy LBMapVal = State->get<LifetimeBoundMapVal>();
+
+
+  for (auto &Entry : LBMap) {
+    const SymExpr *Sym = Entry.first;
+    bool IsSymbolLive = SymReaper.isLive(Sym);
+
+    if (!IsSymbolLive)
+      State = State->remove<LifetimeBoundMap>(Sym);
+  }
+
+  for(auto &Entry: LBMapVal) {
+    const MemRegion *Region = Entry.first;
+    bool IsSymbolLive = SymReaper.isLiveRegion(Region);
+
+    if (!IsSymbolLive)
+      State = State->remove<LifetimeBoundMapVal>(Region);
+  }
+  C.addTransition(State);
+}
+
 void LifetimeAnnotations::printState(raw_ostream &Out, ProgramStateRef State,
                                      const char *NL, const char *Sep) const {
   auto LBMap = State->get<LifetimeBoundMap>();

>From 9ac12179119b835d3b5a8795c66ab6bc97394c96 Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Mon, 15 Jun 2026 19:07:04 +0200
Subject: [PATCH 25/38] [analyzer] Implemented checkDeadSymbols.

---
 .../Checkers/LifetimeAnnotations.cpp          | 38 +++++++++----------
 1 file changed, 19 insertions(+), 19 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index ae73823595633..6a6c228e72d69 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -23,8 +23,7 @@ class LifetimeAnnotations
   void printState(raw_ostream &Out, ProgramStateRef State, const char *NL,
                   const char *Sep) const override;
   bool evalCall(const CallEvent &Call, CheckerContext &C) const;
-  void analyzerLifetimeBound(const CallEvent &Call, const CallExpr *,
-                             CheckerContext &C) const;
+  void analyzerLifetimeBound(const CallEvent &Call, CheckerContext &C) const;
   ProgramStateRef bindValues(ProgramStateRef State, SymbolRef RetValSym,
                              SVal RetVal, const MemRegion *Source) const;
   bool isSourceDangle(const MemRegion *Source, ProgramStateRef State,
@@ -41,9 +40,7 @@ class LifetimeAnnotations
 
   const BugType BugMsg{this, "LifetimeAnnotations", "LifetimeBound"};
 
-  using FnCheck = void (LifetimeAnnotations::*)(const CallEvent &Call,
-                                                const CallExpr *,
-                                                CheckerContext &C) const;
+  using FnCheck = void (LifetimeAnnotations::*)(const CallEvent &Call, CheckerContext &C) const;
 
   const CallDescriptionMap<FnCheck> Callbacks = {
       {{CDM::SimpleFunc, {"clang_analyzer_lifetime_bound"}},
@@ -100,7 +97,7 @@ void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
   }
 
   if (const auto *IC = dyn_cast<CXXInstanceCall>(&Call)) {
-    if (clang::lifetimes::implicitObjectParamIsLifetimeBound(FD)) {
+    if (lifetimes::implicitObjectParamIsLifetimeBound(FD)) {
       if (const MemRegion *AttrRegion = IC->getCXXThisVal().getAsRegion()) {
         State = bindValues(State, RetValSym, RetVal, AttrRegion);
       }
@@ -150,6 +147,9 @@ void LifetimeAnnotations::checkEndFunction(const ReturnStmt *RS,
   auto LBMap = State->get<LifetimeBoundMap>();
   auto LBMapVal = State->get<LifetimeBoundMapVal>();
 
+  if (LBMap.isEmpty() && LBMapVal.isEmpty())
+    return;
+
   const Expr *RetExpr = RS->getRetValue();
   if (!RetExpr)
     return;
@@ -157,9 +157,6 @@ void LifetimeAnnotations::checkEndFunction(const ReturnStmt *RS,
   RetExpr = RetExpr->IgnoreParens();
   SVal RetVal = C.getSVal(RetExpr);
 
-  if (LBMap.isEmpty() && LBMapVal.isEmpty())
-    return;
-
   SymbolRef RetValSym = RetVal.getAsSymbol(/*IncludeBaseRegions=*/true);
   const MemRegion *RetValRegion = RetVal.getAsRegion();
   if (!RetValSym && !RetValRegion)
@@ -194,24 +191,29 @@ void LifetimeAnnotations::checkDeadSymbols(SymbolReaper &SymReaper, CheckerConte
   // Get both maps since both of them needs to be cleaned up
   LifetimeBoundMapTy LBMap = State->get<LifetimeBoundMap>();
   LifetimeBoundMapValTy LBMapVal = State->get<LifetimeBoundMapVal>();
-
+  bool StateChanged = false;
 
   for (auto &Entry : LBMap) {
     const SymExpr *Sym = Entry.first;
     bool IsSymbolLive = SymReaper.isLive(Sym);
 
-    if (!IsSymbolLive)
+    if (!IsSymbolLive) {
       State = State->remove<LifetimeBoundMap>(Sym);
+      StateChanged = true;
+    }
   }
 
   for(auto &Entry: LBMapVal) {
     const MemRegion *Region = Entry.first;
-    bool IsSymbolLive = SymReaper.isLiveRegion(Region);
+    bool IsRegionLive = SymReaper.isLiveRegion(Region);
 
-    if (!IsSymbolLive)
+    if (!IsRegionLive) {
       State = State->remove<LifetimeBoundMapVal>(Region);
+      StateChanged = true;
+    }
   }
-  C.addTransition(State);
+  if (StateChanged)
+    C.addTransition(State);
 }
 
 void LifetimeAnnotations::printState(raw_ostream &Out, ProgramStateRef State,
@@ -244,16 +246,14 @@ bool LifetimeAnnotations::evalCall(const CallEvent &Call,
   if (!Handler)
     return false;
 
-  (this->*(*Handler))(Call, CE, C);
+  (this->*(*Handler))(Call, C);
   return true;
 }
 
-void LifetimeAnnotations::analyzerLifetimeBound(const CallEvent &Call,
-                                                const CallExpr *CE,
-                                                CheckerContext &C) const {
+void LifetimeAnnotations::analyzerLifetimeBound(const CallEvent &Call, CheckerContext &C) const {
 
   ProgramStateRef State = C.getState();
-  unsigned int ArgCount = CE->getNumArgs();
+  unsigned int ArgCount = Call.getNumArgs();
   if (ArgCount != 1)
     return;
 

>From 029c4476220419d6f3ebf2ea2b4c0ca3dcf54fb8 Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Mon, 15 Jun 2026 23:28:33 +0200
Subject: [PATCH 26/38] [analyzer] Error message formatted correctly.

---
 clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp | 2 +-
 clang/test/Analysis/lifetime-bound.cpp                    | 6 ++++++
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index 6a6c228e72d69..030bbdb3281d9 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -180,7 +180,7 @@ void LifetimeAnnotations::reportDanglingSource(const MemRegion *Region,
   llvm::raw_svector_ostream OS(Str);
 
   OS << "Returning value bound to a local " << Region
-     << "that will go out of scope";
+     << " that will go out of scope";
   auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, OS.str(), N);
   C.emitReport(std::move(BR));
 }
diff --git a/clang/test/Analysis/lifetime-bound.cpp b/clang/test/Analysis/lifetime-bound.cpp
index f9389c88854e9..8a4e069123b12 100644
--- a/clang/test/Analysis/lifetime-bound.cpp
+++ b/clang/test/Analysis/lifetime-bound.cpp
@@ -131,3 +131,9 @@ void caller_view() {
   // FIXME: Currently none of the maps cover LazyCompoundVal
   clang_analyzer_lifetime_bound(w); // no-warning
 }
+
+
+
+// These are the test cases for testing the correctness of the emitted warning from the LifetimeAnnotations checker.
+
+

>From 232ab3b677a166cf49fea2e2452afe721557e7e1 Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Tue, 16 Jun 2026 00:53:09 +0200
Subject: [PATCH 27/38] [analyzer] Added test cases for the checker.

---
 .../Checkers/LifetimeAnnotations.cpp          | 11 +++++----
 clang/test/Analysis/lifetime-bound.cpp        | 24 +++++++++++++++++++
 2 files changed, 31 insertions(+), 4 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index 030bbdb3281d9..fbfc4f41eae0f 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -40,7 +40,8 @@ class LifetimeAnnotations
 
   const BugType BugMsg{this, "LifetimeAnnotations", "LifetimeBound"};
 
-  using FnCheck = void (LifetimeAnnotations::*)(const CallEvent &Call, CheckerContext &C) const;
+  using FnCheck = void (LifetimeAnnotations::*)(const CallEvent &Call,
+                                                CheckerContext &C) const;
 
   const CallDescriptionMap<FnCheck> Callbacks = {
       {{CDM::SimpleFunc, {"clang_analyzer_lifetime_bound"}},
@@ -185,7 +186,8 @@ void LifetimeAnnotations::reportDanglingSource(const MemRegion *Region,
   C.emitReport(std::move(BR));
 }
 
-void LifetimeAnnotations::checkDeadSymbols(SymbolReaper &SymReaper, CheckerContext &C) const {
+void LifetimeAnnotations::checkDeadSymbols(SymbolReaper &SymReaper,
+                                           CheckerContext &C) const {
   ProgramStateRef State = C.getState();
 
   // Get both maps since both of them needs to be cleaned up
@@ -203,7 +205,7 @@ void LifetimeAnnotations::checkDeadSymbols(SymbolReaper &SymReaper, CheckerConte
     }
   }
 
-  for(auto &Entry: LBMapVal) {
+  for (auto &Entry : LBMapVal) {
     const MemRegion *Region = Entry.first;
     bool IsRegionLive = SymReaper.isLiveRegion(Region);
 
@@ -250,7 +252,8 @@ bool LifetimeAnnotations::evalCall(const CallEvent &Call,
   return true;
 }
 
-void LifetimeAnnotations::analyzerLifetimeBound(const CallEvent &Call, CheckerContext &C) const {
+void LifetimeAnnotations::analyzerLifetimeBound(const CallEvent &Call,
+                                                CheckerContext &C) const {
 
   ProgramStateRef State = C.getState();
   unsigned int ArgCount = Call.getNumArgs();
diff --git a/clang/test/Analysis/lifetime-bound.cpp b/clang/test/Analysis/lifetime-bound.cpp
index 8a4e069123b12..ef03f78b3e228 100644
--- a/clang/test/Analysis/lifetime-bound.cpp
+++ b/clang/test/Analysis/lifetime-bound.cpp
@@ -136,4 +136,28 @@ void caller_view() {
 
 // These are the test cases for testing the correctness of the emitted warning from the LifetimeAnnotations checker.
 
+int *test_func(int *p [[clang::lifetimebound]]);
 
+
+int *direct_return() {
+  int i = 5;
+  return test_func(&i);
+  // expected-warning at -1 {{Returning value bound to a local i that will go out of scope}}
+  // expected-warning at -2 {{address of stack memory associated with local variable 'i' returned}}
+}
+
+int *variable_return() {
+  int y = 5;
+  int *p = test_func(&y);
+  return p; // expected-warning {{Returning value bound to a local y that will go out of scope}}
+}
+
+int *borrow_from_caller(int *b [[clang::lifetimebound]]) {
+  return test_func(b); // no-warning
+}
+
+void no_return() {
+  int i = 5;
+  int *p = test_func(&i);
+  (void)p; // no-warning
+}

>From 291ec2618d6a6f054dfc9f16670129e5f42d0aee Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Tue, 16 Jun 2026 12:43:58 +0200
Subject: [PATCH 28/38] [analyzer] Removed SymbolRef from bindValues.

---
 .../StaticAnalyzer/Checkers/LifetimeAnnotations.cpp   | 11 ++++-------
 1 file changed, 4 insertions(+), 7 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index fbfc4f41eae0f..4e622e7829d60 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -24,8 +24,7 @@ class LifetimeAnnotations
                   const char *Sep) const override;
   bool evalCall(const CallEvent &Call, CheckerContext &C) const;
   void analyzerLifetimeBound(const CallEvent &Call, CheckerContext &C) const;
-  ProgramStateRef bindValues(ProgramStateRef State, SymbolRef RetValSym,
-                             SVal RetVal, const MemRegion *Source) const;
+  ProgramStateRef bindValues(ProgramStateRef State, SVal RetVal, const MemRegion *Source) const;
   bool isSourceDangle(const MemRegion *Source, ProgramStateRef State,
                       CheckerContext &C) const;
   void reportDanglingSource(const MemRegion *Region, ExplodedNode *N,
@@ -52,13 +51,12 @@ class LifetimeAnnotations
 } // namespace
 
 ProgramStateRef LifetimeAnnotations::bindValues(ProgramStateRef State,
-                                                SymbolRef RetValSym,
                                                 SVal RetVal,
                                                 const MemRegion *Source) const {
   LifetimeSourceSet::Factory &F =
       State->getStateManager().get_context<LifetimeSourceSet>();
 
-  if (RetValSym) {
+  if (SymbolRef RetValSym = RetVal.getAsSymbol(/*IncludeBaseRegions=*/true)) {
     const LifetimeSourceSet *LBSet = State->get<LifetimeBoundMap>(RetValSym);
     LifetimeSourceSet Set = LBSet ? *LBSet : F.getEmptySet();
     Set = F.add(Set, Source);
@@ -86,21 +84,20 @@ void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
     return;
 
   SVal RetVal = Call.getReturnValue();
-  SymbolRef RetValSym = RetVal.getAsSymbol(/*IncludeBaseRegions=*/true);
 
   for (const ParmVarDecl *PVD : FD->parameters()) {
     if (PVD->hasAttr<LifetimeBoundAttr>()) {
       unsigned Idx = PVD->getFunctionScopeIndex();
       SVal Arg = Call.getArgSVal(Idx);
       if (const MemRegion *ArgValRegion = Arg.getAsRegion())
-        State = bindValues(State, RetValSym, RetVal, ArgValRegion);
+        State = bindValues(State, RetVal, ArgValRegion);
     }
   }
 
   if (const auto *IC = dyn_cast<CXXInstanceCall>(&Call)) {
     if (lifetimes::implicitObjectParamIsLifetimeBound(FD)) {
       if (const MemRegion *AttrRegion = IC->getCXXThisVal().getAsRegion()) {
-        State = bindValues(State, RetValSym, RetVal, AttrRegion);
+        State = bindValues(State, RetVal, AttrRegion);
       }
     }
   }

>From 6289948e439ad607323cc63420effa771a4e7cbc Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Tue, 16 Jun 2026 13:47:36 +0200
Subject: [PATCH 29/38] [analyzer] Nits applied.

---
 .../StaticAnalyzer/Checkers/LifetimeAnnotations.cpp  | 12 +++++-------
 1 file changed, 5 insertions(+), 7 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index 4e622e7829d60..7ad4abf702bb9 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -25,7 +25,7 @@ class LifetimeAnnotations
   bool evalCall(const CallEvent &Call, CheckerContext &C) const;
   void analyzerLifetimeBound(const CallEvent &Call, CheckerContext &C) const;
   ProgramStateRef bindValues(ProgramStateRef State, SVal RetVal, const MemRegion *Source) const;
-  bool isSourceDangle(const MemRegion *Source, ProgramStateRef State,
+  bool hasDanglingSource(const MemRegion *Source, ProgramStateRef State,
                       CheckerContext &C) const;
   void reportDanglingSource(const MemRegion *Region, ExplodedNode *N,
                             CheckerContext &C) const;
@@ -104,7 +104,7 @@ void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
   C.addTransition(State);
 }
 
-bool LifetimeAnnotations::isSourceDangle(const MemRegion *Source,
+bool LifetimeAnnotations::hasDanglingSource(const MemRegion *Source,
                                          ProgramStateRef State,
                                          CheckerContext &C) const {
   // FIXME: Currently the checker only focuses on stack MemRegions only since
@@ -129,7 +129,7 @@ void LifetimeAnnotations::checkReturnedBorrower(const MapTy &Map,
   for (auto &&[Origin, SourceSet] : Map) {
     if (Origin == RetKey) {
       for (const MemRegion *Region : SourceSet) {
-        if (isSourceDangle(Region, State, C))
+        if (hasDanglingSource(Region, State, C))
           reportDanglingSource(Region, N, C);
       }
     }
@@ -194,9 +194,8 @@ void LifetimeAnnotations::checkDeadSymbols(SymbolReaper &SymReaper,
 
   for (auto &Entry : LBMap) {
     const SymExpr *Sym = Entry.first;
-    bool IsSymbolLive = SymReaper.isLive(Sym);
 
-    if (!IsSymbolLive) {
+    if (!SymReaper.isLive(Sym)) {
       State = State->remove<LifetimeBoundMap>(Sym);
       StateChanged = true;
     }
@@ -204,9 +203,8 @@ void LifetimeAnnotations::checkDeadSymbols(SymbolReaper &SymReaper,
 
   for (auto &Entry : LBMapVal) {
     const MemRegion *Region = Entry.first;
-    bool IsRegionLive = SymReaper.isLiveRegion(Region);
 
-    if (!IsRegionLive) {
+    if (!SymReaper.isLiveRegion(Region)) {
       State = State->remove<LifetimeBoundMapVal>(Region);
       StateChanged = true;
     }

>From 1e780d08cdca36eca738f57c610142bb8a4ae343 Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Wed, 17 Jun 2026 16:39:41 +0200
Subject: [PATCH 30/38] [analyzer] Implemented out-of-scope dangling ptr
 dereference detection, but many false positives on existing lit test.

---
 FETCH_HEAD                                    |   0
 .../clang/StaticAnalyzer/Checkers/Checkers.td |   4 +
 .../Checkers/LifetimeAnnotations.cpp          | 115 ++++++++++++------
 clang/test/Analysis/lifetime-bound.cpp        |  23 +++-
 4 files changed, 101 insertions(+), 41 deletions(-)
 create mode 100644 FETCH_HEAD

diff --git a/FETCH_HEAD b/FETCH_HEAD
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td b/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td
index 85d59fc139728..432970f8d6c35 100644
--- a/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td
+++ b/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td
@@ -1580,6 +1580,10 @@ def CheckerDocumentationChecker : Checker<"CheckerDocumentation">,
   HelpText<"Defines an empty checker callback for all possible handlers.">,
   Documentation<NotDocumented>;
 
+def DebugLifetimeAnnotations : Checker<"DebugLifetimeAnnotations">,
+  HelpText<"Prints the bindings recorded by the LifetimeAnnotations checker. Use with clang_analyzer_lifetime_bound().">,
+  Documentation<NotDocumented>;
+
 } // end "debug"
 
 
diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index 7ad4abf702bb9..e6e22d2aa0660 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -17,35 +17,27 @@ REGISTER_MAP_WITH_PROGRAMSTATE(LifetimeBoundMapVal, const MemRegion *,
 
 namespace {
 class LifetimeAnnotations
-    : public Checker<check::PostCall, check::EndFunction, eval::Call> {
+    : public Checker<check::PostCall, check::EndFunction, check::LifetimeEnd> {
 public:
   void checkPostCall(const CallEvent &Call, CheckerContext &C) const;
   void printState(raw_ostream &Out, ProgramStateRef State, const char *NL,
                   const char *Sep) const override;
-  bool evalCall(const CallEvent &Call, CheckerContext &C) const;
-  void analyzerLifetimeBound(const CallEvent &Call, CheckerContext &C) const;
   ProgramStateRef bindValues(ProgramStateRef State, SVal RetVal, const MemRegion *Source) const;
   bool hasDanglingSource(const MemRegion *Source, ProgramStateRef State,
-                      CheckerContext &C) const;
+                         CheckerContext &C) const;
   void reportDanglingSource(const MemRegion *Region, ExplodedNode *N,
                             CheckerContext &C) const;
+  void reportUseAfterScope(const VarDecl *Region, ExplodedNode *N, CheckerContext &C) const;
 
   template <typename MapTy, typename KeyTy>
   void checkReturnedBorrower(const MapTy &Map, const KeyTy RetKey,
                              ProgramStateRef State, ExplodedNode *N,
                              CheckerContext &C) const;
   void checkEndFunction(const ReturnStmt *RS, CheckerContext &C) const;
+  void checkLifetimeEnd(const VarDecl *VD, CheckerContext &C) const;
   void checkDeadSymbols(SymbolReaper &SymReaper, CheckerContext &C) const;
 
   const BugType BugMsg{this, "LifetimeAnnotations", "LifetimeBound"};
-
-  using FnCheck = void (LifetimeAnnotations::*)(const CallEvent &Call,
-                                                CheckerContext &C) const;
-
-  const CallDescriptionMap<FnCheck> Callbacks = {
-      {{CDM::SimpleFunc, {"clang_analyzer_lifetime_bound"}},
-       &LifetimeAnnotations::analyzerLifetimeBound},
-  };
 };
 
 } // namespace
@@ -105,8 +97,8 @@ void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
 }
 
 bool LifetimeAnnotations::hasDanglingSource(const MemRegion *Source,
-                                         ProgramStateRef State,
-                                         CheckerContext &C) const {
+                                            ProgramStateRef State,
+                                            CheckerContext &C) const {
   // FIXME: Currently the checker only focuses on stack MemRegions only since
   // that is the scope of week 3. Sources without a stack region are not
   // covered, but should be implemented as well next step.
@@ -171,15 +163,52 @@ void LifetimeAnnotations::checkEndFunction(const ReturnStmt *RS,
     checkReturnedBorrower(LBMapVal, RetValRegion, State, N, C);
 }
 
+void LifetimeAnnotations::checkLifetimeEnd(const VarDecl *VD, CheckerContext &C) const {
+  if (!VD)
+    return;
+
+  ProgramStateRef State = C.getState();
+
+  auto LBMap = State->get<LifetimeBoundMap>();
+  auto LBMapVal = State->get<LifetimeBoundMapVal>();
+
+  if (LBMap.isEmpty() && LBMapVal.isEmpty())
+    return;
+
+  for (auto &&[Origin, SourceSet] : LBMapVal) {
+    for (const MemRegion *Region : SourceSet) {
+      if (const VarDecl *VDRegion = dyn_cast_if_present<VarRegion>(Region)->getDecl()) {
+        if (VDRegion == VD) {
+          ExplodedNode *N = C.generateNonFatalErrorNode();
+          reportUseAfterScope(VDRegion, N, C);
+        }
+      }
+    }
+  }
+
+  for (auto &&[Origin, SourceSet] : LBMap) {
+    for (const MemRegion *Region : SourceSet) {
+      if (const VarDecl *Variable = dyn_cast_if_present<VarRegion>(Region)->getDecl()) {
+        if (Variable == VD) {
+          ExplodedNode *N = C.generateNonFatalErrorNode();
+          reportUseAfterScope(Variable, N, C);
+        }
+      }
+    }
+  }
+}
+
 void LifetimeAnnotations::reportDanglingSource(const MemRegion *Region,
                                                ExplodedNode *N,
                                                CheckerContext &C) const {
-  llvm::SmallString<128> Str;
-  llvm::raw_svector_ostream OS(Str);
+  std::string ErrorMessage = (llvm::Twine("Returning value bound to a local '") + Region->getString() + "' that will go out of scope").str();
+  auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, ErrorMessage, N);
+  C.emitReport(std::move(BR));
+}
 
-  OS << "Returning value bound to a local " << Region
-     << " that will go out of scope";
-  auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, OS.str(), N);
+void LifetimeAnnotations::reportUseAfterScope(const VarDecl *Region, ExplodedNode *N, CheckerContext &C) const {
+  std::string ErrorMessage = (llvm::Twine("Used variable after its scope ended '") + Region->getNameAsString() + "' error.").str();
+  auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, ErrorMessage, N);
   C.emitReport(std::move(BR));
 }
 
@@ -190,26 +219,17 @@ void LifetimeAnnotations::checkDeadSymbols(SymbolReaper &SymReaper,
   // Get both maps since both of them needs to be cleaned up
   LifetimeBoundMapTy LBMap = State->get<LifetimeBoundMap>();
   LifetimeBoundMapValTy LBMapVal = State->get<LifetimeBoundMapVal>();
-  bool StateChanged = false;
 
-  for (auto &Entry : LBMap) {
-    const SymExpr *Sym = Entry.first;
-
-    if (!SymReaper.isLive(Sym)) {
+  for (const SymExpr *Sym : llvm::make_first_range(LBMap)) {
+    if (!SymReaper.isLive(Sym))
       State = State->remove<LifetimeBoundMap>(Sym);
-      StateChanged = true;
-    }
   }
 
-  for (auto &Entry : LBMapVal) {
-    const MemRegion *Region = Entry.first;
-
-    if (!SymReaper.isLiveRegion(Region)) {
+  for (const MemRegion *Region : llvm::make_first_range(LBMapVal)) {
+    if (!SymReaper.isLiveRegion(Region))
       State = State->remove<LifetimeBoundMapVal>(Region);
-      StateChanged = true;
-    }
   }
-  if (StateChanged)
+  if (State != C.getState())
     C.addTransition(State);
 }
 
@@ -232,7 +252,24 @@ void LifetimeAnnotations::printState(raw_ostream &Out, ProgramStateRef State,
   }
 }
 
-bool LifetimeAnnotations::evalCall(const CallEvent &Call,
+namespace {
+class DebugLifetimeAnnotations : public Checker<eval::Call> {
+public:
+  bool evalCall(const CallEvent &Call, CheckerContext &C) const;
+  void analyzerLifetimeBound(const CallEvent &Call, CheckerContext &C) const;
+
+  const BugType BugMsg{this, "DebugLifetimeAnnotations", "DebugLifetimeBound"};
+  using FnCheck = void (DebugLifetimeAnnotations::*)(const CallEvent &Call, CheckerContext &C) const;
+
+  const CallDescriptionMap<FnCheck> Callbacks = {
+      {{CDM::SimpleFunc, {"clang_analyzer_lifetime_bound"}},
+       &DebugLifetimeAnnotations::analyzerLifetimeBound},
+  };
+};
+
+} // namespace
+
+bool DebugLifetimeAnnotations::evalCall(const CallEvent &Call,
                                    CheckerContext &C) const {
 
   const auto *CE = dyn_cast_if_present<CallExpr>(Call.getOriginExpr());
@@ -247,7 +284,7 @@ bool LifetimeAnnotations::evalCall(const CallEvent &Call,
   return true;
 }
 
-void LifetimeAnnotations::analyzerLifetimeBound(const CallEvent &Call,
+void DebugLifetimeAnnotations::analyzerLifetimeBound(const CallEvent &Call,
                                                 CheckerContext &C) const {
 
   ProgramStateRef State = C.getState();
@@ -296,3 +333,11 @@ void ento::registerLifetimeAnnotations(CheckerManager &mgr) {
 bool ento::shouldRegisterLifetimeAnnotations(const CheckerManager &mgr) {
   return true;
 }
+
+void ento::registerDebugLifetimeAnnotations(CheckerManager &mgr) {
+  mgr.registerChecker<DebugLifetimeAnnotations>();
+}
+
+bool ento::shouldRegisterDebugLifetimeAnnotations(const CheckerManager &mgr) {
+  return true;
+}
diff --git a/clang/test/Analysis/lifetime-bound.cpp b/clang/test/Analysis/lifetime-bound.cpp
index ef03f78b3e228..8f1d30b615a02 100644
--- a/clang/test/Analysis/lifetime-bound.cpp
+++ b/clang/test/Analysis/lifetime-bound.cpp
@@ -1,7 +1,7 @@
-// RUN: %clang_analyze_cc1 -analyzer-checker=alpha.cplusplus.LifetimeAnnotations \
-// RUN:   -verify %s
-// RUN: %clang_analyze_cc1 -analyzer-checker=alpha.cplusplus.LifetimeAnnotations \
-// RUN:   -analyzer-config c++-container-inlining=false -verify %s
+// RUN: %clang_analyze_cc1 -analyzer-checker=alpha.cplusplus.LifetimeAnnotations,debug.DebugLifetimeAnnotations \
+// RUN:   -analyzer-config cfg-lifetime=true -verify %s
+// RUN: %clang_analyze_cc1 -analyzer-checker=alpha.cplusplus.LifetimeAnnotations,debug.DebugLifetimeAnnotations \
+// RUN:   -analyzer-config c++-container-inlining=false -analyzer-config cfg-lifetime=true -verify %s
 
 struct A {};
 
@@ -136,20 +136,21 @@ void caller_view() {
 
 // These are the test cases for testing the correctness of the emitted warning from the LifetimeAnnotations checker.
 
+// Return value bound to annotated param cases
 int *test_func(int *p [[clang::lifetimebound]]);
 
 
 int *direct_return() {
   int i = 5;
   return test_func(&i);
-  // expected-warning at -1 {{Returning value bound to a local i that will go out of scope}}
+  // expected-warning at -1 {{Returning value bound to a local 'i' that will go out of scope}}
   // expected-warning at -2 {{address of stack memory associated with local variable 'i' returned}}
 }
 
 int *variable_return() {
   int y = 5;
   int *p = test_func(&y);
-  return p; // expected-warning {{Returning value bound to a local y that will go out of scope}}
+  return p; // expected-warning {{Returning value bound to a local 'y' that will go out of scope}}
 }
 
 int *borrow_from_caller(int *b [[clang::lifetimebound]]) {
@@ -161,3 +162,13 @@ void no_return() {
   int *p = test_func(&i);
   (void)p; // no-warning
 }
+
+// Use-after-scope dangling pointer dereference
+void caller_ten() {
+  int* p = nullptr;
+  {
+    int x = 1;
+    p = test_func(&x);
+  } // expected-warning {{Used variable after its scope ended 'n' error}}
+  *p = 2;
+}

>From 10d4204c05d5a0b71491e67f648eae7d8c797fc9 Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Wed, 17 Jun 2026 16:55:31 +0200
Subject: [PATCH 31/38] [analyzer] Detect use-after-scope lit test warning
 message corrected, but false positives still present.

---
 clang/test/Analysis/lifetime-bound.cpp | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/clang/test/Analysis/lifetime-bound.cpp b/clang/test/Analysis/lifetime-bound.cpp
index 8f1d30b615a02..529a5dbf34d70 100644
--- a/clang/test/Analysis/lifetime-bound.cpp
+++ b/clang/test/Analysis/lifetime-bound.cpp
@@ -166,9 +166,9 @@ void no_return() {
 // Use-after-scope dangling pointer dereference
 void caller_ten() {
   int* p = nullptr;
-  {
+  { // expected-warning {{Used variable after its scope ended 'x' error}}
     int x = 1;
     p = test_func(&x);
-  } // expected-warning {{Used variable after its scope ended 'n' error}}
+  }
   *p = 2;
 }

>From 65be9c24664a28b69640afc09f36836de194b2b7 Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Wed, 17 Jun 2026 22:57:27 +0200
Subject: [PATCH 32/38] [analyzer] DeteRemoved helper since stack region does
 not help for this problem.

---
 clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index e6e22d2aa0660..11f967b3cd39b 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -188,10 +188,10 @@ void LifetimeAnnotations::checkLifetimeEnd(const VarDecl *VD, CheckerContext &C)
 
   for (auto &&[Origin, SourceSet] : LBMap) {
     for (const MemRegion *Region : SourceSet) {
-      if (const VarDecl *Variable = dyn_cast_if_present<VarRegion>(Region)->getDecl()) {
-        if (Variable == VD) {
+      if (const VarDecl *VDRegion = dyn_cast_if_present<VarRegion>(Region)->getDecl()) {
+        if (VDRegion == VD) {
           ExplodedNode *N = C.generateNonFatalErrorNode();
-          reportUseAfterScope(Variable, N, C);
+          reportUseAfterScope(VDRegion, N, C);
         }
       }
     }

>From 70c4f7d94eca7b5e75568ce2581cf8f458bd3484 Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Thu, 18 Jun 2026 17:22:41 +0200
Subject: [PATCH 33/38] [analyzer] Current approach does not differentiate
 between inner and outer scope of a function.

---
 clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp | 1 +
 1 file changed, 1 insertion(+)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index 11f967b3cd39b..4661ab7a0d199 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -14,6 +14,7 @@ REGISTER_MAP_WITH_PROGRAMSTATE(LifetimeBoundMap, SymbolRef, LifetimeSourceSet)
 
 REGISTER_MAP_WITH_PROGRAMSTATE(LifetimeBoundMapVal, const MemRegion *,
                                LifetimeSourceSet)
+REGISTER_SET_WITH_PROGRAMSTATE(DeadSourceSet, const MemRegion *)
 
 namespace {
 class LifetimeAnnotations

>From 979306c5a4b27fd9b312381c2362b748b8ad93ae Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Thu, 18 Jun 2026 21:58:45 +0200
Subject: [PATCH 34/38] [analyzer] Finished implementing checkLifetimeEnd and
 checkLocation callbacks.

---
 .../Checkers/LifetimeAnnotations.cpp          | 85 +++++++++++++------
 clang/test/Analysis/lifetime-bound.cpp        | 24 +++++-
 2 files changed, 82 insertions(+), 27 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index 4661ab7a0d199..58d1808f16364 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -18,17 +18,20 @@ REGISTER_SET_WITH_PROGRAMSTATE(DeadSourceSet, const MemRegion *)
 
 namespace {
 class LifetimeAnnotations
-    : public Checker<check::PostCall, check::EndFunction, check::LifetimeEnd> {
+    : public Checker<check::PostCall, check::EndFunction, check::LifetimeEnd,
+                     check::Location> {
 public:
   void checkPostCall(const CallEvent &Call, CheckerContext &C) const;
   void printState(raw_ostream &Out, ProgramStateRef State, const char *NL,
                   const char *Sep) const override;
-  ProgramStateRef bindValues(ProgramStateRef State, SVal RetVal, const MemRegion *Source) const;
+  ProgramStateRef bindValues(ProgramStateRef State, SVal RetVal,
+                             const MemRegion *Source) const;
   bool hasDanglingSource(const MemRegion *Source, ProgramStateRef State,
                          CheckerContext &C) const;
   void reportDanglingSource(const MemRegion *Region, ExplodedNode *N,
                             CheckerContext &C) const;
-  void reportUseAfterScope(const VarDecl *Region, ExplodedNode *N, CheckerContext &C) const;
+  void reportUseAfterScope(const MemRegion *Region, ExplodedNode *N,
+                           CheckerContext &C) const;
 
   template <typename MapTy, typename KeyTy>
   void checkReturnedBorrower(const MapTy &Map, const KeyTy RetKey,
@@ -36,6 +39,8 @@ class LifetimeAnnotations
                              CheckerContext &C) const;
   void checkEndFunction(const ReturnStmt *RS, CheckerContext &C) const;
   void checkLifetimeEnd(const VarDecl *VD, CheckerContext &C) const;
+  void checkLocation(SVal Loc, bool IsLoad, const Stmt *S,
+                     CheckerContext &C) const;
   void checkDeadSymbols(SymbolReaper &SymReaper, CheckerContext &C) const;
 
   const BugType BugMsg{this, "LifetimeAnnotations", "LifetimeBound"};
@@ -164,36 +169,58 @@ void LifetimeAnnotations::checkEndFunction(const ReturnStmt *RS,
     checkReturnedBorrower(LBMapVal, RetValRegion, State, N, C);
 }
 
-void LifetimeAnnotations::checkLifetimeEnd(const VarDecl *VD, CheckerContext &C) const {
+void LifetimeAnnotations::checkLifetimeEnd(const VarDecl *VD,
+                                           CheckerContext &C) const {
+  ProgramStateRef State = C.getState();
   if (!VD)
     return;
 
-  ProgramStateRef State = C.getState();
+  SVal SourceVal = State->getLValue(VD, C.getStackFrame());
+  if (const MemRegion *SourceRegion = SourceVal.getAsRegion()) {
+    State = State->add<DeadSourceSet>(SourceRegion);
+    C.addTransition(State);
+  }
+}
 
+void LifetimeAnnotations::checkLocation(SVal Loc, bool IsLoad, const Stmt *S,
+                                        CheckerContext &C) const {
+  ProgramStateRef State = C.getState();
   auto LBMap = State->get<LifetimeBoundMap>();
   auto LBMapVal = State->get<LifetimeBoundMapVal>();
 
+  ExplodedNode *N = C.generateNonFatalErrorNode();
+  if (!N)
+    return;
+
+  // FIXME: Because of the CFG::LifetimeEnd elements now the analyzer can
+  // reason about out-of-scope dangling pointer deref even if there is
+  // no annotations in the source code.
+  if (const MemRegion *R = Loc.getAsRegion()) {
+    if (R && State->contains<DeadSourceSet>(R))
+      reportUseAfterScope(R, N, C);
+  }
+
   if (LBMap.isEmpty() && LBMapVal.isEmpty())
     return;
 
-  for (auto &&[Origin, SourceSet] : LBMapVal) {
-    for (const MemRegion *Region : SourceSet) {
-      if (const VarDecl *VDRegion = dyn_cast_if_present<VarRegion>(Region)->getDecl()) {
-        if (VDRegion == VD) {
-          ExplodedNode *N = C.generateNonFatalErrorNode();
-          reportUseAfterScope(VDRegion, N, C);
-        }
+  // FIXME: If a borrower has multiple bound sources the callback warns
+  // if any source has died. The callback should track which source the
+  // borrower actually points.
+
+  if (SymbolRef LocSym = Loc.getAsSymbol(true)) {
+    if (auto *SourceSet = State->get<LifetimeBoundMap>(LocSym)) {
+      for (const MemRegion *Source : *SourceSet) {
+        if (State->contains<DeadSourceSet>(Source))
+          reportUseAfterScope(Source, N, C);
       }
     }
   }
 
-  for (auto &&[Origin, SourceSet] : LBMap) {
-    for (const MemRegion *Region : SourceSet) {
-      if (const VarDecl *VDRegion = dyn_cast_if_present<VarRegion>(Region)->getDecl()) {
-        if (VDRegion == VD) {
-          ExplodedNode *N = C.generateNonFatalErrorNode();
-          reportUseAfterScope(VDRegion, N, C);
-        }
+  if (const MemRegion *LocRegion = Loc.getAsRegion()) {
+    if (auto *SourceSet = State->get<LifetimeBoundMapVal>(LocRegion)) {
+      for (const MemRegion *Source : *SourceSet) {
+        if (State->contains<DeadSourceSet>(Source))
+          reportUseAfterScope(Source, N, C);
       }
     }
   }
@@ -202,13 +229,20 @@ void LifetimeAnnotations::checkLifetimeEnd(const VarDecl *VD, CheckerContext &C)
 void LifetimeAnnotations::reportDanglingSource(const MemRegion *Region,
                                                ExplodedNode *N,
                                                CheckerContext &C) const {
-  std::string ErrorMessage = (llvm::Twine("Returning value bound to a local '") + Region->getString() + "' that will go out of scope").str();
+  std::string ErrorMessage =
+      (llvm::Twine("Returning value bound to a local '") + Region->getString() +
+       "' that will go out of scope")
+          .str();
   auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, ErrorMessage, N);
   C.emitReport(std::move(BR));
 }
 
-void LifetimeAnnotations::reportUseAfterScope(const VarDecl *Region, ExplodedNode *N, CheckerContext &C) const {
-  std::string ErrorMessage = (llvm::Twine("Used variable after its scope ended '") + Region->getNameAsString() + "' error.").str();
+void LifetimeAnnotations::reportUseAfterScope(const MemRegion *Region,
+                                              ExplodedNode *N,
+                                              CheckerContext &C) const {
+  std::string ErrorMessage = (llvm::Twine("Use of '") + Region->getString() +
+                              "' after its lifetime ended.")
+                                 .str();
   auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, ErrorMessage, N);
   C.emitReport(std::move(BR));
 }
@@ -260,7 +294,8 @@ class DebugLifetimeAnnotations : public Checker<eval::Call> {
   void analyzerLifetimeBound(const CallEvent &Call, CheckerContext &C) const;
 
   const BugType BugMsg{this, "DebugLifetimeAnnotations", "DebugLifetimeBound"};
-  using FnCheck = void (DebugLifetimeAnnotations::*)(const CallEvent &Call, CheckerContext &C) const;
+  using FnCheck = void (DebugLifetimeAnnotations::*)(const CallEvent &Call,
+                                                     CheckerContext &C) const;
 
   const CallDescriptionMap<FnCheck> Callbacks = {
       {{CDM::SimpleFunc, {"clang_analyzer_lifetime_bound"}},
@@ -271,7 +306,7 @@ class DebugLifetimeAnnotations : public Checker<eval::Call> {
 } // namespace
 
 bool DebugLifetimeAnnotations::evalCall(const CallEvent &Call,
-                                   CheckerContext &C) const {
+                                        CheckerContext &C) const {
 
   const auto *CE = dyn_cast_if_present<CallExpr>(Call.getOriginExpr());
   if (!CE)
@@ -286,7 +321,7 @@ bool DebugLifetimeAnnotations::evalCall(const CallEvent &Call,
 }
 
 void DebugLifetimeAnnotations::analyzerLifetimeBound(const CallEvent &Call,
-                                                CheckerContext &C) const {
+                                                     CheckerContext &C) const {
 
   ProgramStateRef State = C.getState();
   unsigned int ArgCount = Call.getNumArgs();
diff --git a/clang/test/Analysis/lifetime-bound.cpp b/clang/test/Analysis/lifetime-bound.cpp
index 529a5dbf34d70..e166e76a059e8 100644
--- a/clang/test/Analysis/lifetime-bound.cpp
+++ b/clang/test/Analysis/lifetime-bound.cpp
@@ -166,9 +166,29 @@ void no_return() {
 // Use-after-scope dangling pointer dereference
 void caller_ten() {
   int* p = nullptr;
-  { // expected-warning {{Used variable after its scope ended 'x' error}}
+  {
     int x = 1;
     p = test_func(&x);
   }
-  *p = 2;
+  *p = 2; // expected-warning {{Use of 'x' after its lifetime ended}}
+}
+
+void out_of_scope_ptr() {
+  int *ptr = nullptr;
+  {
+    int n = 5;
+    ptr = &n;
+  }
+  *ptr = 3; // expected-warning {{Use of 'n' after its lifetime ended}}
+}
+
+void f() {
+  int* p;
+  {
+    int x = 1;
+    p = &x;
+  }
+  int y = 2;
+  p = &y;
+  *p = 3; // no-warning
 }

>From 6d6a21dfeea40933ee1722d3e3414e43e07aff76 Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Thu, 18 Jun 2026 22:33:28 +0200
Subject: [PATCH 35/38] [analyzer] Finished implementing checkLifetimeEnd and
 checkLocation callbacks.

---
 .../Checkers/LifetimeAnnotations.cpp          | 25 +++++++++++--------
 1 file changed, 14 insertions(+), 11 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index 58d1808f16364..71b0f89f4e433 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -182,22 +182,21 @@ void LifetimeAnnotations::checkLifetimeEnd(const VarDecl *VD,
   }
 }
 
+// FIXME: Use helper functions for checkLocation
 void LifetimeAnnotations::checkLocation(SVal Loc, bool IsLoad, const Stmt *S,
                                         CheckerContext &C) const {
   ProgramStateRef State = C.getState();
   auto LBMap = State->get<LifetimeBoundMap>();
   auto LBMapVal = State->get<LifetimeBoundMapVal>();
 
-  ExplodedNode *N = C.generateNonFatalErrorNode();
-  if (!N)
-    return;
-
   // FIXME: Because of the CFG::LifetimeEnd elements now the analyzer can
   // reason about out-of-scope dangling pointer deref even if there is
   // no annotations in the source code.
   if (const MemRegion *R = Loc.getAsRegion()) {
-    if (R && State->contains<DeadSourceSet>(R))
-      reportUseAfterScope(R, N, C);
+    if (State->contains<DeadSourceSet>(R)) {
+      if (ExplodedNode *N = C.generateNonFatalErrorNode())
+        reportUseAfterScope(R, N, C);
+    }
   }
 
   if (LBMap.isEmpty() && LBMapVal.isEmpty())
@@ -206,12 +205,13 @@ void LifetimeAnnotations::checkLocation(SVal Loc, bool IsLoad, const Stmt *S,
   // FIXME: If a borrower has multiple bound sources the callback warns
   // if any source has died. The callback should track which source the
   // borrower actually points.
-
   if (SymbolRef LocSym = Loc.getAsSymbol(true)) {
     if (auto *SourceSet = State->get<LifetimeBoundMap>(LocSym)) {
       for (const MemRegion *Source : *SourceSet) {
-        if (State->contains<DeadSourceSet>(Source))
-          reportUseAfterScope(Source, N, C);
+        if (State->contains<DeadSourceSet>(Source)) {
+          if (ExplodedNode *N = C.generateNonFatalErrorNode())
+            reportUseAfterScope(Source, N, C);
+        }
       }
     }
   }
@@ -219,8 +219,11 @@ void LifetimeAnnotations::checkLocation(SVal Loc, bool IsLoad, const Stmt *S,
   if (const MemRegion *LocRegion = Loc.getAsRegion()) {
     if (auto *SourceSet = State->get<LifetimeBoundMapVal>(LocRegion)) {
       for (const MemRegion *Source : *SourceSet) {
-        if (State->contains<DeadSourceSet>(Source))
-          reportUseAfterScope(Source, N, C);
+        if (State->contains<DeadSourceSet>(Source)) {
+          if (ExplodedNode *N = C.generateNonFatalErrorNode()) {
+            reportUseAfterScope(Source, N, C);
+          }
+        }
       }
     }
   }

>From daeec1aede47252c87540cec3f2e2c99c1c23acd Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Mon, 22 Jun 2026 01:33:49 +0200
Subject: [PATCH 36/38] Applied changes to the code base.

---
 .../clang/StaticAnalyzer/Checkers/Checkers.td |  4 +-
 .../Checkers/LifetimeAnnotations.cpp          | 54 +++++++++----------
 clang/test/Analysis/debug-lifetime-bound.cpp  | 12 +++++
 clang/test/Analysis/lifetime-bound.cpp        | 11 +++-
 4 files changed, 48 insertions(+), 33 deletions(-)
 create mode 100644 clang/test/Analysis/debug-lifetime-bound.cpp

diff --git a/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td b/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td
index 432970f8d6c35..45ce97e5e4424 100644
--- a/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td
+++ b/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td
@@ -789,7 +789,8 @@ def SmartPtrChecker: Checker<"SmartPtr">,
   Documentation<HasDocumentation>;
 
 def LifetimeAnnotations : Checker<"LifetimeAnnotations">,
-  HelpText<"Check for lifetime violations using lifetime annotations">,
+  HelpText<"Check for lifetime violations by incorporating lifetime "
+           "annotations into the analysis">,
   Documentation<NotDocumented>;
 
 } // end: "alpha.cplusplus"
@@ -1582,6 +1583,7 @@ def CheckerDocumentationChecker : Checker<"CheckerDocumentation">,
 
 def DebugLifetimeAnnotations : Checker<"DebugLifetimeAnnotations">,
   HelpText<"Prints the bindings recorded by the LifetimeAnnotations checker. Use with clang_analyzer_lifetime_bound().">,
+  WeakDependencies<[LifetimeAnnotations]>,
   Documentation<NotDocumented>;
 
 } // end "debug"
diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index 71b0f89f4e433..07af24f963ace 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -35,8 +35,8 @@ class LifetimeAnnotations
 
   template <typename MapTy, typename KeyTy>
   void checkReturnedBorrower(const MapTy &Map, const KeyTy RetKey,
-                             ProgramStateRef State, ExplodedNode *N,
-                             CheckerContext &C) const;
+                             ProgramStateRef State, CheckerContext &C) const;
+  void reportDanglingBorrower(const LifetimeSourceSet *Sources, CheckerContext &C) const;
   void checkEndFunction(const ReturnStmt *RS, CheckerContext &C) const;
   void checkLifetimeEnd(const VarDecl *VD, CheckerContext &C) const;
   void checkLocation(SVal Loc, bool IsLoad, const Stmt *S,
@@ -122,13 +122,13 @@ template <typename MapTy, typename KeyTy>
 void LifetimeAnnotations::checkReturnedBorrower(const MapTy &Map,
                                                 const KeyTy RetKey,
                                                 ProgramStateRef State,
-                                                ExplodedNode *N,
                                                 CheckerContext &C) const {
   for (auto &&[Origin, SourceSet] : Map) {
     if (Origin == RetKey) {
       for (const MemRegion *Region : SourceSet) {
         if (hasDanglingSource(Region, State, C))
-          reportDanglingSource(Region, N, C);
+          if (ExplodedNode *N = C.generateNonFatalErrorNode())
+            reportDanglingSource(Region, N, C);
       }
     }
   }
@@ -158,15 +158,11 @@ void LifetimeAnnotations::checkEndFunction(const ReturnStmt *RS,
   if (!RetValSym && !RetValRegion)
     return;
 
-  ExplodedNode *N = C.generateNonFatalErrorNode();
-  if (!N)
-    return;
-
   if (RetValSym)
-    checkReturnedBorrower(LBMap, RetValSym, State, N, C);
+    checkReturnedBorrower(LBMap, RetValSym, State, C);
 
   if (RetValRegion)
-    checkReturnedBorrower(LBMapVal, RetValRegion, State, N, C);
+    checkReturnedBorrower(LBMapVal, RetValRegion, State, C);
 }
 
 void LifetimeAnnotations::checkLifetimeEnd(const VarDecl *VD,
@@ -182,7 +178,17 @@ void LifetimeAnnotations::checkLifetimeEnd(const VarDecl *VD,
   }
 }
 
-// FIXME: Use helper functions for checkLocation
+void LifetimeAnnotations::reportDanglingBorrower(const LifetimeSourceSet *Sources, CheckerContext &C) const {
+    ProgramStateRef State = C.getState();
+
+    for (const MemRegion *Source : *Sources) {
+      if (State->contains<DeadSourceSet>(Source)) {
+        if (ExplodedNode *N = C.generateNonFatalErrorNode())
+          reportUseAfterScope(Source, N, C);
+      }
+    }
+}
+
 void LifetimeAnnotations::checkLocation(SVal Loc, bool IsLoad, const Stmt *S,
                                         CheckerContext &C) const {
   ProgramStateRef State = C.getState();
@@ -191,7 +197,8 @@ void LifetimeAnnotations::checkLocation(SVal Loc, bool IsLoad, const Stmt *S,
 
   // FIXME: Because of the CFG::LifetimeEnd elements now the analyzer can
   // reason about out-of-scope dangling pointer deref even if there is
-  // no annotations in the source code.
+  // no annotations in the source code. This detection part of the detection
+  // should live in a separate checker.
   if (const MemRegion *R = Loc.getAsRegion()) {
     if (State->contains<DeadSourceSet>(R)) {
       if (ExplodedNode *N = C.generateNonFatalErrorNode())
@@ -206,26 +213,13 @@ void LifetimeAnnotations::checkLocation(SVal Loc, bool IsLoad, const Stmt *S,
   // if any source has died. The callback should track which source the
   // borrower actually points.
   if (SymbolRef LocSym = Loc.getAsSymbol(true)) {
-    if (auto *SourceSet = State->get<LifetimeBoundMap>(LocSym)) {
-      for (const MemRegion *Source : *SourceSet) {
-        if (State->contains<DeadSourceSet>(Source)) {
-          if (ExplodedNode *N = C.generateNonFatalErrorNode())
-            reportUseAfterScope(Source, N, C);
-        }
-      }
-    }
+    if (auto *SourceSet = State->get<LifetimeBoundMap>(LocSym))
+      reportDanglingBorrower(SourceSet, C);
   }
 
   if (const MemRegion *LocRegion = Loc.getAsRegion()) {
-    if (auto *SourceSet = State->get<LifetimeBoundMapVal>(LocRegion)) {
-      for (const MemRegion *Source : *SourceSet) {
-        if (State->contains<DeadSourceSet>(Source)) {
-          if (ExplodedNode *N = C.generateNonFatalErrorNode()) {
-            reportUseAfterScope(Source, N, C);
-          }
-        }
-      }
-    }
+    if (auto *SourceSet = State->get<LifetimeBoundMapVal>(LocRegion))
+      reportDanglingBorrower(SourceSet, C);
   }
 }
 
@@ -233,7 +227,7 @@ void LifetimeAnnotations::reportDanglingSource(const MemRegion *Region,
                                                ExplodedNode *N,
                                                CheckerContext &C) const {
   std::string ErrorMessage =
-      (llvm::Twine("Returning value bound to a local '") + Region->getString() +
+      (llvm::Twine("Returning value bound to '") + Region->getString() +
        "' that will go out of scope")
           .str();
   auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, ErrorMessage, N);
diff --git a/clang/test/Analysis/debug-lifetime-bound.cpp b/clang/test/Analysis/debug-lifetime-bound.cpp
new file mode 100644
index 0000000000000..3325d7bb08404
--- /dev/null
+++ b/clang/test/Analysis/debug-lifetime-bound.cpp
@@ -0,0 +1,12 @@
+// RUN: %clang_analyze_cc1 -analyzer-checker=debug.DebugLifetimeAnnotations \
+// RUN:   -verify %s
+
+void clang_analyzer_lifetime_bound(int);
+
+// Verify that the DebugLifetimeAnnotations checkre can be used without
+// the LifetimeAnnotations checker being enabled and it does not cause
+// crash.
+void test() {
+  int x = 5;
+  clang_analyzer_lifetime_bound(x); // expected-no-diagnostics
+}
diff --git a/clang/test/Analysis/lifetime-bound.cpp b/clang/test/Analysis/lifetime-bound.cpp
index e166e76a059e8..252209d2a034e 100644
--- a/clang/test/Analysis/lifetime-bound.cpp
+++ b/clang/test/Analysis/lifetime-bound.cpp
@@ -143,14 +143,14 @@ int *test_func(int *p [[clang::lifetimebound]]);
 int *direct_return() {
   int i = 5;
   return test_func(&i);
-  // expected-warning at -1 {{Returning value bound to a local 'i' that will go out of scope}}
+  // 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}}
 }
 
 int *variable_return() {
   int y = 5;
   int *p = test_func(&y);
-  return p; // expected-warning {{Returning value bound to a local 'y' that will go out of scope}}
+  return p; // expected-warning {{Returning value bound to 'y' that will go out of scope}}
 }
 
 int *borrow_from_caller(int *b [[clang::lifetimebound]]) {
@@ -192,3 +192,10 @@ void f() {
   p = &y;
   *p = 3; // no-warning
 }
+
+int* g() {
+  int i = 5;
+  int* p = test_func(&i);
+  (void)p;
+  return nullptr; // no-warning
+}

>From d39148e997edeafa54da23abee3e102c1c83ca5d Mon Sep 17 00:00:00 2001
From: Benedek Kaibas <82393336+benedekaibas at users.noreply.github.com>
Date: Mon, 22 Jun 2026 01:35:43 +0200
Subject: [PATCH 37/38] Delete non-related file.

---
 FETCH_HEAD | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 FETCH_HEAD

diff --git a/FETCH_HEAD b/FETCH_HEAD
deleted file mode 100644
index e69de29bb2d1d..0000000000000

>From 309b2a54df5da849f12148210c891b76e2634836 Mon Sep 17 00:00:00 2001
From: benedekaibas <kaibas.benedek02 at gmail.com>
Date: Mon, 22 Jun 2026 19:42:08 +0200
Subject: [PATCH 38/38] Removed the logic for detecting use-after-scope
 dangling ptrs when there is no annotation.

---
 .../Checkers/LifetimeAnnotations.cpp          | 62 +++++++------------
 clang/test/Analysis/lifetime-bound.cpp        | 30 ---------
 2 files changed, 21 insertions(+), 71 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
index 07af24f963ace..584d665da8f6b 100644
--- a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -14,12 +14,11 @@ REGISTER_MAP_WITH_PROGRAMSTATE(LifetimeBoundMap, SymbolRef, LifetimeSourceSet)
 
 REGISTER_MAP_WITH_PROGRAMSTATE(LifetimeBoundMapVal, const MemRegion *,
                                LifetimeSourceSet)
-REGISTER_SET_WITH_PROGRAMSTATE(DeadSourceSet, const MemRegion *)
+REGISTER_SET_WITH_PROGRAMSTATE(DeallocatedSourceSet, const MemRegion *)
 
 namespace {
 class LifetimeAnnotations
-    : public Checker<check::PostCall, check::EndFunction, check::LifetimeEnd,
-                     check::Location> {
+    : public Checker<check::PostCall, check::EndFunction, check::Location> {
 public:
   void checkPostCall(const CallEvent &Call, CheckerContext &C) const;
   void printState(raw_ostream &Out, ProgramStateRef State, const char *NL,
@@ -36,9 +35,9 @@ class LifetimeAnnotations
   template <typename MapTy, typename KeyTy>
   void checkReturnedBorrower(const MapTy &Map, const KeyTy RetKey,
                              ProgramStateRef State, CheckerContext &C) const;
-  void reportDanglingBorrower(const LifetimeSourceSet *Sources, CheckerContext &C) const;
+  void reportDanglingBorrower(const LifetimeSourceSet *Sources,
+                              CheckerContext &C) const;
   void checkEndFunction(const ReturnStmt *RS, CheckerContext &C) const;
-  void checkLifetimeEnd(const VarDecl *VD, CheckerContext &C) const;
   void checkLocation(SVal Loc, bool IsLoad, const Stmt *S,
                      CheckerContext &C) const;
   void checkDeadSymbols(SymbolReaper &SymReaper, CheckerContext &C) const;
@@ -105,9 +104,11 @@ void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
 bool LifetimeAnnotations::hasDanglingSource(const MemRegion *Source,
                                             ProgramStateRef State,
                                             CheckerContext &C) const {
-  // FIXME: Currently the checker only focuses on stack MemRegions only since
-  // that is the scope of week 3. Sources without a stack region are not
-  // covered, but should be implemented as well next step.
+  // FIXME: The checker currently handles stack-region sources. Other
+  // region kinds require separate methodology. For example, heap
+  // regions do not go out of scope at the end of a stack frame, so
+  // in order to detect those type of dangling sources the function
+  // needs to be expanded to an event-driven approach as well.
   if (const auto *StackSpace =
           Source->getMemorySpaceAs<StackSpaceRegion>(State)) {
     const StackFrame *SF = StackSpace->getStackFrame();
@@ -165,28 +166,16 @@ void LifetimeAnnotations::checkEndFunction(const ReturnStmt *RS,
     checkReturnedBorrower(LBMapVal, RetValRegion, State, C);
 }
 
-void LifetimeAnnotations::checkLifetimeEnd(const VarDecl *VD,
-                                           CheckerContext &C) const {
+void LifetimeAnnotations::reportDanglingBorrower(
+    const LifetimeSourceSet *Sources, CheckerContext &C) const {
   ProgramStateRef State = C.getState();
-  if (!VD)
-    return;
-
-  SVal SourceVal = State->getLValue(VD, C.getStackFrame());
-  if (const MemRegion *SourceRegion = SourceVal.getAsRegion()) {
-    State = State->add<DeadSourceSet>(SourceRegion);
-    C.addTransition(State);
-  }
-}
-
-void LifetimeAnnotations::reportDanglingBorrower(const LifetimeSourceSet *Sources, CheckerContext &C) const {
-    ProgramStateRef State = C.getState();
 
-    for (const MemRegion *Source : *Sources) {
-      if (State->contains<DeadSourceSet>(Source)) {
-        if (ExplodedNode *N = C.generateNonFatalErrorNode())
-          reportUseAfterScope(Source, N, C);
-      }
+  for (const MemRegion *Source : *Sources) {
+    if (State->contains<DeallocatedSourceSet>(Source)) {
+      if (ExplodedNode *N = C.generateNonFatalErrorNode())
+        reportUseAfterScope(Source, N, C);
     }
+  }
 }
 
 void LifetimeAnnotations::checkLocation(SVal Loc, bool IsLoad, const Stmt *S,
@@ -195,23 +184,14 @@ void LifetimeAnnotations::checkLocation(SVal Loc, bool IsLoad, const Stmt *S,
   auto LBMap = State->get<LifetimeBoundMap>();
   auto LBMapVal = State->get<LifetimeBoundMapVal>();
 
-  // FIXME: Because of the CFG::LifetimeEnd elements now the analyzer can
-  // reason about out-of-scope dangling pointer deref even if there is
-  // no annotations in the source code. This detection part of the detection
-  // should live in a separate checker.
-  if (const MemRegion *R = Loc.getAsRegion()) {
-    if (State->contains<DeadSourceSet>(R)) {
-      if (ExplodedNode *N = C.generateNonFatalErrorNode())
-        reportUseAfterScope(R, N, C);
-    }
-  }
-
   if (LBMap.isEmpty() && LBMapVal.isEmpty())
     return;
 
-  // FIXME: If a borrower has multiple bound sources the callback warns
-  // if any source has died. The callback should track which source the
-  // borrower actually points.
+  // FIXME: If a borrower has multiple bound sources the callback
+  // warns if any of the sources have died. PathDiagnosticVisitor
+  // should be used to trace and identify which annotated parameter
+  // recorded the binding. Attaching this information as path notes
+  // would make the diagnostics more useful to the user.
   if (SymbolRef LocSym = Loc.getAsSymbol(true)) {
     if (auto *SourceSet = State->get<LifetimeBoundMap>(LocSym))
       reportDanglingBorrower(SourceSet, C);
diff --git a/clang/test/Analysis/lifetime-bound.cpp b/clang/test/Analysis/lifetime-bound.cpp
index 252209d2a034e..f7b0155caa769 100644
--- a/clang/test/Analysis/lifetime-bound.cpp
+++ b/clang/test/Analysis/lifetime-bound.cpp
@@ -163,36 +163,6 @@ void no_return() {
   (void)p; // no-warning
 }
 
-// Use-after-scope dangling pointer dereference
-void caller_ten() {
-  int* p = nullptr;
-  {
-    int x = 1;
-    p = test_func(&x);
-  }
-  *p = 2; // expected-warning {{Use of 'x' after its lifetime ended}}
-}
-
-void out_of_scope_ptr() {
-  int *ptr = nullptr;
-  {
-    int n = 5;
-    ptr = &n;
-  }
-  *ptr = 3; // expected-warning {{Use of 'n' after its lifetime ended}}
-}
-
-void f() {
-  int* p;
-  {
-    int x = 1;
-    p = &x;
-  }
-  int y = 2;
-  p = &y;
-  *p = 3; // no-warning
-}
-
 int* g() {
   int i = 5;
   int* p = test_func(&i);



More information about the cfe-commits mailing list