Author: Benedek Kaibas Date: 2026-06-29T01:25:49+02:00 New Revision: f3a69752ac184360afeb921d157ae08b89c2c091
URL: https://github.com/llvm/llvm-project/commit/f3a69752ac184360afeb921d157ae08b89c2c091 DIFF: https://github.com/llvm/llvm-project/commit/f3a69752ac184360afeb921d157ae08b89c2c091.diff LOG: [analyzer] Implement UseAfterLifetimeEnd checker (#205521) Implemented the UseAfterLifetimEnd checker which is responsible for detecting lifetime safety violations involving the [[clang::lifetimebound]] annotation. This checker can catch violations in annotated code such as dangling pointer/reference bound to local variables that go out of scope. This checker is one of the reporting checkers that depend on the LifetimeModeling checker #205951. To detect dangling sources the checker queries the state at function exit points through the checkEndFunction callback. This checker does not handle lifetime issues where the code is unannotated. Detailed work history of this checker can be found here: #200145 Co-authored-by: Balázs Benics <[email protected]> Added: clang/lib/StaticAnalyzer/Checkers/UseAfterLifetimeEnd.cpp clang/test/Analysis/debug-lifetime-bound.cpp clang/test/Analysis/lifetime-bound.cpp Modified: clang/include/clang/StaticAnalyzer/Checkers/Checkers.td clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt Removed: ################################################################################ diff --git a/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td b/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td index d02c3195069f3..b565481d28fdb 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 UseAfterLifetimeEnd : Checker<"UseAfterLifetimeEnd">, + HelpText<"Check for uses of references or pointers that " + "outlive their bound object">, + 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 DebugUseAfterLifetimeEnd : Checker<"DebugUseAfterLifetimeEnd">, + HelpText<"Prints the bindings recorded by the UseAfterLifetimeEnd checker. " + "Use with clang_analyzer_dumpLifetimeOriginsOf().">, + WeakDependencies<[UseAfterLifetimeEnd]>, + Documentation<NotDocumented>; + } // end "debug" diff --git a/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt b/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt index 8a0621077b977..46c0c36fda736 100644 --- a/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt +++ b/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt @@ -126,6 +126,7 @@ add_clang_library(clangStaticAnalyzerCheckers UninitializedObject/UninitializedPointee.cpp UnixAPIChecker.cpp UnreachableCodeChecker.cpp + UseAfterLifetimeEnd.cpp VforkChecker.cpp VLASizeChecker.cpp VAListChecker.cpp @@ -146,6 +147,7 @@ add_clang_library(clangStaticAnalyzerCheckers clangAST clangASTMatchers clangAnalysis + clangAnalysisLifetimeSafety clangBasic clangLex clangStaticAnalyzerCore diff --git a/clang/lib/StaticAnalyzer/Checkers/UseAfterLifetimeEnd.cpp b/clang/lib/StaticAnalyzer/Checkers/UseAfterLifetimeEnd.cpp new file mode 100644 index 0000000000000..db776f1c1c49d --- /dev/null +++ b/clang/lib/StaticAnalyzer/Checkers/UseAfterLifetimeEnd.cpp @@ -0,0 +1,258 @@ +#include "clang/AST/Attr.h" +#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" + +using namespace clang; +using namespace ento; + +REGISTER_SET_FACTORY_WITH_PROGRAMSTATE(LifetimeSourceSet, const MemRegion *) +REGISTER_MAP_WITH_PROGRAMSTATE(LifetimeBoundMap, SVal, LifetimeSourceSet) + +namespace { +class UseAfterLifetimeEnd + : public Checker<check::PostCall, check::EndFunction, 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 checkReturnedBorrower(SVal Val, ProgramStateRef State, + CheckerContext &C) const; + void checkEndFunction(const ReturnStmt *RS, CheckerContext &C) const; + void checkDeadSymbols(SymbolReaper &SymReaper, CheckerContext &C) const; + const BugType BugMsg{this, "UseAfterLifetimeEnd", "LifetimeBound"}; +}; + +} // namespace + +static ProgramStateRef bindValues(ProgramStateRef State, SVal RetVal, + const MemRegion *Source) { + LifetimeSourceSet::Factory &F = State->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 UseAfterLifetimeEnd::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 UseAfterLifetimeEnd::checkReturnedBorrower(SVal Val, ProgramStateRef State, + CheckerContext &C) const { + if (auto *SourceSet = State->get<LifetimeBoundMap>(Val)) { + ExplodedNode *N = nullptr; + for (const MemRegion *Region : *SourceSet) { + if (hasDanglingSource(Region, State, C)) { + if (!N) + N = C.generateNonFatalErrorNode(); + if (!N) + return; + reportDanglingSource(Region, N, C); + } + } + } +} + +void UseAfterLifetimeEnd::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 UseAfterLifetimeEnd::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"), + N); + C.emitReport(std::move(BR)); +} + +void UseAfterLifetimeEnd::checkDeadSymbols(SymbolReaper &SymReaper, + CheckerContext &C) const { + ProgramStateRef State = C.getState(); + LifetimeBoundMapTy LBMap = State->get<LifetimeBoundMap>(); + + 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); + } + } + + C.addTransition(State); +} + +void UseAfterLifetimeEnd::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 DebugUseAfterLifetimeEnd : public Checker<eval::Call> { +public: + bool evalCall(const CallEvent &Call, CheckerContext &C) const; + void analyzerDumpLifetimeOriginsOf(const CallEvent &Call, + CheckerContext &C) const; + + const BugType BugMsg{this, "DebugUseAfterLifetimeEnd", + "DebugUseAfterLifetimeEnd"}; + using FnCheck = void (DebugUseAfterLifetimeEnd::*)(const CallEvent &Call, + CheckerContext &C) const; + + const CallDescriptionMap<FnCheck> Callbacks = { + {{CDM::SimpleFunc, {"clang_analyzer_dumpLifetimeOriginsOf"}}, + &DebugUseAfterLifetimeEnd::analyzerDumpLifetimeOriginsOf}, + }; +}; + +} // namespace + +bool DebugUseAfterLifetimeEnd::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 DebugUseAfterLifetimeEnd::analyzerDumpLifetimeOriginsOf( + const CallEvent &Call, CheckerContext &C) const { + ProgramStateRef State = C.getState(); + + if (Call.getNumArgs() != 1) { + if (ExplodedNode *N = C.generateNonFatalErrorNode()) { + auto BR = std::make_unique<PathSensitiveBugReport>( + BugMsg, + "clang_analyzer_dumpLifetimeOriginsOf requires exactly 1 argument", + N); + C.emitReport(std::move(BR)); + } + return; + } + + SVal ArgSVal = Call.getArgSVal(0); + const LifetimeSourceSet *SourceSet = State->get<LifetimeBoundMap>(ArgSVal); + + if (!SourceSet) + return; + + if (ExplodedNode *N = C.generateNonFatalErrorNode()) { + llvm::SmallVector<std::string> RegionNames = + to_vector(map_range(llvm::make_pointee_range(*SourceSet), + std::mem_fn(&MemRegion::getString))); + llvm::sort(RegionNames); + + llvm::SmallString<128> Str; + llvm::raw_svector_ostream OS(Str); + OS << " Origin " << ArgSVal << " bound to "; + llvm::interleaveComma(RegionNames, OS); + C.emitReport(std::make_unique<PathSensitiveBugReport>(BugMsg, OS.str(), N)); + } +} + +void ento::registerUseAfterLifetimeEnd(CheckerManager &Mgr) { + Mgr.registerChecker<UseAfterLifetimeEnd>(); +} + +bool ento::shouldRegisterUseAfterLifetimeEnd(const CheckerManager &Mgr) { + return true; +} + +void ento::registerDebugUseAfterLifetimeEnd(CheckerManager &Mgr) { + Mgr.registerChecker<DebugUseAfterLifetimeEnd>(); +} + +bool ento::shouldRegisterDebugUseAfterLifetimeEnd(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..8ef704195dcc6 --- /dev/null +++ b/clang/test/Analysis/debug-lifetime-bound.cpp @@ -0,0 +1,11 @@ +// RUN: %clang_analyze_cc1 -analyzer-checker=core,alpha.cplusplus.UseAfterLifetimeEnd,debug.DebugUseAfterLifetimeEnd -verify %s + +// expected-no-diagnostics + +void clang_analyzer_dumpLifetimeOriginsOf(int); + +void test() { + int x = 5; + clang_analyzer_dumpLifetimeOriginsOf(x); // no-warning +} + diff --git a/clang/test/Analysis/lifetime-bound.cpp b/clang/test/Analysis/lifetime-bound.cpp new file mode 100644 index 0000000000000..e99299b5d03bf --- /dev/null +++ b/clang/test/Analysis/lifetime-bound.cpp @@ -0,0 +1,219 @@ +// RUN: %clang_analyze_cc1 -analyzer-checker=core,alpha.cplusplus.UseAfterLifetimeEnd,debug.DebugUseAfterLifetimeEnd \ +// RUN: -analyzer-config cfg-lifetime=true -verify %s +// RUN: %clang_analyze_cc1 -analyzer-checker=core,alpha.cplusplus.UseAfterLifetimeEnd,debug.DebugUseAfterLifetimeEnd \ +// RUN: -analyzer-config c++-container-inlining=false -analyzer-config cfg-lifetime=true -verify %s + +struct A {}; + +void clang_analyzer_dumpLifetimeOriginsOf(int*); +void clang_analyzer_dumpLifetimeOriginsOf(int&); +void clang_analyzer_dumpLifetimeOriginsOf(A*); +void clang_analyzer_dumpLifetimeOriginsOf(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_dumpLifetimeOriginsOf(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_dumpLifetimeOriginsOf(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_dumpLifetimeOriginsOf(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_dumpLifetimeOriginsOf(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_dumpLifetimeOriginsOf(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_dumpLifetimeOriginsOf(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_dumpLifetimeOriginsOf(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_dumpLifetimeOriginsOf(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_dumpLifetimeOriginsOf(numbers); // expected-warning-re {{Origin &SymRegion{{.*}} bound to first_num, second_num}} +} + +struct View { + int *p; +}; +View makeView(int &x [[clang::lifetimebound]]); + +void clang_analyzer_dumpLifetimeOriginsOf(View); + +void caller_view() { + int v = 42; + View w = makeView(v); + // FIXME: Currently none of the maps cover LazyCompoundVal. + clang_analyzer_dumpLifetimeOriginsOf(w); // no-warning +} + + + +// These are the test cases for testing the correctness of the emitted warning from the UseAfterLifetimeEnd 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@-1 {{Returning value bound to 'i' that will go out of scope}} + // expected-warning@-2 {{address of stack memory associated with local variable 'i' returned}} +} + +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 +} + +int &multi_param_test_ref(int &a [[clang::lifetimebound]], int &b [[clang::lifetimebound]]); + +// Return value bound to annotated parameters (two dangling sources). +int &dangling_sources_ref() { + int x = 1, y = 2; + return multi_param_test_ref(x, y); + // expected-warning@-1 {{Returning value bound to 'x' that will go out of scope}} + // expected-warning@-2 {{Returning value bound to 'y' that will go out of scope}} + // expected-warning@-3 {{reference to stack memory associated with local variable 'x' returned}} + // expected-warning@-4 {{reference to stack memory associated with local variable 'y' returned}} +} + +// Return value bound to annotated parameters (no dangling sources). +int &no_dangling_sources_ref(int &a [[clang::lifetimebound]], int &b [[clang::lifetimebound]]) { + return multi_param_test_ref(a, b); // no-warning +} + +// Return value bound to annotated parameters (one dangling source). +int &one_dangling_source_ref(int &a [[clang::lifetimebound]]) { + int x = 1; + return multi_param_test_ref(a, x); + // expected-warning@-1 {{Returning value bound to 'x' that will go out of scope}} + // expected-warning@-2 {{reference to stack memory associated with local variable 'x' returned}} +} + +int *multi_param_test_ptr(int *a [[clang::lifetimebound]], int *b [[clang::lifetimebound]]); + +// Return value bound to annotated parameters (two dangling sources). +int *dangling_sources_ptr() { + int x = 1, y = 2; + int *x_ptr = &x; + int *y_ptr = &y; + return multi_param_test_ptr(x_ptr, y_ptr); + // expected-warning@-1 {{Returning value bound to 'x' that will go out of scope}} + // expected-warning@-2 {{Returning value bound to 'y' that will go out of scope}} +} + +// Return value bound to annotated parameters (no dangling sources). +int *no_dangling_sources_ptr(int *a [[clang::lifetimebound]], int *b [[clang::lifetimebound]]) { + return multi_param_test_ptr(a, b); // no-warning +} + +// Return value bound to annotated parameters (one dangling source). +int *one_dangling_source_ptr(int *a [[clang::lifetimebound]]) { + int x = 1; + int *x_ptr = &x; + return multi_param_test_ptr(a, x_ptr); // expected-warning {{Returning value bound to 'x' that will go out of scope}} +} + _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
