[clang] [LifetimeSafety] Model GNU statement expressions (PR #204841)
Gábor Horváth via cfe-commits
cfe-commits at lists.llvm.org
Tue Jun 23 04:48:52 PDT 2026
https://github.com/Xazax-hun updated https://github.com/llvm/llvm-project/pull/204841
>From 870f880dcc584be516819fc8bfae30fdc9d40319 Mon Sep 17 00:00:00 2001
From: Gabor Horvath <gaborh at apple.com>
Date: Fri, 19 Jun 2026 15:39:45 +0100
Subject: [PATCH] [LifetimeSafety] Model GNU statement expressions
A statement expression `({ ...; e; })` carried none of its final expression's
loans, so a borrow used through it was silently dropped. Forward the statement
expression's origins to `e` (sharing, so the loan `e` produces sits in this
origin before the body's locals expire), and mark the value used in a new
VisitStmtExpr. That use lands at the statement expression's program point, which
the CFG places after the body's locals expire, keeping the borrow live across
those expiries: a borrow of a body-local is reported as use-after-scope, and a
borrow forwarded from an outer object propagates to its consumer.
Assisted-by: Claude Opus 4.8
---
.../Analyses/LifetimeSafety/FactsGenerator.h | 1 +
.../LifetimeSafety/FactsGenerator.cpp | 9 +++
clang/lib/Analysis/LifetimeSafety/Origins.cpp | 11 +++
clang/test/Sema/LifetimeSafety/safety.cpp | 74 +++++++++++++++++++
4 files changed, 95 insertions(+)
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
index 5ac67263681ac..8dc5213dd8de2 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
@@ -57,6 +57,7 @@ class FactsGenerator : public ConstStmtVisitor<FactsGenerator> {
void VisitArraySubscriptExpr(const ArraySubscriptExpr *ASE);
void VisitCXXNewExpr(const CXXNewExpr *NE);
void VisitCXXDeleteExpr(const CXXDeleteExpr *DE);
+ void VisitStmtExpr(const StmtExpr *SE);
private:
OriginList *getOriginsList(const ValueDecl &D);
diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index 3861117005752..e054c38971c7f 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -780,6 +780,15 @@ void FactsGenerator::VisitCXXDeleteExpr(const CXXDeleteExpr *DE) {
FactMgr.createFact<InvalidateOriginFact>(List->getOuterOriginID(), DE));
}
+void FactsGenerator::VisitStmtExpr(const StmtExpr *SE) {
+ // Mark the value used here (its origins are shared with the final expression;
+ // see getOrCreateList). The CFG runs this point after the body's locals
+ // expire, so the use keeps that origin live across a body-local's expiry --
+ // the only way to catch such a borrow, since liveness comes only from a
+ // direct use, never backward through the flow that later delivers the value.
+ handleUse(SE);
+}
+
bool FactsGenerator::escapesViaReturn(OriginID OID) const {
return llvm::any_of(EscapesInCurrentBlock, [OID](const Fact *F) {
if (const auto *EF = F->getAs<ReturnEscapeFact>())
diff --git a/clang/lib/Analysis/LifetimeSafety/Origins.cpp b/clang/lib/Analysis/LifetimeSafety/Origins.cpp
index c837f246fa17b..f8cb4c60f6773 100644
--- a/clang/lib/Analysis/LifetimeSafety/Origins.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Origins.cpp
@@ -243,6 +243,17 @@ OriginList *OriginManager::getOrCreateList(const Expr *E) {
if (It != ExprToList.end())
return It->second;
+ // A statement expression (`({ ...; e; })`) yields `e`'s value: share `e`'s
+ // origins rather than flowing into a fresh one. The flow would run at the
+ // statement expression's program point, which the CFG places after the body's
+ // locals expire; sharing instead keeps the loan `e` produced in this origin
+ // from before those expiries. VisitStmtExpr adds the matching use.
+ if (const auto *SE = dyn_cast<StmtExpr>(E))
+ if (const CompoundStmt *CS = SE->getSubStmt(); CS && !CS->body_empty())
+ if (const auto *Last = dyn_cast<Expr>(CS->body_back()))
+ if (OriginList *List = getOrCreateList(Last))
+ return ExprToList[E] = List;
+
QualType Type = E->getType();
// Special handling for 'this' expressions to share origins with the method's
// implicit object parameter.
diff --git a/clang/test/Sema/LifetimeSafety/safety.cpp b/clang/test/Sema/LifetimeSafety/safety.cpp
index abd3d9c61b784..a37f5a5da046e 100644
--- a/clang/test/Sema/LifetimeSafety/safety.cpp
+++ b/clang/test/Sema/LifetimeSafety/safety.cpp
@@ -3895,3 +3895,77 @@ struct [[gsl::Pointer()]] PtrWithInt { int x; };
PtrWithInt f() {
return PtrWithInt{10};
}
+
+// A GNU statement expression (`({ ...; e; })`) yields the value of its final
+// expression `e`. Its origins are shared with `e`, and the value is used at the
+// statement expression's program point (after the body's locals expire), so a
+// borrow `e` carries is tracked: a borrow of a body-local is a use-after-scope,
+// and a borrow forwarded from an outer object propagates.
+namespace statement_expression {
+void use(int *p);
+
+// A borrow of a statement-expression-local escaping via the value.
+void borrow_of_local() {
+ int *p = ({ int x = 7; &x; }); // expected-warning {{local variable 'x' does not live long enough}} expected-note {{local variable 'x' is destroyed here}} expected-note {{later used here}}
+ use(p);
+}
+
+// An outer borrow forwarded through a statement expression and returned:
+// use-after-return.
+int *return_borrow_of_local() {
+ int local = 0;
+ return ({ (void)0; &local; }); // expected-warning {{stack memory associated with local variable 'local' is returned}} expected-note {{returned here}}
+}
+
+// A view bound to a temporary produced by the statement expression dangles.
+void borrow_temporary() {
+ std::string_view view = ({ std::string x = "long enough heap string!!!!!!"; x; }); // expected-warning {{temporary object does not live long enough}} expected-note {{temporary object is destroyed here}}
+ (void)view; // expected-note {{later used here}}
+}
+
+// Forwarding an outer borrow that dangles.
+void forward_outer_borrow() {
+ int *p;
+ {
+ int local = 0;
+ p = ({ (void)0; &local; }); // expected-warning {{local variable 'local' does not live long enough}}
+ } // expected-note {{local variable 'local' is destroyed here}}
+ use(p); // expected-note {{later used here}}
+}
+
+// The statement-expression result carries the borrow, so a `?:` sibling
+// supplying a valid loan no longer hides it via the merge.
+void masked(bool c) {
+ static int valid;
+ int *keep = &valid;
+ int *r;
+ {
+ int local = 0;
+ r = c ? keep : ({ &local; }); // expected-warning {{local variable 'local' does not live long enough}}
+ } // expected-note {{local variable 'local' is destroyed here}}
+ use(r); // expected-note {{later used here}}
+}
+
+// Both conditional arms are statement expressions borrowing a body-local; each
+// dangle is caught.
+int *conditional_arms(bool c) {
+ return c ? ({ int x = 7; &x; }) // expected-warning {{local variable 'x' does not live long enough}} expected-note {{local variable 'x' is destroyed here}} expected-note {{later used here}}
+ : ({ int y = 7; &y; }); // expected-warning {{local variable 'y' does not live long enough}} expected-note {{local variable 'y' is destroyed here}} expected-note {{later used here}}
+}
+
+// Negative: a statement expression yielding a long-lived borrow stays silent.
+void ok() {
+ static int s;
+ int *p = ({ int unused = 0; (void)unused; &s; });
+ use(p); // no-warning
+}
+
+// FIXME: A discarded statement expression over-reports. The value is marked
+// used at the statement expression's program point regardless of whether it is
+// consumed, so a borrow of a body-local is flagged even though the address is
+// discarded and never read. Gating the injected use on the result being
+// consumed would fix this.
+void discarded_body_local() {
+ (void)({ int x = 7; &x; }); // expected-warning {{local variable 'x' does not live long enough}} expected-note {{local variable 'x' is destroyed here}} expected-note {{later used here}}
+}
+} // namespace statement_expression
More information about the cfe-commits
mailing list