[clang] [analyzer] Implement LifetimeAnnotations checker. (PR #205521)
via cfe-commits
cfe-commits at lists.llvm.org
Wed Jun 24 03:14:38 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-clang-static-analyzer-1
@llvm/pr-subscribers-clang
Author: Benedek Kaibas (benedekaibas)
<details>
<summary>Changes</summary>
Implemented the `LifetimeAnnotations` checker which is responsible for detecting lifetime safety violations involving the `[[clang::lifetimebound]]` annotation. This checker is dependent on the `LifetimeModeling` checker which will be part of a future PR.
For detailed history of the work of this checker, please see: #<!-- -->200145
---
Full diff: https://github.com/llvm/llvm-project/pull/205521.diff
5 Files Affected:
- (modified) clang/include/clang/StaticAnalyzer/Checkers/Checkers.td (+11)
- (modified) clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt (+2)
- (added) clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp (+305)
- (added) clang/test/Analysis/debug-lifetime-bound.cpp (+10)
- (added) clang/test/Analysis/lifetime-bound.cpp (+171)
``````````diff
diff --git a/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td b/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td
index d02c3195069f3..5ba220ab6d60e 100644
--- a/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td
+++ b/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td
@@ -788,6 +788,11 @@ def SmartPtrChecker: Checker<"SmartPtr">,
Dependencies<[SmartPtrModeling]>,
Documentation<HasDocumentation>;
+def LifetimeAnnotations : Checker<"LifetimeAnnotations">,
+ HelpText<"Check for lifetime violations by incorporating lifetime "
+ "annotations into the analysis">,
+ Documentation<NotDocumented>;
+
} // end: "alpha.cplusplus"
//===----------------------------------------------------------------------===//
@@ -1576,6 +1581,12 @@ 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().">,
+ WeakDependencies<[LifetimeAnnotations]>,
+ Documentation<NotDocumented>;
+
} // end "debug"
diff --git a/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt b/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt
index 8a0621077b977..8363f345f4cc8 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
@@ -146,6 +147,7 @@ add_clang_library(clangStaticAnalyzerCheckers
clangAST
clangASTMatchers
clangAnalysis
+ clangAnalysisLifetimeSafety
clangBasic
clangLex
clangStaticAnalyzerCore
diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
new file mode 100644
index 0000000000000..e25d076dd0bd5
--- /dev/null
+++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeAnnotations.cpp
@@ -0,0 +1,305 @@
+#include "clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h"
+#include "clang/AST/Attrs.inc"
+#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"
+
+using namespace clang;
+using namespace ento;
+
+REGISTER_SET_FACTORY_WITH_PROGRAMSTATE(LifetimeSourceSet, const MemRegion *)
+REGISTER_MAP_WITH_PROGRAMSTATE(LifetimeBoundMap, SVal, LifetimeSourceSet)
+
+REGISTER_SET_WITH_PROGRAMSTATE(DeallocatedSourceSet, const MemRegion *)
+
+namespace {
+class LifetimeAnnotations
+ : public Checker<check::PostCall, check::EndFunction, check::Location,
+ check::DeadSymbols> {
+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 reportDanglingSource(const MemRegion *Region, ExplodedNode *N,
+ CheckerContext &C) const;
+ void reportUseAfterScope(const MemRegion *Region, ExplodedNode *N,
+ CheckerContext &C) const;
+
+ void checkReturnedBorrower(SVal Val, ProgramStateRef State,
+ CheckerContext &C) const;
+ void reportDanglingBorrower(const LifetimeSourceSet *Sources,
+ CheckerContext &C) const;
+ void checkEndFunction(const ReturnStmt *RS, 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"};
+};
+
+} // namespace
+
+static ProgramStateRef bindValues(ProgramStateRef State, SVal RetVal,
+ const MemRegion *Source) {
+ LifetimeSourceSet::Factory &F =
+ State->getStateManager().get_context<LifetimeSourceSet>();
+
+ const LifetimeSourceSet *LSet = State->get<LifetimeBoundMap>(RetVal);
+ LifetimeSourceSet Set = LSet ? *LSet : F.getEmptySet();
+ Set = F.add(Set, Source);
+ State = State->set<LifetimeBoundMap>(RetVal, Set);
+ return State;
+}
+
+void LifetimeAnnotations::checkPostCall(const CallEvent &Call,
+ CheckerContext &C) const {
+ ProgramStateRef State = C.getState();
+
+ const auto *FC = dyn_cast<AnyFunctionCall>(&Call);
+ if (!FC)
+ return;
+
+ const FunctionDecl *FD = FC->getDecl();
+ if (!FD)
+ return;
+
+ SVal RetVal = Call.getReturnValue();
+
+ 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, RetVal, ArgValRegion);
+ }
+ }
+
+ if (const auto *IC = dyn_cast<CXXInstanceCall>(&Call)) {
+ if (lifetimes::implicitObjectParamIsLifetimeBound(FD)) {
+ if (const MemRegion *AttrRegion = IC->getCXXThisVal().getAsRegion()) {
+ State = bindValues(State, RetVal, AttrRegion);
+ }
+ }
+ }
+ C.addTransition(State);
+}
+
+static bool hasDanglingSource(const MemRegion *Source, ProgramStateRef State,
+ CheckerContext &C) {
+ // 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();
+ const StackFrame *CurrentSF = C.getStackFrame();
+ if (SF == CurrentSF || !SF->isParentOf(CurrentSF))
+ return true;
+ }
+ return false;
+}
+
+void LifetimeAnnotations::checkReturnedBorrower(SVal Val, ProgramStateRef State,
+ CheckerContext &C) const {
+ if (auto *SourceSet = State->get<LifetimeBoundMap>(Val)) {
+ if (ExplodedNode *N = C.generateNonFatalErrorNode()) {
+ for (const MemRegion *Region : *SourceSet) {
+ if (hasDanglingSource(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>();
+
+ if (LBMap.isEmpty())
+ return;
+
+ const Expr *RetExpr = RS->getRetValue();
+ if (!RetExpr)
+ return;
+
+ RetExpr = RetExpr->IgnoreParens();
+ SVal RetVal = C.getSVal(RetExpr);
+ checkReturnedBorrower(RetVal, State, C);
+}
+
+void LifetimeAnnotations::reportDanglingBorrower(
+ const LifetimeSourceSet *Sources, CheckerContext &C) const {
+ ProgramStateRef State = C.getState();
+
+ ExplodedNode *N = C.generateNonFatalErrorNode();
+ if (!N)
+ return;
+
+ for (const MemRegion *Source : *Sources) {
+ if (State->contains<DeallocatedSourceSet>(Source)) {
+ reportUseAfterScope(Source, N, C);
+ }
+ }
+}
+
+void LifetimeAnnotations::checkLocation(SVal Loc, bool IsLoad, const Stmt *S,
+ CheckerContext &C) const {
+ ProgramStateRef State = C.getState();
+ auto LBMap = State->get<LifetimeBoundMap>();
+
+ if (LBMap.isEmpty())
+ return;
+
+ // 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 (auto *SourceSet = State->get<LifetimeBoundMap>(Loc))
+ reportDanglingBorrower(SourceSet, C);
+}
+
+void LifetimeAnnotations::reportDanglingSource(const MemRegion *Region,
+ ExplodedNode *N,
+ CheckerContext &C) const {
+ auto BR = std::make_unique<PathSensitiveBugReport>(
+ BugMsg,
+ (llvm::Twine("Returning value bound to '") + Region->getString() +
+ "' that will go out of scope")
+ .str(),
+ N);
+ C.emitReport(std::move(BR));
+}
+
+void LifetimeAnnotations::reportUseAfterScope(const MemRegion *Region,
+ ExplodedNode *N,
+ CheckerContext &C) const {
+ auto BR = std::make_unique<PathSensitiveBugReport>(
+ BugMsg,
+ (llvm::Twine("Use of '") + Region->getString() +
+ "' after its lifetime ended.")
+ .str(),
+ N);
+ C.emitReport(std::move(BR));
+}
+
+void LifetimeAnnotations::checkDeadSymbols(SymbolReaper &SymReaper,
+ CheckerContext &C) const {
+ ProgramStateRef State = C.getState();
+ LifetimeBoundMapTy LBMap = State->get<LifetimeBoundMap>();
+
+ DeallocatedSourceSetTy Sources = State->get<DeallocatedSourceSet>();
+
+ for (SVal Val : llvm::make_first_range(LBMap)) {
+ if (const MemRegion *ValRegion = Val.getAsRegion()) {
+ if (!SymReaper.isLiveRegion(ValRegion))
+ State = State->remove<LifetimeBoundMap>(Val);
+ } else if (SymbolRef ValRef =
+ Val.getAsSymbol(/*IncludeBaseRegions=*/true)) {
+ if (!SymReaper.isLive(ValRef))
+ State = State->remove<LifetimeBoundMap>(Val);
+ }
+ }
+
+ for (const MemRegion *Region : Sources) {
+ if (!SymReaper.isLiveRegion(Region))
+ State = State->remove<DeallocatedSourceSet>(Region);
+ }
+
+ C.addTransition(State);
+}
+
+void LifetimeAnnotations::printState(raw_ostream &Out, ProgramStateRef State,
+ const char *NL, const char *Sep) const {
+ auto LBMap = State->get<LifetimeBoundMap>();
+
+ if (LBMap.isEmpty())
+ return;
+
+ Out << Sep << "LifetimeBound bindings:" << NL;
+ for (auto &&[OriginSym, SourceSet] : LBMap) {
+ for (const auto *Region : SourceSet)
+ Out << " Origin " << OriginSym << " contains Loan " << Region << NL;
+ }
+}
+
+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());
+ if (!CE)
+ return false;
+
+ const FnCheck *Handler = Callbacks.lookup(Call);
+ if (!Handler)
+ return false;
+
+ (this->*(*Handler))(Call, C);
+ return true;
+}
+
+void DebugLifetimeAnnotations::analyzerLifetimeBound(const CallEvent &Call,
+ CheckerContext &C) const {
+
+ ProgramStateRef State = C.getState();
+ unsigned int ArgCount = Call.getNumArgs();
+ if (ArgCount != 1)
+ return;
+
+ SVal ArgSVal = Call.getArgSVal(0);
+
+ if (auto *SourceSet = State->get<LifetimeBoundMap>(ArgSVal)) {
+ if (ExplodedNode *N = C.generateNonFatalErrorNode()) {
+ for (const auto *Region : *SourceSet) {
+ llvm::SmallString<128> Str;
+ llvm::raw_svector_ostream OS(Str);
+ OS << " Origin " << ArgSVal << " bound to " << Region;
+ auto BR = std::make_unique<PathSensitiveBugReport>(BugMsg, OS.str(), N);
+ C.emitReport(std::move(BR));
+ }
+ }
+ }
+}
+
+void ento::registerLifetimeAnnotations(CheckerManager &mgr) {
+ mgr.registerChecker<LifetimeAnnotations>();
+}
+
+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/debug-lifetime-bound.cpp b/clang/test/Analysis/debug-lifetime-bound.cpp
new file mode 100644
index 0000000000000..e62c51ae6bc53
--- /dev/null
+++ b/clang/test/Analysis/debug-lifetime-bound.cpp
@@ -0,0 +1,10 @@
+// RUN: %clang_analyze_cc1 -analyzer-checker=core,alpha.cplusplus.LifetimeAnnotations,debug.DebugLifetimeAnnotations -verify %s
+
+// expected-no-diagnostics
+
+void clang_analyzer_lifetime_bound(int);
+
+void test() {
+ int x = 5;
+ clang_analyzer_lifetime_bound(x); // no-warning: verifies debug checker does not crash standalone
+}
diff --git a/clang/test/Analysis/lifetime-bound.cpp b/clang/test/Analysis/lifetime-bound.cpp
new file mode 100644
index 0000000000000..ca97419b63e53
--- /dev/null
+++ b/clang/test/Analysis/lifetime-bound.cpp
@@ -0,0 +1,171 @@
+// RUN: %clang_analyze_cc1 -analyzer-checker=core,alpha.cplusplus.LifetimeAnnotations,debug.DebugLifetimeAnnotations \
+// RUN: -analyzer-config cfg-lifetime=true -verify %s
+// RUN: %clang_analyze_cc1 -analyzer-checker=core,alpha.cplusplus.LifetimeAnnotations,debug.DebugLifetimeAnnotations \
+// RUN: -analyzer-config c++-container-inlining=false -analyzer-config cfg-lifetime=true -verify %s
+
+struct A {};
+
+void clang_analyzer_lifetime_bound(int*);
+void clang_analyzer_lifetime_bound(int&);
+void clang_analyzer_lifetime_bound(A*);
+void clang_analyzer_lifetime_bound(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 caller() {
+ int v = 0;
+ X obj;
+ int& r = obj.choose(v);
+ clang_analyzer_lifetime_bound(r); // expected-warning {{Origin &v bound to v}}
+}
+
+// Obj ref type function return annotated case
+struct Y {
+ A a;
+ A& getA() [[clang::lifetimebound]] { return 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}}
+}
+
+// Obj ptr type function return annotated case
+struct Z {
+ A a;
+ A* getA() [[clang::lifetimebound]] { return &a; }
+};
+
+void caller_three() {
+ Z z;
+ A* func = z.getA();
+ clang_analyzer_lifetime_bound(func); // expected-warning {{Origin &z.a bound to z}}
+}
+
+// Free function with annotated param and ref return
+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 {{Origin &num bound to num}}
+}
+
+// Free function with annotated param and ptr return
+int* boo(int* num [[clang::lifetimebound]]) { return num; }
+
+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}}
+}
+
+// Free function with both annotated and non-annotated parameters.
+int& fn(int& f, int& s [[clang::lifetimebound]]) { return s; }
+
+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}}
+}
+
+
+
+// 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 caller_seven() {
+ int y = 15;
+ int* y_ptr = &y;
+ auto* bind = foo(y_ptr);
+
+ clang_analyzer_lifetime_bound(bind); // expected-warning-re {{Origin &SymRegion{{.*}} bound to y}}
+}
+
+// Function returns a reference and has an annotated parameter
+int& func(int& some_number [[clang::lifetimebound]]);
+
+void caller_eight() {
+ int f = 15;
+ auto& bind = func(f);
+
+ clang_analyzer_lifetime_bound(bind); // expected-warning-re {{Origin &SymRegion{{.*}} bound to f}}
+}
+
+// Function returns a reference and has two annotated parameters.
+int& f(int& a [[clang::lifetimebound]], int& b [[clang::lifetimebound]]);
+
+void caller_nine() {
+ int first_num = 1;
+ int second_num = 2;
+ int& numbers = f(first_num, second_num);
+
+ clang_analyzer_lifetime_bound(numbers);
+ // expected-warning-re at -1 {{Origin &SymRegion{{.*}} bound to first_num}}
+ // expected-warning-re at -2 {{Origin &SymRegion{{.*}} bound to second_num}}
+}
+
+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
+}
+
+
+
+// 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 '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 '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
+}
+
+int* g() {
+ int i = 5;
+ int* p = test_func(&i);
+ (void)p;
+ return nullptr; // no-warning
+}
``````````
</details>
https://github.com/llvm/llvm-project/pull/205521
More information about the cfe-commits
mailing list