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 <[email protected]> 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 _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
