[clang] [LifetimeSafety] Add support for lifetime capture_by (PR #196884)
Kashika Akhouri via cfe-commits
cfe-commits at lists.llvm.org
Thu May 21 03:29:14 PDT 2026
https://github.com/kashika0112 updated https://github.com/llvm/llvm-project/pull/196884
>From caf2b8db8a05245b2d085f9f26519cfeec8eeffd Mon Sep 17 00:00:00 2001
From: Kashika Akhouri <akhourik at google.com>
Date: Mon, 11 May 2026 06:40:43 +0000
Subject: [PATCH 1/3] Add support for lifetime capture_by
---
.../Analyses/LifetimeSafety/FactsGenerator.h | 5 ++
.../LifetimeSafety/FactsGenerator.cpp | 69 ++++++++++++++++++
clang/test/Sema/warn-lifetime-safety.cpp | 73 +++++++++++++++++++
3 files changed, 147 insertions(+)
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
index 766742e98101a..af545d5e9386c 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
@@ -91,6 +91,11 @@ class FactsGenerator : public ConstStmtVisitor<FactsGenerator> {
void handleMovedArgsInCall(const FunctionDecl *FD,
ArrayRef<const Expr *> Args);
+ // Detects [[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 dc80d2783fc03..2908a9f1dd924 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -864,6 +864,74 @@ void FactsGenerator::handleImplicitObjectFieldUses(const Expr *Call,
});
}
+void FactsGenerator::handleLifetimeCaptureBy(const FunctionDecl *FD,
+ ArrayRef<const Expr *> Args) {
+ bool isInstance = false;
+ if (const auto *Method = dyn_cast<CXXMethodDecl>(FD);
+ Method && Method->isInstance() && !isa<CXXConstructorDecl>(FD)) {
+ isInstance = true;
+ }
+ auto getArgCaptureBy = [FD,
+ isInstance](unsigned I) -> LifetimeCaptureByAttr * {
+ const ParmVarDecl *PVD = nullptr;
+ // FIXME: Add support for capture_by on function declarations
+ if (isInstance) {
+ 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;
+ };
+ if (Args.empty())
+ return;
+ 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 (isGslPointerType(Args[I]->getType())) {
+ assert(!Args[I]->isGLValue() || CapturedOriginList->getLength() >= 2);
+ CapturedOriginList = getRValueOrigins(Args[I], CapturedOriginList);
+ }
+ if (!CapturedOriginList)
+ continue;
+ for (int CapturedByIdx : Attr->params()) {
+ if (CapturedByIdx == LifetimeCaptureByAttr::Global) {
+ for (OriginList *L = CapturedOriginList; L != nullptr;
+ L = L->peelOuterOrigin())
+ EscapesInCurrentBlock.push_back(FactMgr.createFact<GlobalEscapeFact>(
+ L->getOuterOriginID(), nullptr));
+ continue;
+ }
+ if (CapturedByIdx == LifetimeCaptureByAttr::Unknown ||
+ CapturedByIdx == LifetimeCaptureByAttr::Invalid)
+ continue;
+ unsigned CapturedByArgIdx =
+ (CapturedByIdx == LifetimeCaptureByAttr::This)
+ ? 0
+ : (unsigned)CapturedByIdx + (isInstance ? 1 : 0);
+ if (CapturedByArgIdx >= Args.size())
+ continue;
+ OriginList *CapturedByOriginList =
+ getOriginsList(*Args[CapturedByArgIdx]);
+ if (CapturedByOriginList) {
+ OriginList *Dest =
+ (isGslPointerType(Args[CapturedByArgIdx]->getType()) ||
+ isGslOwnerType(Args[CapturedByArgIdx]->getType()))
+ ? CapturedByOriginList->peelOuterOrigin()
+ : CapturedByOriginList;
+ flow(Dest, CapturedOriginList, /*Kill=*/false);
+ }
+ }
+ }
+}
+
void FactsGenerator::handleFunctionCall(const Expr *Call,
const FunctionDecl *FD,
ArrayRef<const Expr *> Args,
@@ -880,6 +948,7 @@ void FactsGenerator::handleFunctionCall(const Expr *Call,
handleDestructiveCall(Call, FD, Args);
handleMovedArgsInCall(FD, Args);
handleImplicitObjectFieldUses(Call, FD);
+ handleLifetimeCaptureBy(FD, Args);
if (!CallList)
return;
auto IsArgLifetimeBound = [FD](unsigned I) -> bool {
diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp
index 30b450c333fbd..aaef28a85f507 100644
--- a/clang/test/Sema/warn-lifetime-safety.cpp
+++ b/clang/test/Sema/warn-lifetime-safety.cpp
@@ -3284,3 +3284,76 @@ void assign_non_capturing_to_function_ref(function_ref &r) {
}
} // namespace GH126600
+
+//===----------------------------------------------------------------------===//
+// lifetime-capture-by
+//===----------------------------------------------------------------------===//
+
+void setCaptureBy(View& in, View out [[clang::lifetime_capture_by(in)]]);
+
+void use_after_free_capture_by() {
+ View res;
+ {
+ MyObj a;
+ setCaptureBy(res, a); // expected-warning {{object whose reference is captured 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 {{address of stack memory is returned later}}
+ return res; // expected-note {{returned here}}
+
+}
+
+void transitive_capture() {
+ View v1, v2;
+ {
+ MyObj local;
+ setCaptureBy(v1, local); // expected-warning {{object whose reference is captured does not live long enough}}
+ setCaptureBy(v2, v1);
+ } // expected-note {{destroyed here}}
+ (void)v2; // 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 {{object whose reference is captured does not live long enough}}
+ } // expected-note {{destroyed here}}
+ (void)c.stored; // expected-note {{later used here}}
+}
+
+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 {{object whose reference is captured 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 2 {{object whose reference is captured does not live long enough}}
+ } // expected-note 2 {{destroyed here}}
+ (void)res; // expected-note 2 {{later used here}}
+}
+
+
>From 4311c1f4a8ed347bca632eca058880e72b074fca Mon Sep 17 00:00:00 2001
From: Kashika Akhouri <akhourik at google.com>
Date: Thu, 21 May 2026 10:09:28 +0000
Subject: [PATCH 2/3] Modify origin flow logic and add more tests
---
.../LifetimeSafety/FactsGenerator.cpp | 65 ++++++++-----------
clang/test/Sema/warn-lifetime-safety.cpp | 51 ++++++++++++++-
2 files changed, 78 insertions(+), 38 deletions(-)
diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index 2908a9f1dd924..e126fcd6101a3 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -866,28 +866,27 @@ void FactsGenerator::handleImplicitObjectFieldUses(const Expr *Call,
void FactsGenerator::handleLifetimeCaptureBy(const FunctionDecl *FD,
ArrayRef<const Expr *> Args) {
- bool isInstance = false;
- if (const auto *Method = dyn_cast<CXXMethodDecl>(FD);
- Method && Method->isInstance() && !isa<CXXConstructorDecl>(FD)) {
- isInstance = true;
- }
+ 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 * {
+ IsInstance](unsigned I) -> LifetimeCaptureByAttr * {
const ParmVarDecl *PVD = nullptr;
- // FIXME: Add support for capture_by on function declarations
- if (isInstance) {
- if (I > 0 && I - 1 < FD->getNumParams()) {
+ if (IsInstance) {
+ // FIXME: Add support for capture_by on function declarations
+ if (I > 0 && I - 1 < FD->getNumParams())
PVD = FD->getParamDecl(I - 1);
- }
} else {
- if (I < FD->getNumParams()) {
+ if (I < FD->getNumParams())
PVD = FD->getParamDecl(I);
- }
}
return PVD ? PVD->getAttr<LifetimeCaptureByAttr>() : nullptr;
};
- if (Args.empty())
- return;
for (unsigned I = 0; I < Args.size(); ++I) {
const LifetimeCaptureByAttr *Attr = getArgCaptureBy(I);
if (!Attr)
@@ -902,32 +901,24 @@ void FactsGenerator::handleLifetimeCaptureBy(const FunctionDecl *FD,
if (!CapturedOriginList)
continue;
for (int CapturedByIdx : Attr->params()) {
- if (CapturedByIdx == LifetimeCaptureByAttr::Global) {
- for (OriginList *L = CapturedOriginList; L != nullptr;
- L = L->peelOuterOrigin())
- EscapesInCurrentBlock.push_back(FactMgr.createFact<GlobalEscapeFact>(
- L->getOuterOriginID(), nullptr));
- continue;
- }
- if (CapturedByIdx == LifetimeCaptureByAttr::Unknown ||
+ // FIXME: Add support for capturing to Global/unknown.
+ if (CapturedByIdx == LifetimeCaptureByAttr::Global ||
+ CapturedByIdx == LifetimeCaptureByAttr::Unknown ||
CapturedByIdx == LifetimeCaptureByAttr::Invalid)
continue;
- unsigned CapturedByArgIdx =
- (CapturedByIdx == LifetimeCaptureByAttr::This)
- ? 0
- : (unsigned)CapturedByIdx + (isInstance ? 1 : 0);
- if (CapturedByArgIdx >= Args.size())
+ ArrayRef<const Expr *> CallArgs = IsInstance ? Args.slice(1) : Args;
+ const Expr *CapturedByArg = (CapturedByIdx == LifetimeCaptureByAttr::This)
+ ? Args[0]
+ : CallArgs[CapturedByIdx];
+ assert(CapturedByArg && "Capturer expression must be valid");
+
+ OriginList *CapturedByOriginList = getOriginsList(*CapturedByArg);
+ OriginList *Dest = getRValueOrigins(CapturedByArg, CapturedByOriginList);
+ if (!Dest)
continue;
- OriginList *CapturedByOriginList =
- getOriginsList(*Args[CapturedByArgIdx]);
- if (CapturedByOriginList) {
- OriginList *Dest =
- (isGslPointerType(Args[CapturedByArgIdx]->getType()) ||
- isGslOwnerType(Args[CapturedByArgIdx]->getType()))
- ? CapturedByOriginList->peelOuterOrigin()
- : CapturedByOriginList;
- flow(Dest, CapturedOriginList, /*Kill=*/false);
- }
+ CurrentBlockFacts.push_back(FactMgr.createFact<OriginFlowFact>(
+ Dest->getOuterOriginID(), CapturedOriginList->getOuterOriginID(),
+ /*KillDest=*/false));
}
}
}
diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp
index aaef28a85f507..fb581f1d00f1b 100644
--- a/clang/test/Sema/warn-lifetime-safety.cpp
+++ b/clang/test/Sema/warn-lifetime-safety.cpp
@@ -3289,7 +3289,7 @@ void assign_non_capturing_to_function_ref(function_ref &r) {
// lifetime-capture-by
//===----------------------------------------------------------------------===//
-void setCaptureBy(View& in, View out [[clang::lifetime_capture_by(in)]]);
+void setCaptureBy(View& res, View in [[clang::lifetime_capture_by(res)]]);
void use_after_free_capture_by() {
View res;
@@ -3318,6 +3318,40 @@ void transitive_capture() {
(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 {{object whose reference is captured 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 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 {{object whose reference is captured 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)]]);
@@ -3332,6 +3366,21 @@ void member_capture() {
(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)]]);
>From 947bb147c418b32ea5995e4eb7894b812033e96d Mon Sep 17 00:00:00 2001
From: Kashika Akhouri <akhourik at google.com>
Date: Thu, 21 May 2026 10:28:42 +0000
Subject: [PATCH 3/3] Nit change
---
.../clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
index af545d5e9386c..f3eadc16473a4 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
@@ -91,7 +91,7 @@ class FactsGenerator : public ConstStmtVisitor<FactsGenerator> {
void handleMovedArgsInCall(const FunctionDecl *FD,
ArrayRef<const Expr *> Args);
- // Detects [[clang::lifetime_capture_by(X)]] annotations on a function call to
+ // 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);
More information about the cfe-commits
mailing list