[clang] RAII reseters and dangling field (PR #214212)
Utkarsh Saxena via cfe-commits
cfe-commits at lists.llvm.org
Wed Aug 5 05:31:31 PDT 2026
https://github.com/usx95 updated https://github.com/llvm/llvm-project/pull/214212
>From 245c32b7c5c71fc63ec69e96e11a65cbfbbab796 Mon Sep 17 00:00:00 2001
From: Utkarsh Saxena <usx at google.com>
Date: Wed, 5 Aug 2026 12:18:32 +0000
Subject: [PATCH] RAII reseters and dangling field
---
.../Analysis/Analyses/LifetimeSafety/Facts.h | 16 ++++++++++
.../Analyses/LifetimeSafety/LifetimeSafety.h | 1 +
clang/include/clang/Basic/DiagnosticGroups.td | 7 +++++
.../clang/Basic/DiagnosticSemaKinds.td | 6 ++++
clang/lib/Analysis/LifetimeSafety/Checker.cpp | 12 ++++---
.../LifetimeSafety/FactsGenerator.cpp | 14 +++++++++
clang/lib/Sema/SemaLifetimeSafety.h | 9 ++++--
.../Sema/LifetimeSafety/dangling-field.cpp | 31 +++++++++++++++++++
8 files changed, 89 insertions(+), 7 deletions(-)
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
index 94db2a7f311ae..a5dfc867a4f0e 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
@@ -22,6 +22,7 @@
#include "clang/Analysis/CFG.h"
#include "llvm/ADT/STLFunctionalExtras.h"
#include "llvm/ADT/SmallVector.h"
+#include "llvm/ADT/DenseSet.h"
#include "llvm/Support/Debug.h"
#include "llvm/Support/raw_ostream.h"
#include <cstdint>
@@ -394,6 +395,14 @@ class FactManager {
OriginManager &getOriginMgr() { return OriginMgr; }
const OriginManager &getOriginMgr() const { return OriginMgr; }
+ void addCapturedField(const FieldDecl *FD) { CapturedFields.insert(FD); }
+ bool isFieldCapturedByLambda(const FieldDecl *FD) const {
+ return IsThisCapturedByLambda || CapturedFields.contains(FD);
+ }
+ void setThisCapturedByLambda() {
+ IsThisCapturedByLambda = true;
+ }
+
private:
FactID NextFactID{0};
LoanManager LoanMgr;
@@ -401,6 +410,13 @@ class FactManager {
/// Facts for each CFG block, indexed by block ID.
llvm::SmallVector<llvm::SmallVector<const Fact *>> BlockToFacts;
llvm::BumpPtrAllocator FactAllocator;
+
+ /// Set of field declarations that are explicitly or init-captured in any
+ /// lambda within the analyzed function.
+ llvm::DenseSet<const FieldDecl *> CapturedFields;
+ /// Whether the 'this' pointer is captured by any lambda within the analyzed
+ /// function. When true, any field of 'this' is considered captured.
+ bool IsThisCapturedByLambda = false;
};
} // namespace clang::lifetimes::internal
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h
index a51ef2f7cc0ba..ed62cdc538175 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h
@@ -76,6 +76,7 @@ class LifetimeSafetySemaHelper {
virtual void reportDanglingField(const Expr *IssueExpr,
const FieldDecl *Field,
const Expr *MovedExpr,
+ bool IsCapturedByLambda,
SourceLocation ExpiryLoc) {}
virtual void reportDanglingGlobal(const Expr *IssueExpr,
diff --git a/clang/include/clang/Basic/DiagnosticGroups.td b/clang/include/clang/Basic/DiagnosticGroups.td
index b7072634cccf3..f670f442a8c0d 100644
--- a/clang/include/clang/Basic/DiagnosticGroups.td
+++ b/clang/include/clang/Basic/DiagnosticGroups.td
@@ -608,6 +608,12 @@ Warning to detect dangling field references.
This may contain false-positives, e.g. when the borrowed storage is potentially moved and is not destroyed at function exit.
}];
}
+def LifetimeSafetyDanglingFieldLambdaCapture : DiagGroup<"lifetime-safety-dangling-field-lambda-capture"> {
+ code Documentation = [{
+Warning to detect dangling field references.
+This may contain false-positives, e.g. when the field is captured by a lambda that resets the field before function exit.
+ }];
+}
def LifetimeSafetyDanglingGlobal : DiagGroup<"lifetime-safety-dangling-global"> {
code Documentation = [{
@@ -667,6 +673,7 @@ def LifetimeSafetyStrict : DiagGroup<"lifetime-safety-strict",
LifetimeSafetyUseAfterScopeMoved,
LifetimeSafetyReturnStackAddrMoved,
LifetimeSafetyDanglingFieldMoved,
+ LifetimeSafetyDanglingFieldLambdaCapture,
LifetimeSafetyDanglingGlobalMoved,
LifetimeSafetyInvalidation]>;
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index cce6f70a58893..a1c51e4ce065a 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -11057,6 +11057,12 @@ def warn_lifetime_safety_dangling_field_moved
"Consider moving first and then aliasing later to resolve the issue">,
InGroup<LifetimeSafetyDanglingFieldMoved>,
DefaultIgnore;
+def warn_lifetime_safety_dangling_field_lambda_capture
+ : Warning<"stack memory associated with %0 may escape to the %1 which will dangle. "
+ "This could be a false positive as the field was captured by a lambda and "
+ "may have been reset before escaping">,
+ InGroup<LifetimeSafetyDanglingFieldLambdaCapture>,
+ DefaultIgnore;
def warn_lifetime_safety_dangling_global
: Warning<"stack memory associated with %0 escapes to the %1 which will dangle">,
InGroup<LifetimeSafetyDanglingGlobal>,
diff --git a/clang/lib/Analysis/LifetimeSafety/Checker.cpp b/clang/lib/Analysis/LifetimeSafety/Checker.cpp
index 155c6072a33a5..a69768d7b92cc 100644
--- a/clang/lib/Analysis/LifetimeSafety/Checker.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Checker.cpp
@@ -323,11 +323,15 @@ class LifetimeChecker {
// Return stack address.
SemaHelper->reportUseAfterReturn(
IssueExpr, RetEscape->getReturnExpr(), MovedExpr);
- else if (const auto *FieldEscape = dyn_cast<FieldEscapeFact>(OEF))
+ else if (const auto *FieldEscape = dyn_cast<FieldEscapeFact>(OEF)) {
// Dangling field.
- SemaHelper->reportDanglingField(
- IssueExpr, FieldEscape->getFieldDecl(), MovedExpr, ExpiryLoc);
- else if (const auto *GlobalEscape = dyn_cast<GlobalEscapeFact>(OEF))
+ bool IsCapturedByLambda =
+ FactMgr.isFieldCapturedByLambda(FieldEscape->getFieldDecl());
+ SemaHelper->reportDanglingField(IssueExpr,
+ FieldEscape->getFieldDecl(),
+ MovedExpr, IsCapturedByLambda,
+ ExpiryLoc);
+ } else if (const auto *GlobalEscape = dyn_cast<GlobalEscapeFact>(OEF))
// Global escape.
SemaHelper->reportDanglingGlobal(IssueExpr, GlobalEscape->getGlobal(),
MovedExpr, ExpiryLoc);
diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index ac6267dabf48e..7cab40d9e929c 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -696,6 +696,20 @@ void FactsGenerator::VisitMaterializeTemporaryExpr(
}
void FactsGenerator::VisitLambdaExpr(const LambdaExpr *LE) {
+ for (const LambdaCapture &C : LE->captures()) {
+ if (C.capturesThis())
+ FactMgr.setThisCapturedByLambda();
+ else if (C.capturesVariable() && C.getCapturedVar()->isInitCapture()) {
+ const Expr *Init = cast<VarDecl>(C.getCapturedVar())->getInit();
+ if (!Init)
+ continue;
+ if (const auto *ME = dyn_cast<MemberExpr>(Init->IgnoreParenImpCasts())) {
+ if (const auto *FD = dyn_cast<FieldDecl>(ME->getMemberDecl()))
+ FactMgr.addCapturedField(FD);
+ }
+ }
+ }
+
// The lambda gets a single merged origin that aggregates all captured
// pointer-like origins. Currently we only need to detect whether the lambda
// outlives any capture.
diff --git a/clang/lib/Sema/SemaLifetimeSafety.h b/clang/lib/Sema/SemaLifetimeSafety.h
index 1d9f94be7e22d..e37ab74730d9f 100644
--- a/clang/lib/Sema/SemaLifetimeSafety.h
+++ b/clang/lib/Sema/SemaLifetimeSafety.h
@@ -146,10 +146,13 @@ class LifetimeSafetySemaHelperImpl : public LifetimeSafetySemaHelper {
void reportDanglingField(const Expr *IssueExpr,
const FieldDecl *DanglingField,
const Expr *MovedExpr,
+ bool IsCapturedByLambda,
SourceLocation ExpiryLoc) override {
- unsigned DiagID = MovedExpr
- ? diag::warn_lifetime_safety_dangling_field_moved
- : diag::warn_lifetime_safety_dangling_field;
+ unsigned DiagID =
+ IsCapturedByLambda
+ ? diag::warn_lifetime_safety_dangling_field_lambda_capture
+ : (MovedExpr ? diag::warn_lifetime_safety_dangling_field_moved
+ : diag::warn_lifetime_safety_dangling_field);
S.Diag(IssueExpr->getExprLoc(), DiagID)
<< getDiagSubjectDescription(IssueExpr)
diff --git a/clang/test/Sema/LifetimeSafety/dangling-field.cpp b/clang/test/Sema/LifetimeSafety/dangling-field.cpp
index bc73c4f7e8644..8b68d962d807c 100644
--- a/clang/test/Sema/LifetimeSafety/dangling-field.cpp
+++ b/clang/test/Sema/LifetimeSafety/dangling-field.cpp
@@ -255,3 +255,34 @@ struct DtorSet {
}
};
} // namespace DtorNoWarn
+
+namespace LambdaCaptureReset {
+struct MyObj {};
+struct HasField {
+ MyObj* ptr; // expected-note 3 {{this field dangles}}
+
+ void this_capture() {
+ MyObj local;
+ ptr = &local; // expected-warning-re {{stack memory associated with local variable 'local' may escape to the field 'ptr' which will dangle. {{.*}} captured by a lambda}}
+ auto cleanup = [this]() {
+ ptr = nullptr;
+ };
+ }
+
+ void capture_by_ref() {
+ MyObj local;
+ ptr = &local; // expected-warning-re {{stack memory associated with local variable 'local' may escape to the field 'ptr' which will dangle. {{.*}} captured by a lambda}}
+ auto cleanup = [&]() {
+ ptr = nullptr;
+ };
+ }
+
+ void foo_init_capture() {
+ MyObj local;
+ ptr = &local; // expected-warning-re {{stack memory associated with local variable 'local' may escape to the field 'ptr' which will dangle. {{.*}} captured by a lambda}}
+ auto cleanup = [&p = ptr]() {
+ p = nullptr;
+ };
+ }
+};
+} // namespace LambdaCaptureReset
More information about the cfe-commits
mailing list