[clang] [LifetimeSafety] Add parameter lifetime tracking in CFG (PR #169320)
Utkarsh Saxena via cfe-commits
cfe-commits at lists.llvm.org
Mon Nov 24 06:16:32 PST 2025
https://github.com/usx95 updated https://github.com/llvm/llvm-project/pull/169320
>From 6a25dabd399c869be8437f5440f925279dbe6c84 Mon Sep 17 00:00:00 2001
From: Utkarsh Saxena <usx at google.com>
Date: Mon, 24 Nov 2025 11:54:49 +0000
Subject: [PATCH] fix-missing-lifetimeends-for-params
---
clang/lib/Analysis/CFG.cpp | 16 +++++-
clang/test/Analysis/lifetime-cfg-output.cpp | 28 +++++++++
clang/test/Analysis/scopes-cfg-output.cpp | 2 +
clang/test/Sema/warn-lifetime-safety.cpp | 57 ++++++++++++++++---
.../Analysis/FlowSensitive/LoggerTest.cpp | 11 +++-
5 files changed, 105 insertions(+), 9 deletions(-)
diff --git a/clang/lib/Analysis/CFG.cpp b/clang/lib/Analysis/CFG.cpp
index cdde849b0e026..c3101299c08ac 100644
--- a/clang/lib/Analysis/CFG.cpp
+++ b/clang/lib/Analysis/CFG.cpp
@@ -1666,6 +1666,12 @@ std::unique_ptr<CFG> CFGBuilder::buildCFG(const Decl *D, Stmt *Statement) {
assert(Succ == &cfg->getExit());
Block = nullptr; // the EXIT block is empty. Create all other blocks lazily.
+ // Add parameters to the initial scope to handle their dtos and lifetime ends.
+ LocalScope *paramScope = nullptr;
+ if (const auto *FD = dyn_cast_or_null<FunctionDecl>(D))
+ for (ParmVarDecl *PD : FD->parameters())
+ paramScope = addLocalScopeForVarDecl(PD, paramScope);
+
if (BuildOpts.AddImplicitDtors)
if (const CXXDestructorDecl *DD = dyn_cast_or_null<CXXDestructorDecl>(D))
addImplicitDtorsForDestructor(DD);
@@ -2246,6 +2252,9 @@ LocalScope* CFGBuilder::addLocalScopeForVarDecl(VarDecl *VD,
if (!VD->hasLocalStorage())
return Scope;
+ if (isa<ParmVarDecl>(VD) && VD->getType()->isReferenceType())
+ return Scope;
+
if (!BuildOpts.AddLifetime && !BuildOpts.AddScopes &&
!needsAutomaticDestruction(VD)) {
assert(BuildOpts.AddImplicitDtors);
@@ -5616,8 +5625,13 @@ class StmtPrinterHelper : public PrinterHelper {
bool handleDecl(const Decl *D, raw_ostream &OS) {
DeclMapTy::iterator I = DeclMap.find(D);
- if (I == DeclMap.end())
+ if (I == DeclMap.end()) {
+ if (auto* PVD = dyn_cast_or_null<ParmVarDecl>(D)) {
+ OS << "[Parm: " << PVD->getNameAsString() << "]";
+ return true;
+ }
return false;
+ }
if (currentBlock >= 0 && I->second.first == (unsigned) currentBlock
&& I->second.second == currStmt) {
diff --git a/clang/test/Analysis/lifetime-cfg-output.cpp b/clang/test/Analysis/lifetime-cfg-output.cpp
index 0a75c5bcc0bcc..36b36eddc440c 100644
--- a/clang/test/Analysis/lifetime-cfg-output.cpp
+++ b/clang/test/Analysis/lifetime-cfg-output.cpp
@@ -935,3 +935,31 @@ int backpatched_goto() {
goto label;
i++;
}
+
+// CHECK: [B2 (ENTRY)]
+// CHECK-NEXT: Succs (1): B1
+// CHECK: [B1]
+// CHECK-NEXT: 1: a
+// CHECK-NEXT: 2: [B1.1] (ImplicitCastExpr, LValueToRValue, int)
+// CHECK-NEXT: 3: b
+// CHECK-NEXT: 4: [B1.3] (ImplicitCastExpr, LValueToRValue, int)
+// CHECK-NEXT: 5: [B1.2] + [B1.4]
+// CHECK-NEXT: 6: c
+// CHECK-NEXT: 7: [B1.6] (ImplicitCastExpr, LValueToRValue, int)
+// CHECK-NEXT: 8: [B1.5] + [B1.7]
+// CHECK-NEXT: 9: int res = a + b + c;
+// CHECK-NEXT: 10: res
+// CHECK-NEXT: 11: [B1.10] (ImplicitCastExpr, LValueToRValue, int)
+// CHECK-NEXT: 12: return [B1.11];
+// CHECK-NEXT: 13: [B1.9] (Lifetime ends)
+// CHECK-NEXT: 14: [Parm: c] (Lifetime ends)
+// CHECK-NEXT: 15: [Parm: b] (Lifetime ends)
+// CHECK-NEXT: 16: [Parm: a] (Lifetime ends)
+// CHECK-NEXT: Preds (1): B2
+// CHECK-NEXT: Succs (1): B0
+// CHECK: [B0 (EXIT)]
+// CHECK-NEXT: Preds (1): B1
+int test_param_scope_end_order(int a, int b, int c) {
+ int res = a + b + c;
+ return res;
+}
diff --git a/clang/test/Analysis/scopes-cfg-output.cpp b/clang/test/Analysis/scopes-cfg-output.cpp
index 6ed6f3638f75b..9c75492c33a42 100644
--- a/clang/test/Analysis/scopes-cfg-output.cpp
+++ b/clang/test/Analysis/scopes-cfg-output.cpp
@@ -1437,12 +1437,14 @@ void test_cleanup_functions() {
// CHECK-NEXT: 4: return;
// CHECK-NEXT: 5: CleanupFunction (cleanup_int)
// CHECK-NEXT: 6: CFGScopeEnd(i)
+// CHECK-NEXT: 7: CFGScopeEnd(m)
// CHECK-NEXT: Preds (1): B3
// CHECK-NEXT: Succs (1): B0
// CHECK: [B2]
// CHECK-NEXT: 1: return;
// CHECK-NEXT: 2: CleanupFunction (cleanup_int)
// CHECK-NEXT: 3: CFGScopeEnd(i)
+// CHECK-NEXT: 4: CFGScopeEnd(m)
// CHECK-NEXT: Preds (1): B3
// CHECK-NEXT: Succs (1): B0
// CHECK: [B3]
diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp
index e80a05860389c..1191469e23df1 100644
--- a/clang/test/Sema/warn-lifetime-safety.cpp
+++ b/clang/test/Sema/warn-lifetime-safety.cpp
@@ -529,14 +529,14 @@ TriviallyDestructedClass* trivial_class_uar () {
return ptr; // expected-note {{returned here}}
}
-// FIXME: No lifetime warning for this as no expire facts are generated for parameters
const int& return_parameter(int a) {
- return a;
+ return a; // expected-warning {{address of stack memory is returned later}}
+ // expected-note at -1 {{returned here}}
}
-// FIXME: No lifetime warning for this as no expire facts are generated for parameters
int* return_pointer_to_parameter(int a) {
- return &a;
+ return &a; // expected-warning {{address of stack memory is returned later}}
+ // expected-note at -1 {{returned here}}
}
const int& return_reference_to_parameter(int a)
@@ -788,9 +788,52 @@ const MyObj& lifetimebound_return_ref_to_local() {
// expected-note at -1 {{returned here}}
}
-// FIXME: Fails to diagnose UAR when a reference to a by-value param escapes via the return value.
-View lifetimebound_return_of_by_value_param(MyObj stack_param) {
- return Identity(stack_param);
+View lifetimebound_return_by_value_param(MyObj stack_param) {
+ return Identity(stack_param); // expected-warning {{address of stack memory is returned later}}
+ // expected-note at -1 {{returned here}}
+}
+
+View lifetimebound_return_by_value_multiple_param(int cond, MyObj a, MyObj b, MyObj c) {
+ if (cond == 1)
+ return Identity(a); // expected-warning {{address of stack memory is returned later}}
+ // expected-note at -1 {{returned here}}
+ if (cond == 2)
+ return Identity(b); // expected-warning {{address of stack memory is returned later}}
+ // expected-note at -1 {{returned here}}
+ return Identity(c); // expected-warning {{address of stack memory is returned later}}
+ // expected-note at -1 {{returned here}}
+}
+
+template<class T>
+View lifetimebound_return_by_value_param_template(T t) {
+ return Identity(t); // expected-warning {{address of stack memory is returned later}}
+ // expected-note at -1 {{returned here}}
+}
+void use_lifetimebound_return_by_value_param_template() {
+ lifetimebound_return_by_value_param_template(MyObj{}); // expected-note {{in instantiation of}}
+}
+
+void lambda_uar_param() {
+ auto lambda = [](MyObj stack_param) {
+ return Identity(stack_param); // expected-warning {{address of stack memory is returned later}}
+ // expected-note at -1 {{returned here}}
+ };
+ lambda(MyObj{});
+}
+
+// FIXME: This should be detected. We see correct destructors but origin flow breaks somewhere.
+namespace VariadicTemplatedParamsUAR{
+
+template<typename... Args>
+View Max(Args... args [[clang::lifetimebound]]);
+
+template<typename... Args>
+View lifetimebound_return_of_variadic_param(Args... args) {
+ return Max(args...);
+}
+void test_variadic() {
+ lifetimebound_return_of_variadic_param(MyObj{1}, MyObj{2}, MyObj{3});
+}
}
// FIXME: Fails to diagnose UAF when a reference to a by-value param escapes via an out-param.
diff --git a/clang/unittests/Analysis/FlowSensitive/LoggerTest.cpp b/clang/unittests/Analysis/FlowSensitive/LoggerTest.cpp
index 88630119ba8a1..609255437fe82 100644
--- a/clang/unittests/Analysis/FlowSensitive/LoggerTest.cpp
+++ b/clang/unittests/Analysis/FlowSensitive/LoggerTest.cpp
@@ -149,9 +149,18 @@ recordState(Elements=8, Branches=2, Joins=1)
enterElement(return b ? p : q;)
transfer()
recordState(Elements=9, Branches=2, Joins=1)
+enterElement([Parm: q] (Lifetime ends))
+transfer()
+recordState(Elements=10, Branches=2, Joins=1)
+enterElement([Parm: p] (Lifetime ends))
+transfer()
+recordState(Elements=11, Branches=2, Joins=1)
+enterElement([Parm: b] (Lifetime ends))
+transfer()
+recordState(Elements=12, Branches=2, Joins=1)
enterBlock(0, false)
-recordState(Elements=9, Branches=2, Joins=1)
+recordState(Elements=12, Branches=2, Joins=1)
endAnalysis()
)");
More information about the cfe-commits
mailing list