[clang] [LifetimeSafety] Detect use-after-scope through fields in member calls (PR #191731)
via cfe-commits
cfe-commits at lists.llvm.org
Mon Apr 13 04:45:51 PDT 2026
https://github.com/NeKon69 updated https://github.com/llvm/llvm-project/pull/191731
>From 382e365ec09c88eb36fbd83874a5923295fe9b95 Mon Sep 17 00:00:00 2001
From: NeKon69 <nobodqwe at gmail.com>
Date: Sun, 12 Apr 2026 21:24:43 +0300
Subject: [PATCH 1/8] [LifetimeSafety] implement basic fix, add tests
---
.../Analyses/LifetimeSafety/FactsGenerator.h | 2 ++
.../LifetimeSafety/FactsGenerator.cpp | 14 ++++++++
clang/test/Sema/warn-lifetime-safety.cpp | 32 +++++++++++++++++++
3 files changed, 48 insertions(+)
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
index 2dbadb27981a7..a3b759c473a16 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
@@ -74,6 +74,8 @@ class FactsGenerator : public ConstStmtVisitor<FactsGenerator> {
void handleExitBlock();
+ void handleImplicitObjectFieldUses(const Expr *Call, const FunctionDecl *FD);
+
void handleGSLPointerConstruction(const CXXConstructExpr *CCE);
/// Detects arguments passed to rvalue reference parameters and creates
diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index 82b890b57817e..eaf26cb49aba5 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -723,6 +723,19 @@ void FactsGenerator::handleInvalidatingCall(const Expr *Call,
ThisList->getOuterOriginID(), Call));
}
+void FactsGenerator::handleImplicitObjectFieldUses(const Expr *Call,
+ const FunctionDecl *FD) {
+ const auto *MD = dyn_cast<CXXMethodDecl>(FD);
+ if (!MD || !MD->isInstance())
+ return;
+
+ for (const auto *Field : MD->getParent()->fields()) {
+ OriginList *FieldList = getOriginsList(*Field);
+ if (FieldList)
+ CurrentBlockFacts.push_back(FactMgr.createFact<UseFact>(Call, FieldList));
+ }
+}
+
void FactsGenerator::handleFunctionCall(const Expr *Call,
const FunctionDecl *FD,
ArrayRef<const Expr *> Args,
@@ -737,6 +750,7 @@ void FactsGenerator::handleFunctionCall(const Expr *Call,
handleUse(Arg);
handleInvalidatingCall(Call, FD, Args);
handleMovedArgsInCall(FD, Args);
+ handleImplicitObjectFieldUses(Call, FD);
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 77d8e3370676d..63a82680955ac 100644
--- a/clang/test/Sema/warn-lifetime-safety.cpp
+++ b/clang/test/Sema/warn-lifetime-safety.cpp
@@ -2531,3 +2531,35 @@ int *noreturn_dead_nested(bool cond, bool cond2, int *num) {
}
} // namespace conditional_operator_control_flow
+
+namespace method_call_uses_field_origins {
+// https://github.com/llvm/llvm-project/issues/182945
+
+int val;
+
+struct S {
+public:
+ int* p_;
+ void bar();
+ void foo() {
+ {
+ int num;
+ this->p_ = # // expected-warning {{object whose reference is captured does not live long enough}}
+ } // expected-note {{destroyed here}}
+ bar(); // expected-note {{later used here}}
+ this->p_ = &val;
+ }
+};
+
+// FIXME: False-positive: the analysis tracks a, but not that it belongs to s1.
+void foo() {
+ S s;
+ {
+ int num;
+ s.p_ = #
+ }
+ s.bar();
+ s.p_ = &val;
+}
+
+} // namespace method_call_uses_field_origins
>From ae721e42f219fe61c85493c7699673b099838145 Mon Sep 17 00:00:00 2001
From: NeKon69 <nobodqwe at gmail.com>
Date: Sun, 12 Apr 2026 21:41:44 +0300
Subject: [PATCH 2/8] add a new test
---
clang/test/Sema/warn-lifetime-safety.cpp | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp
index 63a82680955ac..1864247e53886 100644
--- a/clang/test/Sema/warn-lifetime-safety.cpp
+++ b/clang/test/Sema/warn-lifetime-safety.cpp
@@ -2551,6 +2551,16 @@ struct S {
}
};
+struct T {
+public:
+ std::string_view v;
+ void bar();
+ void foo() {
+ v = std::string("tmp"); // expected-warning {{object whose reference is captured does not live long enough}} expected-note {{destroyed here}}
+ bar(); // expected-note {{later used here}}
+ }
+};
+
// FIXME: False-positive: the analysis tracks a, but not that it belongs to s1.
void foo() {
S s;
>From 69897653f520b9c26843ccf8aa51684420f32548 Mon Sep 17 00:00:00 2001
From: NeKon69 <nobodqwe at gmail.com>
Date: Sun, 12 Apr 2026 22:23:49 +0300
Subject: [PATCH 3/8] update failed test
---
clang/unittests/Analysis/LifetimeSafetyTest.cpp | 1 -
1 file changed, 1 deletion(-)
diff --git a/clang/unittests/Analysis/LifetimeSafetyTest.cpp b/clang/unittests/Analysis/LifetimeSafetyTest.cpp
index 6cf65dd64ef83..879df0d147267 100644
--- a/clang/unittests/Analysis/LifetimeSafetyTest.cpp
+++ b/clang/unittests/Analysis/LifetimeSafetyTest.cpp
@@ -1408,7 +1408,6 @@ TEST_F(LifetimeAnalysisTest, TrivialClassDestructorsUAF) {
}
)");
EXPECT_THAT(Origin("ptr"), HasLoansTo({"s"}, "p1"));
- EXPECT_THAT(Origins({"ptr"}), MustBeLiveAt("p1"));
}
TEST_F(LifetimeAnalysisTest, SimpleReturnStackAddress) {
>From 806694352ec90e0785a10fe9254adf20f86b640b Mon Sep 17 00:00:00 2001
From: NeKon69 <nobodqwe at gmail.com>
Date: Mon, 13 Apr 2026 09:13:37 +0300
Subject: [PATCH 4/8] update test
---
clang/test/Sema/warn-lifetime-safety.cpp | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp
index 1864247e53886..ef1911ede59ad 100644
--- a/clang/test/Sema/warn-lifetime-safety.cpp
+++ b/clang/test/Sema/warn-lifetime-safety.cpp
@@ -2533,9 +2533,8 @@ int *noreturn_dead_nested(bool cond, bool cond2, int *num) {
} // namespace conditional_operator_control_flow
namespace method_call_uses_field_origins {
-// https://github.com/llvm/llvm-project/issues/182945
-
int val;
+std::string GLOBAL{"123"};
struct S {
public:
@@ -2559,9 +2558,16 @@ struct T {
v = std::string("tmp"); // expected-warning {{object whose reference is captured does not live long enough}} expected-note {{destroyed here}}
bar(); // expected-note {{later used here}}
}
+ void baz(){
+ std::vector<std::string> vec = {"42"};
+ v = vec[0]; // expected-warning {{object whose reference is captured is later invalidated}}
+ vec.push_back("1"); // expected-note {{invalidated here}}
+ bar(); // expected-note {{later used here}}
+ v = GLOBAL;
+ }
};
-// FIXME: False-positive: the analysis tracks a, but not that it belongs to s1.
+// FIXME: False-negative: the analysis tracks `p_`, but not that it belongs to s1.
void foo() {
S s;
{
>From 91ce6ffbe52ae0a41c28629e40dc33b0efe2116a Mon Sep 17 00:00:00 2001
From: NeKon69 <nobodqwe at gmail.com>
Date: Mon, 13 Apr 2026 09:17:01 +0300
Subject: [PATCH 5/8] add a comment
---
.../clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h | 2 ++
1 file changed, 2 insertions(+)
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
index a3b759c473a16..000f0dde2217d 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
@@ -74,6 +74,8 @@ class FactsGenerator : public ConstStmtVisitor<FactsGenerator> {
void handleExitBlock();
+ /// Mark all fields of the implicit object as used for an instance method
+ /// call, since the callee may access any part of the object.
void handleImplicitObjectFieldUses(const Expr *Call, const FunctionDecl *FD);
void handleGSLPointerConstruction(const CXXConstructExpr *CCE);
>From 13dc1c4a8d9d71874dd38197e1bec536166ccfa7 Mon Sep 17 00:00:00 2001
From: NeKon69 <nobodqwe at gmail.com>
Date: Mon, 13 Apr 2026 11:15:25 +0300
Subject: [PATCH 6/8] address review comments
---
.../LifetimeSafety/FactsGenerator.cpp | 6 ++---
.../warn-lifetime-safety-invalidations.cpp | 15 +++++++++++
clang/test/Sema/warn-lifetime-safety.cpp | 25 +++++++++----------
3 files changed, 29 insertions(+), 17 deletions(-)
diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index eaf26cb49aba5..1fbee2f3598d9 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -729,11 +729,9 @@ void FactsGenerator::handleImplicitObjectFieldUses(const Expr *Call,
if (!MD || !MD->isInstance())
return;
- for (const auto *Field : MD->getParent()->fields()) {
- OriginList *FieldList = getOriginsList(*Field);
- if (FieldList)
+ for (const auto *Field : MD->getParent()->fields())
+ if (auto *FieldList = getOriginsList(*Field))
CurrentBlockFacts.push_back(FactMgr.createFact<UseFact>(Call, FieldList));
- }
}
void FactsGenerator::handleFunctionCall(const Expr *Call,
diff --git a/clang/test/Sema/warn-lifetime-safety-invalidations.cpp b/clang/test/Sema/warn-lifetime-safety-invalidations.cpp
index c7f920c03736a..5a77ac97d16b1 100644
--- a/clang/test/Sema/warn-lifetime-safety-invalidations.cpp
+++ b/clang/test/Sema/warn-lifetime-safety-invalidations.cpp
@@ -512,3 +512,18 @@ void ref_capture_reassigned_to_safe() {
lambda(); // should not warn
}
} // namespace lambda_capture_invalidation
+
+namespace method_call_uses_field_invalidation {
+
+struct S {
+ std::string_view v;
+ void bar();
+ void baz(){
+ std::vector<std::string> vec = {"42"};
+ v = vec[0]; // expected-warning {{object whose reference is captured is later invalidated}}
+ vec.push_back("1"); // expected-note {{invalidated here}}
+ bar(); // expected-note {{later used here}}
+ v = nullptr;
+ }
+};
+} // namespace method_call_uses_field_invalidation
diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp
index ef1911ede59ad..4e562d6a7c06c 100644
--- a/clang/test/Sema/warn-lifetime-safety.cpp
+++ b/clang/test/Sema/warn-lifetime-safety.cpp
@@ -2533,11 +2533,10 @@ int *noreturn_dead_nested(bool cond, bool cond2, int *num) {
} // namespace conditional_operator_control_flow
namespace method_call_uses_field_origins {
-int val;
-std::string GLOBAL{"123"};
+int GLOBAL_INT;
+std::string GLOBAL_STRING{"123"};
struct S {
-public:
int* p_;
void bar();
void foo() {
@@ -2546,25 +2545,25 @@ struct S {
this->p_ = # // expected-warning {{object whose reference is captured does not live long enough}}
} // expected-note {{destroyed here}}
bar(); // expected-note {{later used here}}
- this->p_ = &val;
+ this->p_ = &GLOBAL_INT;
+ }
+ void baz() {
+ {
+ int num;
+ this->p_ = #
+ }
+ this->p_ = &GLOBAL_INT;
+ bar();
}
};
struct T {
-public:
std::string_view v;
void bar();
void foo() {
v = std::string("tmp"); // expected-warning {{object whose reference is captured does not live long enough}} expected-note {{destroyed here}}
bar(); // expected-note {{later used here}}
}
- void baz(){
- std::vector<std::string> vec = {"42"};
- v = vec[0]; // expected-warning {{object whose reference is captured is later invalidated}}
- vec.push_back("1"); // expected-note {{invalidated here}}
- bar(); // expected-note {{later used here}}
- v = GLOBAL;
- }
};
// FIXME: False-negative: the analysis tracks `p_`, but not that it belongs to s1.
@@ -2575,7 +2574,7 @@ void foo() {
s.p_ = #
}
s.bar();
- s.p_ = &val;
+ s.p_ = &GLOBAL_INT;
}
} // namespace method_call_uses_field_origins
>From 8d623bb7c8a74a5e08b02f68987e3ab86ba4a7e8 Mon Sep 17 00:00:00 2001
From: NeKon69 <nobodqwe at gmail.com>
Date: Mon, 13 Apr 2026 14:41:06 +0300
Subject: [PATCH 7/8] use base fields too (doesn't work currently)
---
.../LifetimeSafety/FactsGenerator.cpp | 23 ++++++++++++++++---
1 file changed, 20 insertions(+), 3 deletions(-)
diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index 1fbee2f3598d9..3e4807c5b2e48 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -729,9 +729,26 @@ void FactsGenerator::handleImplicitObjectFieldUses(const Expr *Call,
if (!MD || !MD->isInstance())
return;
- for (const auto *Field : MD->getParent()->fields())
- if (auto *FieldList = getOriginsList(*Field))
- CurrentBlockFacts.push_back(FactMgr.createFact<UseFact>(Call, FieldList));
+ const auto *ClassDecl = MD->getParent()->getDefinition();
+ if (!ClassDecl)
+ return;
+
+ const auto UseFields =
+ [&](const CXXRecordDecl *RD) {
+ for (const auto *Field : RD->fields())
+ if (auto *FieldList = getOriginsList(*Field)) {
+ CurrentBlockFacts.push_back(
+ FactMgr.createFact<UseFact>(Call, FieldList));
+ Field->dumpColor();
+ }
+ };
+
+ UseFields(ClassDecl);
+
+ ClassDecl->forallBases([&](const CXXRecordDecl *Base) {
+ UseFields(Base);
+ return true;
+ });
}
void FactsGenerator::handleFunctionCall(const Expr *Call,
>From d6e79449ea987347c94e8b86f44dad2b703bd7c1 Mon Sep 17 00:00:00 2001
From: NeKon69 <nobodqwe at gmail.com>
Date: Mon, 13 Apr 2026 14:45:35 +0300
Subject: [PATCH 8/8] cleanup lambda
---
.../Analysis/LifetimeSafety/FactsGenerator.cpp | 15 ++++++---------
1 file changed, 6 insertions(+), 9 deletions(-)
diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index 3e4807c5b2e48..71614407b8558 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -733,15 +733,12 @@ void FactsGenerator::handleImplicitObjectFieldUses(const Expr *Call,
if (!ClassDecl)
return;
- const auto UseFields =
- [&](const CXXRecordDecl *RD) {
- for (const auto *Field : RD->fields())
- if (auto *FieldList = getOriginsList(*Field)) {
- CurrentBlockFacts.push_back(
- FactMgr.createFact<UseFact>(Call, FieldList));
- Field->dumpColor();
- }
- };
+ const auto UseFields = [&](const CXXRecordDecl *RD) {
+ for (const auto *Field : RD->fields())
+ if (auto *FieldList = getOriginsList(*Field))
+ CurrentBlockFacts.push_back(
+ FactMgr.createFact<UseFact>(Call, FieldList));
+ };
UseFields(ClassDecl);
More information about the cfe-commits
mailing list