[clang] [LifetimeSafety] Propagate inner origins through std::move and related casts (PR #199600)
Zhijie Wang via cfe-commits
cfe-commits at lists.llvm.org
Wed May 27 12:58:51 PDT 2026
https://github.com/aeft updated https://github.com/llvm/llvm-project/pull/199600
>From f7274dbaf3e8f563e6b6890b16bd2bf75d19cc39 Mon Sep 17 00:00:00 2001
From: Zhijie Wang <yesterda9 at gmail.com>
Date: Mon, 25 May 2026 21:21:35 -0700
Subject: [PATCH] [LifetimeSafety] Propagate inner origins through std::move
and related casts
---
.../LifetimeSafety/LifetimeAnnotations.h | 5 +++
.../LifetimeSafety/FactsGenerator.cpp | 12 +++++-
.../LifetimeSafety/LifetimeAnnotations.cpp | 15 +++++++
clang/test/Sema/Inputs/lifetime-analysis.h | 3 ++
clang/test/Sema/warn-lifetime-safety.cpp | 42 ++++++++++++++++---
5 files changed, 70 insertions(+), 7 deletions(-)
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h
index a97df7a08dfeb..47fcd5dbfd569 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h
@@ -106,6 +106,11 @@ bool destructsFirstArg(const FunctionDecl &FD);
/// that can propagate the stored lambda's origins.
bool isStdCallableWrapperType(const CXXRecordDecl *RD);
+/// Returns true for std reference-cast builtins (e.g., std::move). Their result
+/// refers to the same object as the argument, so all origins propagate from
+/// argument to result.
+bool isStdReferenceCast(const FunctionDecl *FD);
+
} // namespace clang::lifetimes
#endif // LLVM_CLANG_ANALYSIS_ANALYSES_LIFETIMEANNOTATIONS_H
diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index 9038f56689779..323802c6a88d5 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -886,6 +886,14 @@ void FactsGenerator::handleFunctionCall(const Expr *Call,
handleImplicitObjectFieldUses(Call, FD);
if (!CallList)
return;
+ if (isStdReferenceCast(FD)) {
+ assert(Args.size() == 1 &&
+ "std reference cast builtins take exactly one argument");
+ // std reference-cast functions like std::move return a result that refers
+ // to the same object as the argument, so propagate the full origins.
+ flow(CallList, getOriginsList(*Args[0]), /*Kill=*/true);
+ return;
+ }
auto IsArgLifetimeBound = [FD, &Args](unsigned I) -> bool {
const ParmVarDecl *PVD = nullptr;
if (const auto *Method = dyn_cast<CXXMethodDecl>(FD);
@@ -954,7 +962,7 @@ void FactsGenerator::handleFunctionCall(const Expr *Call,
}
} else if (shouldTrackPointerImplicitObjectArg(I)) {
assert(ArgList->getLength() >= 2 &&
- "Object arg of pointer type should have atleast two origins");
+ "Object arg of pointer type should have at least two origins");
// See through the GSLPointer reference to see the pointer's value.
CurrentBlockFacts.push_back(FactMgr.createFact<OriginFlowFact>(
CallList->getOuterOriginID(),
@@ -963,7 +971,7 @@ void FactsGenerator::handleFunctionCall(const Expr *Call,
} else if (IsArgLifetimeBound(I)) {
// Lifetimebound on a non-GSL-ctor function means the returned
// pointer/reference itself must not outlive the arguments. This
- // only constraints the top-level origin.
+ // only constrains the top-level origin.
CurrentBlockFacts.push_back(FactMgr.createFact<OriginFlowFact>(
CallList->getOuterOriginID(), ArgList->getOuterOriginID(), KillSrc));
KillSrc = false;
diff --git a/clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp b/clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp
index 2f26e77d5a0eb..6a52616c5d590 100644
--- a/clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp
@@ -460,4 +460,19 @@ bool isStdCallableWrapperType(const CXXRecordDecl *RD) {
return Name == "function" || Name == "move_only_function";
}
+bool isStdReferenceCast(const FunctionDecl *FD) {
+ if (!FD)
+ return false;
+ switch (FD->getBuiltinID()) {
+ case Builtin::BImove:
+ case Builtin::BImove_if_noexcept:
+ case Builtin::BIforward:
+ case Builtin::BIforward_like:
+ case Builtin::BIas_const:
+ return true;
+ default:
+ return false;
+ }
+}
+
} // namespace clang::lifetimes
diff --git a/clang/test/Sema/Inputs/lifetime-analysis.h b/clang/test/Sema/Inputs/lifetime-analysis.h
index 2ae6ed38714b7..4d727ae9499d6 100644
--- a/clang/test/Sema/Inputs/lifetime-analysis.h
+++ b/clang/test/Sema/Inputs/lifetime-analysis.h
@@ -37,6 +37,9 @@ ForwardIt1 search( ForwardIt1 first, ForwardIt1 last,
template<typename T>
typename remove_reference<T>::type &&move(T &&t) noexcept;
+template<typename T>
+T &&forward(typename remove_reference<T>::type &t) noexcept;
+
template <typename C>
auto data(const C &c) -> decltype(c.data());
diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp
index d5f558c7918b3..ae9d5d08e358b 100644
--- a/clang/test/Sema/warn-lifetime-safety.cpp
+++ b/clang/test/Sema/warn-lifetime-safety.cpp
@@ -3249,15 +3249,12 @@ std::function<void()> chained_copy_assign() {
return f3; // expected-note {{returned here}}
}
-// FIXME: False negative. std::move's lifetimebound handling in
-// `handleFunctionCall` only flows the outermost origin, missing inner origins
-// that carry the lambda's loans.
std::function<void()> move_assign() {
int x;
- std::function<void()> f = [&x]() { (void)x; }; // Should warn.
+ std::function<void()> f = [&x]() { (void)x; }; // expected-warning {{stack memory associated with local variable 'x' is returned}}
std::function<void()> f2 = []() {};
f2 = std::move(f);
- return f2;
+ return f2; // expected-note {{returned here}}
}
std::function<void()> reassign_safe_then_unsafe() {
@@ -3376,3 +3373,38 @@ void deref_use_after_scope() {
}
} // namespace GH188832
+
+namespace GH191954 {
+ int* return_moved_pointer() {
+ int x;
+ int* f = &x; // expected-warning {{stack memory associated with local variable 'x' is returned}}
+ int* a;
+ a = std::move(f);
+ return a; // expected-note {{returned here}}
+ }
+
+ int* return_moved_pointer2() {
+ int x;
+ int* f = &x; // expected-warning {{stack memory associated with local variable 'x' is returned}}
+ return std::move(f); // expected-note {{returned here}}
+ }
+
+ View return_moved_view() {
+ MyObj o;
+ View v(o); // expected-warning {{stack memory associated with local variable 'o' is returned}}
+ View v2 = std::move(v);
+ return v2; // expected-note {{returned here}}
+ }
+
+ int* return_forwarded_pointer() {
+ int x;
+ int* f = &x; // expected-warning {{stack memory associated with local variable 'x' is returned}}
+ return std::forward<int*>(f); // expected-note {{returned here}}
+ }
+
+ int g;
+ int* return_moved_pointer_to_global() {
+ int* f = &g;
+ return std::move(f);
+ }
+} // namespace GH191954
More information about the cfe-commits
mailing list