[clang] 0059fe6 - [LifetimeSafety] Add support for lifetime capture_by (#196884)
via cfe-commits
cfe-commits at lists.llvm.org
Mon Jun 1 03:58:53 PDT 2026
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
More information about the cfe-commits
mailing list