Author: Kashika Akhouri Date: 2026-06-01T16:28:48+05:30 New Revision: 0059fe6596aee0052f9ba40188ee2cef27b93af0
URL: https://github.com/llvm/llvm-project/commit/0059fe6596aee0052f9ba40188ee2cef27b93af0 DIFF: https://github.com/llvm/llvm-project/commit/0059fe6596aee0052f9ba40188ee2cef27b93af0.diff LOG: [LifetimeSafety] Add support for lifetime capture_by (#196884) This PR implements support for the `[[clang::lifetime_capture_by(X)]]` attribute within the lifetime-safety analysis. The PR introduces a new helper in `FactGenerator.cpp` called `handleLifetimeCaptureBy` which detects `[[clang::lifetime_capture_by(X)]]` on parameters. If detected, the analyzer now generates an `OriginFlowFact` ensuring that captured dependencies are added to the capturer's state. The PR supports capture_by params and `this` and currently doesn't implement attributes on function declarations. Example: Integrate `[[clang::lifetimebound]]`: This existing Clang annotation is crucial for specifying that the lifetime of a function's output is tied to one of its inputs. ```cpp void setS(std::string_view& in, std::string_view out [[clang::lifetime_capture_by(X)]]); std::string_view foo() { std::string a = "on stack"; std::string_view res; setS(res, a); return res; // warning: returning address of local variable 'a'. } ``` Lifetime Facts: ```cpp Function: foo Block B2: End of Block Block B1: OriginFlow: Dest: 0 (Expr: ImplicitCastExpr, Type : const char *) Src: 1 (Expr: StringLiteral, Type : const char (&)[9]) Use (0 (Expr: ImplicitCastExpr, Type : const char *), Read) Use (2 (Expr: CXXDefaultArgExpr, Type : const class std::allocator<char> &), Read) Expire (MaterializeTemporaryExpr at 0x562afa69fdb0) OriginFlow: Dest: 3 (Decl: res, Type : std::string_view) Src: 4 (Expr: CXXConstructExpr, Type : std::string_view) Use (3 (Decl: res, Type : std::string_view), Read) Issue (0 (Path: res), ToOrigin: 7 (Expr: DeclRefExpr, Decl: res)) Issue (1 (Path: a), ToOrigin: 8 (Expr: DeclRefExpr, Decl: a)) OriginFlow: Dest: 9 (Expr: ImplicitCastExpr, Type : const class std::basic_string<char> &) Src: 8 (Expr: DeclRefExpr, Decl: a) Use (9 (Expr: ImplicitCastExpr, Type : const class std::basic_string<char> &), Read) OriginFlow: Dest: 10 (Expr: CXXMemberCallExpr, Type : __sv_type) Src: 9 (Expr: ImplicitCastExpr, Type : const class std::basic_string<char> &) OriginFlow: Dest: 11 (Expr: ImplicitCastExpr, Type : __sv_type) Src: 10 (Expr: CXXMemberCallExpr, Type : __sv_type) Use (11 (Expr: ImplicitCastExpr, Type : __sv_type), Read) // New Origin Flow fact added OriginFlow: Dest: 3 (Decl: res, Type : std::string_view) Src: 11 (Expr: ImplicitCastExpr, Type : __sv_type), Merge Use (3 (Decl: res, Type : std::string_view), Read) Issue (2 (Path: res), ToOrigin: 12 (Expr: DeclRefExpr, Decl: res)) OriginFlow: Dest: 13 (Expr: ImplicitCastExpr, Type : const std::string_view &) Src: 12 (Expr: DeclRefExpr, Decl: res) OriginFlow: Dest: 14 (Expr: ImplicitCastExpr, Type : std::string_view) Src: 3 (Decl: res, Type : std::string_view) OriginFlow: Dest: 15 (Expr: CXXConstructExpr, Type : std::string_view) Src: 14 (Expr: ImplicitCastExpr, Type : std::string_view) Expire (a) Expire (res, Origin: 3 (Decl: res, Type : std::string_view)) OriginEscapes (15 (Expr: CXXConstructExpr, Type : std::string_view), via Return) End of Block Block B0: End of Block ``` Added: Modified: clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp clang/test/Sema/warn-lifetime-safety.cpp Removed: ################################################################################ diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h index 0b6d8c4fc026e..8f0670728bae9 100644 --- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h +++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h @@ -100,6 +100,11 @@ class FactsGenerator : public ConstStmtVisitor<FactsGenerator> { void handleMovedArgsInCall(const FunctionDecl *FD, ArrayRef<const Expr *> Args); + // Handles [[clang::lifetime_capture_by(X)]] annotations on a function call to + // create flow facts from captured arguments to the capturer + void handleLifetimeCaptureBy(const FunctionDecl *FD, + ArrayRef<const Expr *> Args); + /// Checks if a call-like expression creates a borrow by passing a value to a /// reference parameter, creating an IssueFact if it does. /// \param IsGslConstruction True if this is a GSL construction where all diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp index b34a04f1f3bda..17617fc92a7e8 100644 --- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp +++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp @@ -878,6 +878,66 @@ void FactsGenerator::handleImplicitObjectFieldUses(const Expr *Call, }); } +void FactsGenerator::handleLifetimeCaptureBy(const FunctionDecl *FD, + ArrayRef<const Expr *> Args) { + if (Args.empty()) + return; + // FIXME: Add support for capture_by on constructors. + if (isa<CXXConstructorDecl>(FD)) + return; + const auto *Method = dyn_cast<CXXMethodDecl>(FD); + bool IsInstance = + Method && Method->isInstance() && !isa<CXXConstructorDecl>(FD); + auto getArgCaptureBy = [FD, + IsInstance](unsigned I) -> LifetimeCaptureByAttr * { + const ParmVarDecl *PVD = nullptr; + if (IsInstance) { + // FIXME: Add support for I == 0 i.e. capture_by on function declarations + if (I > 0 && I - 1 < FD->getNumParams()) + PVD = FD->getParamDecl(I - 1); + } else { + if (I < FD->getNumParams()) + PVD = FD->getParamDecl(I); + } + return PVD ? PVD->getAttr<LifetimeCaptureByAttr>() : nullptr; + }; + for (unsigned I = 0; I < Args.size(); ++I) { + const LifetimeCaptureByAttr *Attr = getArgCaptureBy(I); + if (!Attr) + continue; + OriginList *CapturedOriginList = getOriginsList(*Args[I]); + if (!CapturedOriginList) + continue; + if (!CapturedOriginList) + continue; + for (int CapturingArgIdx : Attr->params()) { + // FIXME: Add support for capturing to Global/unknown. + if (CapturingArgIdx == LifetimeCaptureByAttr::Global || + CapturingArgIdx == LifetimeCaptureByAttr::Unknown || + CapturingArgIdx == LifetimeCaptureByAttr::Invalid) + continue; + ArrayRef<const Expr *> CallArgs = IsInstance ? Args.drop_front() : Args; + const Expr *CapturedByArg = + (CapturingArgIdx == LifetimeCaptureByAttr::This) + ? Args[0] + : CallArgs[CapturingArgIdx]; + assert(CapturedByArg && "Capturer expression must be valid"); + + OriginList *CapturingOriginList = getOriginsList(*CapturedByArg); + OriginList *Dest = getRValueOrigins(CapturedByArg, CapturingOriginList); + if (!Dest) + continue; + // KillDest=false because we cannot know if previous captures are being + // replaced or accumulated. Multiple successive captures into the same + // destination must all be tracked, so captured lifetimes are always + // merged. + CurrentBlockFacts.push_back(FactMgr.createFact<OriginFlowFact>( + Dest->getOuterOriginID(), CapturedOriginList->getOuterOriginID(), + /*KillDest=*/false)); + } + } +} + void FactsGenerator::handleFunctionCall(const Expr *Call, const FunctionDecl *FD, ArrayRef<const Expr *> Args, @@ -894,6 +954,7 @@ void FactsGenerator::handleFunctionCall(const Expr *Call, handleDestructiveCall(Call, FD, Args); handleMovedArgsInCall(FD, Args); handleImplicitObjectFieldUses(Call, FD); + handleLifetimeCaptureBy(FD, Args); if (!CallList) return; if (isStdReferenceCast(FD)) { diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp index 06097d4600af5..8209ce7dda73b 100644 --- a/clang/test/Sema/warn-lifetime-safety.cpp +++ b/clang/test/Sema/warn-lifetime-safety.cpp @@ -3420,3 +3420,163 @@ namespace GH191954 { return std::move(f); } } // namespace GH191954 + +//===----------------------------------------------------------------------===// +// lifetime-capture-by +//===----------------------------------------------------------------------===// + +void setCaptureBy(View& res, View in [[clang::lifetime_capture_by(res)]]); + +void use_after_free_capture_by() { + View res; + { + MyObj a; + setCaptureBy(res, a); // expected-warning {{local variable 'a' does not live long enough}} + } // expected-note {{destroyed here}} + (void)res; // expected-note {{later used here}} +} + +View use_after_return_capture_by() { + MyObj a; + View res; + setCaptureBy(res, a); // expected-warning {{stack memory associated with local variable 'a' is returned}} + return res; // expected-note {{returned here}} + +} + +void transitive_capture() { + View v1, v2; + { + MyObj local; + setCaptureBy(v1, local); // expected-warning {{local variable 'local' does not live long enough}} + setCaptureBy(v2, v1); + } // expected-note {{destroyed here}} + (void)v2; // expected-note {{later used here}} +} + +void set1(View& res, const MyObj& in [[clang::lifetime_capture_by(res)]]); + +void test_reference_to_view() { + View v; + { + MyObj local; + set1(v, local); // expected-warning {{local variable 'local' does not live long enough}} + } // expected-note {{destroyed here}} + (void)v; // expected-note {{later used here}} +} + +// FIXME: Add special handling for multi-level pointers and lvalue expressions which are not DeclRefExpr. +void set2(MyObj** res, const MyObj& in [[clang::lifetime_capture_by(res)]]); + +void test_pointer_to_pointer() { + MyObj *ptr = nullptr; + { + MyObj local; + set2(&ptr, local); + } + (void)ptr; +} + +void test_pointer_to_pointer_2(MyObj **ptr) { + { + MyObj local; + set2(ptr, local); + } + (void)ptr; +} + +void set3(MyObj*& res, const MyObj& in [[clang::lifetime_capture_by(res)]]); + +void test_reference_to_pointer() { + MyObj *ptr = nullptr; + { + MyObj local; + set3(ptr, local); // expected-warning {{local variable 'local' does not live long enough}} + } // expected-note {{destroyed here}} + (void)ptr; // expected-note {{later used here}} +} + +struct [[gsl::Pointer]] MyContainer { + View stored; + void set(View s [[clang::lifetime_capture_by(this)]]); +}; + +void member_capture() { + MyContainer c; + { + MyObj local; + c.set(local); // expected-warning {{local variable 'local' does not live long enough}} + } // expected-note {{destroyed here}} + (void)c.stored; // expected-note {{later used here}} +} + +// FIXME: Add support for simple containers without annotations. +struct SimpleContainer { + View stored; + void set(View s [[clang::lifetime_capture_by(this)]]); +}; + +void member_capture_simple_container() { + SimpleContainer c; + { + MyObj local; + c.set(local); + } + (void)c.stored; +} + +void captureTwo(View& into, + View a [[clang::lifetime_capture_by(into)]], + View b [[clang::lifetime_capture_by(into)]]); + +void multiple_captures() { + View res; + MyObj val1; + { + MyObj val2; + captureTwo(res, val1, val2); // expected-warning {{local variable 'val2' does not live long enough}} + } // expected-note {{destroyed here}} + (void)res; // expected-note {{later used here}} +} + +void multiple_local_captures() { + View res; + { + MyObj val1; + MyObj val2; + captureTwo(res, val1, val2); // expected-warning {{local variable 'val1' does not live long enough}} // expected-warning {{local variable 'val2' does not live long enough}} + } // expected-note 2 {{destroyed here}} + (void)res; // expected-note 2 {{later used here}} +} + +void captureIntoTwo(View& v1, View& v2, + View s [[clang::lifetime_capture_by(v1, v2)]]); + +void captured_by_multiple_params() { + View v1, v2; + { + MyObj local; + captureIntoTwo(v1, v2, local); // expected-warning {{local variable 'local' does not live long enough}} + } // expected-note {{destroyed here}} + (void)v1; // expected-note {{later used here}} +} + +void captured_by_multiple_params_2() { + View v1, v2; + { + MyObj local; + captureIntoTwo(v1, v2, local); // expected-warning {{local variable 'local' does not live long enough}} + } // expected-note {{destroyed here}} + (void)v2; // expected-note {{later used here}} +} + +void capturing_multiple_locals() { + View v; + { + MyObj local1; + setCaptureBy(v, local1); // expected-warning{{local variable 'local1' does not live long enough}} + MyObj local2; + setCaptureBy(v, local2); // expected-warning{{local variable 'local2' does not live long enough}} + } // expected-note 2 {{destroyed here}} + (void)v; // expected-note 2 {{later used here}} +} \ No newline at end of file _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
