Author: Benedek Kaibas Date: 2026-08-03T13:50:34+02:00 New Revision: 6a7f6a02bb95a75c615cde3194ad77d776b869c5
URL: https://github.com/llvm/llvm-project/commit/6a7f6a02bb95a75c615cde3194ad77d776b869c5 DIFF: https://github.com/llvm/llvm-project/commit/6a7f6a02bb95a75c615cde3194ad77d776b869c5.diff LOG: [analyzer] Implement BugReporterVisitor for UseAfterLifetimeEnd to trace lifetime source binding (#207052) Currently the `UseAfterLifetimeEnd` checker can emit warnings, but those warnings cannot clearly describe to which annotated parameter the return value is actually bound. When multiple parameters are annotated, it is unclear which one the return value is bound to. Using `BugReporterVisitor` to trace back the nodes and emit a note that explains where the lifetime of the annotated parameter (the source) ended can be helpful for users. ***NOTE***: This PR is built on #205951. It should only be merged after #205951 is merged. Consider the following case: ```cpp #include <stddef.h> class Arena { char buf[128]; char *buffer = buf; size_t offset = 0; public: void *allocate(size_t size) [[clang::lifetimebound]] { void *p = buffer + offset; offset += size; return p; } void reset() {offset = 0;} }; void *arena_dangling() { Arena arena; void *p = arena.allocate(128); arena.reset(); return p; // arena goes out of scope therefore p dangles } ``` The `UseAfterLifetimeEnd` checker correctly detects this error and emits path notes that trace where the value was bound and where its lifetime ends: ```text temp.cpp:21:3: warning: Returning value bound to 'arena' that will go out of scope [alpha.cplusplus.UseAfterLifetimeEnd] 21 | return p; | ^~~~~~~~ temp.cpp:19:13: note: Value bound to 'arena' here 19 | void *p = arena.allocate(128); | ^~~~~~~~~~~~~~~~~~~ temp.cpp:21:3: note: Lifetime of 'arena' ended here 21 | return p; | ^~~~~~~~ 1 warning generated. ``` The motivating example comes from here: https://discourse.llvm.org/t/clang-static-analyzer-gsoc-2025-teach-the-clang-static-analyzer-to-understand-lifetime-annotations/84487/41?u=bkaibas01 Added: Modified: clang/lib/StaticAnalyzer/Checkers/LifetimeModeling.cpp clang/lib/StaticAnalyzer/Checkers/LifetimeModeling.h clang/lib/StaticAnalyzer/Checkers/UseAfterLifetimeEnd.cpp clang/test/Analysis/lifetime-bound.cpp Removed: ################################################################################ diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeModeling.cpp b/clang/lib/StaticAnalyzer/Checkers/LifetimeModeling.cpp index 4b6d76a09575a..7b9fb7acb21ab 100644 --- a/clang/lib/StaticAnalyzer/Checkers/LifetimeModeling.cpp +++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeModeling.cpp @@ -71,6 +71,11 @@ std::vector<const MemRegion *> lifetime_modeling::getDanglingRegionsAfterReturn( return Regions; } +bool lifetime_modeling::isBoundToLifetimeSource(ProgramStateRef State, + SVal Val) { + return State->get<LifetimeBoundMap>(Val) != nullptr; +} + bool lifetime_modeling::isDeallocated(ProgramStateRef State, const MemRegion *Region) { return State->contains<DeallocatedSourceSet>(Region->getBaseRegion()); diff --git a/clang/lib/StaticAnalyzer/Checkers/LifetimeModeling.h b/clang/lib/StaticAnalyzer/Checkers/LifetimeModeling.h index 1e2ef8f7810ac..8d6c8e4882d1c 100644 --- a/clang/lib/StaticAnalyzer/Checkers/LifetimeModeling.h +++ b/clang/lib/StaticAnalyzer/Checkers/LifetimeModeling.h @@ -16,6 +16,9 @@ getDanglingRegionsAfterReturn(SVal Source, ProgramStateRef State, /// Returns true if the underlying MemRegion is deallocated. bool isDeallocated(ProgramStateRef State, const MemRegion *Region); +/// Returns true if \p Val is a key in the LifetimeBoundMap. +bool isBoundToLifetimeSource(ProgramStateRef State, SVal Val); + /// Returns the descriptive name of the memory region or a placeholder if a /// descriptive name cannot be constructed for it. std::string getRegionName(const MemRegion *Reg); diff --git a/clang/lib/StaticAnalyzer/Checkers/UseAfterLifetimeEnd.cpp b/clang/lib/StaticAnalyzer/Checkers/UseAfterLifetimeEnd.cpp index 0f320eb910930..a9065352adae6 100644 --- a/clang/lib/StaticAnalyzer/Checkers/UseAfterLifetimeEnd.cpp +++ b/clang/lib/StaticAnalyzer/Checkers/UseAfterLifetimeEnd.cpp @@ -15,8 +15,52 @@ class UseAfterLifetimeEnd : public Checker<check::EndFunction> { const BugType BugMsg{this, "UseAfterLifetimeEnd", "LifetimeBound"}; }; +class UseAfterLifetimeEndBRVisitor : public BugReporterVisitor { + SVal BoundVal; + const MemRegion *SourceRegion; + +public: + explicit UseAfterLifetimeEndBRVisitor(SVal Val, const MemRegion *Source) + : BoundVal(Val), SourceRegion(Source) {} + + void Profile(llvm::FoldingSetNodeID &ID) const override { + static int X = 0; + ID.AddPointer(&X); + BoundVal.Profile(ID); + SourceRegion->Profile(ID); + } + + PathDiagnosticPieceRef VisitNode(const ExplodedNode *N, + BugReporterContext &BRC, + PathSensitiveBugReport &BR) override; + PathDiagnosticPieceRef getEndPath(const ExplodedNode *N, + BugReporterContext &BRC, + PathSensitiveBugReport &BR) override; + PathDiagnosticPieceRef createSourcePiece(const ExplodedNode *N, + BugReporterContext &BRC, + StringRef Message) const; +}; + } // namespace +static const Expr *getLifetimeBoundArg(const Expr *RetExpr) { + const CallExpr *Expr = dyn_cast_or_null<CallExpr>(RetExpr); + if (!Expr) + return nullptr; + const FunctionDecl *FD = Expr->getDirectCallee(); + if (!FD) + return nullptr; + + for (const ParmVarDecl *PVD : FD->parameters()) { + if (PVD->hasAttr<LifetimeBoundAttr>()) { + unsigned Idx = PVD->getFunctionScopeIndex(); + if (Idx < Expr->getNumArgs()) + return Expr->getArg(Idx); + } + } + return nullptr; +} + void UseAfterLifetimeEnd::checkEndFunction(const ReturnStmt *RS, CheckerContext &C) const { if (!RS) @@ -43,6 +87,12 @@ void UseAfterLifetimeEnd::checkEndFunction(const ReturnStmt *RS, } } +static SourceRange getRegionDeclRange(const MemRegion *Source) { + if (const auto *VR = dyn_cast_or_null<VarRegion>(Source)) + return VR->getDecl()->getSourceRange(); + return SourceRange(); +} + void UseAfterLifetimeEnd::reportDanglingSource(const MemRegion *Source, SVal RetVal, ExplodedNode *N, CheckerContext &C) const { @@ -51,10 +101,68 @@ void UseAfterLifetimeEnd::reportDanglingSource(const MemRegion *Source, (llvm::Twine("Returning value bound to ") + lifetime_modeling::getRegionName(Source) + " that will go out of scope"), N); + + if (SourceRange Range = getRegionDeclRange(Source); Range.isValid()) + BR->addRange(Range); + + BR->addVisitor<UseAfterLifetimeEndBRVisitor>(RetVal, Source); bugreporter::trackStoredValue(RetVal, Source, *BR); C.emitReport(std::move(BR)); } +PathDiagnosticPieceRef UseAfterLifetimeEndBRVisitor::createSourcePiece( + const ExplodedNode *N, BugReporterContext &BRC, StringRef Message) const { + const Stmt *S = N->getStmtForDiagnostics(); + if (!S) + return nullptr; + + const Expr *RetExpr = dyn_cast_or_null<Expr>(S); + const Expr *Arg = getLifetimeBoundArg(RetExpr); + + PathDiagnosticLocation Pos; + + Pos = PathDiagnosticLocation(Arg ? Arg : S, BRC.getSourceManager(), + N->getStackFrame()); + + auto Note = std::make_shared<PathDiagnosticEventPiece>(Pos, Message, true); + if (SourceRange Range = getRegionDeclRange(SourceRegion); Range.isValid()) + Note->addRange(Range); + + return Note; +} + +PathDiagnosticPieceRef +UseAfterLifetimeEndBRVisitor::VisitNode(const ExplodedNode *N, + BugReporterContext &BRC, + PathSensitiveBugReport &BR) { + const ExplodedNode *Pred = N->getFirstPred(); + if (!Pred) + return nullptr; + + if (!lifetime_modeling::isBoundToLifetimeSource(N->getState(), BoundVal) || + lifetime_modeling::isBoundToLifetimeSource(Pred->getState(), BoundVal)) + return nullptr; + + auto Piece = createSourcePiece( + N, BRC, + (llvm::Twine("Value's lifetime bound to the lifetime of ") + + lifetime_modeling::getRegionName(SourceRegion) + " here") + .str()); + return Piece; +} + +PathDiagnosticPieceRef +UseAfterLifetimeEndBRVisitor::getEndPath(const ExplodedNode *N, + BugReporterContext &BRC, + PathSensitiveBugReport &BR) { + auto Piece = createSourcePiece( + N, BRC, + (llvm::Twine("Lifetime of ") + + lifetime_modeling::getRegionName(SourceRegion) + " ended here") + .str()); + return Piece; +} + void ento::registerUseAfterLifetimeEnd(CheckerManager &Mgr) { Mgr.registerChecker<UseAfterLifetimeEnd>(); } diff --git a/clang/test/Analysis/lifetime-bound.cpp b/clang/test/Analysis/lifetime-bound.cpp index ef8ffeb87a8dd..06c6ee9a4dd3a 100644 --- a/clang/test/Analysis/lifetime-bound.cpp +++ b/clang/test/Analysis/lifetime-bound.cpp @@ -1,7 +1,6 @@ // RUN: %clang_analyze_cc1 -analyzer-checker=core,alpha.cplusplus.UseAfterLifetimeEnd,debug.DebugLifetimeModeling \ // RUN: -analyzer-config cfg-lifetime=true -analyzer-output=text -verify %s -// RUN: %clang_analyze_cc1 -analyzer-checker=core,alpha.cplusplus.UseAfterLifetimeEnd,debug.DebugLifetimeModeling \ -// RUN: -analyzer-config c++-container-inlining=false -analyzer-config cfg-lifetime=true -analyzer-output=text -verify %s + struct A {}; struct Pair { @@ -100,7 +99,7 @@ void caller_five() { clang_analyzer_dumpLifetimeOriginsOf(s); // expected-warning@-1 {{Origin '&n' bound to 'n'}} - // expected-note@-2 {{Origin '&n' bound to 'n'}} + // expected-note@-2 {{Origin '&n' bound to 'n'}} } // Free function with both annotated and non-annotated parameters. @@ -179,19 +178,20 @@ int *test_func(int *p [[clang::lifetimebound]]); int *direct_return() { - int i = 5; //expected-note {{'i' initialized here}} + int i = 5; // expected-note {{'i' initialized here}} return test_func(&i); // expected-warning@-1 {{Returning value bound to 'i' that will go out of scope}} // expected-warning@-2 {{address of stack memory associated with local variable 'i' returned}} - // expected-note@-3 {{Returning value bound to 'i' that will go out of scope}} + // expected-note@-3 {{Value's lifetime bound to the lifetime of 'i' here}} + // expected-note@-4 {{Lifetime of 'i' ended here}} } int *variable_return() { int y = 5; // expected-note {{'y' initialized here}} - int *p = test_func(&y); + int *p = test_func(&y); // expected-note {{Value's lifetime bound to the lifetime of 'y' here}} return p; // expected-warning@-1 {{Returning value bound to 'y' that will go out of scope}} - // expected-note@-2 {{Returning value bound to 'y' that will go out of scope}} + // expected-note@-2 {{Lifetime of 'y' ended here}} } int *borrow_from_caller(int *b [[clang::lifetimebound]]) { @@ -220,11 +220,13 @@ int &dangling_sources_ref() { // expected-note@-2 {{'y' initialized here}} return multi_param_test_ref(x, y); // expected-warning@-1 {{Returning value bound to 'x' that will go out of scope}} - // expected-note@-2 {{Returning value bound to 'x' that will go out of scope}} - // expected-warning@-3 {{Returning value bound to 'y' that will go out of scope}} - // expected-note@-4 {{Returning value bound to 'y' that will go out of scope}} - // expected-warning@-5 {{reference to stack memory associated with local variable 'x' returned}} - // expected-warning@-6 {{reference to stack memory associated with local variable 'y' returned}} + // expected-warning@-2 {{Returning value bound to 'y' that will go out of scope}} + // expected-warning@-3 {{reference to stack memory associated with local variable 'x' returned}} + // expected-warning@-4 {{reference to stack memory associated with local variable 'y' returned}} + // expected-note@-5 {{Value's lifetime bound to the lifetime of 'x' here}} + // expected-note@-6 {{Value's lifetime bound to the lifetime of 'y' here}} + // expected-note@-7 {{Lifetime of 'x' ended here}} + // expected-note@-8 {{Lifetime of 'y' ended here}} } // Return value bound to annotated parameters (no dangling sources). @@ -234,11 +236,12 @@ int &no_dangling_sources_ref(int &a [[clang::lifetimebound]], int &b [[clang::li // Return value bound to annotated parameters (one dangling source). int &one_dangling_source_ref(int &a [[clang::lifetimebound]]) { - int x = 1; // expected-note {{'x' initialized here}} + int x = 1; // expected-note {{'x' initialized here}} return multi_param_test_ref(a, x); // expected-warning@-1 {{Returning value bound to 'x' that will go out of scope}} - // expected-note@-2 {{Returning value bound to 'x' that will go out of scope}} - // expected-warning@-3 {{reference to stack memory associated with local variable 'x' returned}} + // expected-warning@-2 {{reference to stack memory associated with local variable 'x' returned}} + // expected-note@-3 {{Value's lifetime bound to the lifetime of 'x' here}} + // expected-note@-4 {{Lifetime of 'x' ended here}} } int *multi_param_test_ptr(int *a [[clang::lifetimebound]], int *b [[clang::lifetimebound]]); @@ -252,9 +255,11 @@ int *dangling_sources_ptr() { int *y_ptr = &y; return multi_param_test_ptr(x_ptr, y_ptr); // expected-warning@-1 {{Returning value bound to 'x' that will go out of scope}} - // expected-note@-2 {{Returning value bound to 'x' that will go out of scope}} - // expected-warning@-3 {{Returning value bound to 'y' that will go out of scope}} - // expected-note@-4 {{Returning value bound to 'y' that will go out of scope}} + // expected-note@-2 {{Value's lifetime bound to the lifetime of 'x' here}} + // expected-note@-3 {{Lifetime of 'x' ended here}} + // expected-warning@-4 {{Returning value bound to 'y' that will go out of scope}} + // expected-note@-5 {{Value's lifetime bound to the lifetime of 'y' here}} + // expected-note@-6 {{Lifetime of 'y' ended here}} } // Return value bound to annotated parameters (no dangling sources). @@ -268,7 +273,8 @@ int *one_dangling_source_ptr(int *a [[clang::lifetimebound]]) { int *x_ptr = &x; return multi_param_test_ptr(a, x_ptr); // expected-warning@-1 {{Returning value bound to 'x' that will go out of scope}} - // expected-note@-2 {{Returning value bound to 'x' that will go out of scope}} + // expected-note@-2 {{Value's lifetime bound to the lifetime of 'x' here}} + // expected-note@-3 {{Lifetime of 'x' ended here}} } struct S { @@ -290,20 +296,23 @@ void outer() { int *danglingLocal() { S s; // expected-note {{'s' initialized here}} - return s.get(); // expected-note {{Returning value bound to 's' that will go out of scope}} + return s.get(); // expected-warning@-1 {{Returning value bound to 's' that will go out of scope}} - // expected-warning@-2 {{Address of stack memory associated with local variable 's' returned}} - // expected-note@-3 {{Address of stack memory associated with local variable 's' returned to caller}} - // expected-warning@-4 {{address of stack memory associated with local variable 's' returned}} + // expected-warning@-2 {{Address of stack memory associated with local variable 's' returned to caller}} + // expected-warning@-3 {{address of stack memory associated with local variable 's' returned}} + // expected-note@-4 {{Address of stack memory associated with local variable 's' returned to caller}} + // expected-note@-5 {{Value's lifetime bound to the lifetime of 's' here}} + // expected-note@-6 {{Lifetime of 's' ended here}} } int *danglingParam(S param) { return param.get(); // expected-warning@-1 {{Returning value bound to 'param' that will go out of scope}} - // expected-note@-2 {{Returning value bound to 'param' that will go out of scope}} - // expected-warning@-3 {{Address of stack memory associated with local variable 'param' returned}} + // expected-warning@-2 {{Address of stack memory associated with local variable 'param' returned to caller}} + // expected-warning@-3 {{address of stack memory associated with parameter 'param' returned}} // expected-note@-4 {{Address of stack memory associated with local variable 'param' returned to caller}} - // expected-warning@-5 {{address of stack memory associated with parameter 'param' returned}} + // expected-note@-5 {{Value's lifetime bound to the lifetime of 'param' here}} + // expected-note@-6 {{Lifetime of 'param' ended here}} } int *getFieldPtr(Pair &p [[clang::lifetimebound]]) { return &p.a; } @@ -312,10 +321,11 @@ int *field_subobject_dangling() { Pair pair{3, 5}; // expected-note {{'pair' initialized here}} return getFieldPtr(pair); // expected-warning@-1 {{Returning value bound to 'pair' that will go out of scope}} - // expected-note@-2 {{Returning value bound to 'pair' that will go out of scope}} - // expected-warning@-3 {{Address of stack memory associated with local variable 'pair' returned to caller}} + // expected-warning@-2 {{Address of stack memory associated with local variable 'pair' returned to caller}} + // expected-warning@-3 {{address of stack memory associated with local variable 'pair' returned}} // expected-note@-4 {{Address of stack memory associated with local variable 'pair' returned to caller}} - // expected-warning@-5 {{address of stack memory associated with local variable 'pair' returned}} + // expected-note@-5 {{Value's lifetime bound to the lifetime of 'pair' here}} + // expected-note@-6 {{Lifetime of 'pair' ended here}} } int *getBasePtr(Derived &d [[clang::lifetimebound]]) { @@ -326,10 +336,11 @@ int *base_subobject_dangling() { Derived derived{}; // expected-note {{'derived' initialized here}} return getBasePtr(derived); // expected-warning@-1 {{Returning value bound to 'derived' that will go out of scope}} - // expected-note@-2 {{Returning value bound to 'derived' that will go out of scope}} - // expected-warning@-3 {{Address of stack memory associated with local variable 'derived' returned to caller}} + // expected-warning@-2 {{Address of stack memory associated with local variable 'derived' returned to caller}} + // expected-warning@-3 {{address of stack memory associated with local variable 'derived' returned}} // expected-note@-4 {{Address of stack memory associated with local variable 'derived' returned to caller}} - // expected-warning@-5 {{address of stack memory associated with local variable 'derived' returned}} + // expected-note@-5 {{Value's lifetime bound to the lifetime of 'derived' here}} + // expected-note@-6 {{Lifetime of 'derived' ended here}} } int *getNestedFieldPtr(Outer &o [[clang::lifetimebound]]) { @@ -340,10 +351,11 @@ int *nested_subobject_dangling() { Outer outer{}; // expected-note {{'outer' initialized here}} return getNestedFieldPtr(outer); // expected-warning@-1 {{Returning value bound to 'outer' that will go out of scope}} - // expected-note@-2 {{Returning value bound to 'outer' that will go out of scope}} - // expected-warning@-3 {{Address of stack memory associated with local variable 'outer' returned to caller}} + // expected-warning@-2 {{Address of stack memory associated with local variable 'outer' returned to caller}} + // expected-warning@-3 {{address of stack memory associated with local variable 'outer' returned}} // expected-note@-4 {{Address of stack memory associated with local variable 'outer' returned to caller}} - // expected-warning@-5 {{address of stack memory associated with local variable 'outer' returned}} + // expected-note@-5 {{Value's lifetime bound to the lifetime of 'outer' here}} + // expected-note@-6 {{Lifetime of 'outer' ended here}} } int *getArrayElementPtr(Buffer &b [[clang::lifetimebound]]) { @@ -354,10 +366,11 @@ int *array_member_subobject_dangling() { Buffer buf{}; // expected-note {{'buf' initialized here}} return getArrayElementPtr(buf); // expected-warning@-1 {{Returning value bound to 'buf' that will go out of scope}} - // expected-note@-2 {{Returning value bound to 'buf' that will go out of scope}} - // expected-warning@-3 {{Address of stack memory associated with local variable 'buf' returned to caller}} + // expected-warning@-2 {{Address of stack memory associated with local variable 'buf' returned to caller}} + // expected-warning@-3 {{address of stack memory associated with local variable 'buf' returned}} // expected-note@-4 {{Address of stack memory associated with local variable 'buf' returned to caller}} - // expected-warning@-5 {{address of stack memory associated with local variable 'buf' returned}} + // expected-note@-5 {{Value's lifetime bound to the lifetime of 'buf' here}} + // expected-note@-6 {{Lifetime of 'buf' ended here}} } // FIXME: Heap allocated memory regions are not yet handled by the lifetime checkers. _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
