[clang] [LifetimeSafety] Handle escape through assignment to global storage (PR #181646)
Abhinav Pradeep via cfe-commits
cfe-commits at lists.llvm.org
Thu Mar 5 03:58:29 PST 2026
https://github.com/AbhinavPradeep updated https://github.com/llvm/llvm-project/pull/181646
>From 11b0f9865cdf2d5b7358d8e10d3dbeecceeb45af Mon Sep 17 00:00:00 2001
From: Abhinav Pradeep <abhinav.pradeep at oracle.com>
Date: Mon, 16 Feb 2026 22:10:24 +1000
Subject: [PATCH 1/6] Create and emit GlobalEscapeFact.
---
.../Analysis/Analyses/LifetimeSafety/Facts.h | 20 +++++++++++++++++-
.../Analyses/LifetimeSafety/LifetimeSafety.h | 4 ++++
clang/lib/Analysis/LifetimeSafety/Checker.cpp | 3 +++
clang/lib/Analysis/LifetimeSafety/Facts.cpp | 8 +++++++
.../LifetimeSafety/FactsGenerator.cpp | 21 +++++++++++++++++++
.../Analysis/LifetimeSafety/LiveOrigins.cpp | 2 ++
6 files changed, 57 insertions(+), 1 deletion(-)
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
index f9d55991f2e09..f797e93fdd85a 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
@@ -14,6 +14,7 @@
#ifndef LLVM_CLANG_ANALYSIS_ANALYSES_LIFETIMESAFETY_FACTS_H
#define LLVM_CLANG_ANALYSIS_ANALYSES_LIFETIMESAFETY_FACTS_H
+#include "clang/AST/Decl.h"
#include "clang/Analysis/Analyses/LifetimeSafety/Loans.h"
#include "clang/Analysis/Analyses/LifetimeSafety/Origins.h"
#include "clang/Analysis/Analyses/LifetimeSafety/Utils.h"
@@ -151,7 +152,7 @@ class OriginEscapesFact : public Fact {
enum class EscapeKind : uint8_t {
Return, /// Escapes via return statement.
Field, /// Escapes via assignment to a field.
- // FIXME: Add support for escape to global (dangling global ptr).
+ Global, /// Escapes via assignment to global storage.
} EscKind;
static bool classof(const Fact *F) {
@@ -202,6 +203,23 @@ class FieldEscapeFact : public OriginEscapesFact {
const OriginManager &OM) const override;
};
+class GlobalEscapeFact : public OriginEscapesFact {
+ const VarDecl *VDecl;
+
+public:
+ GlobalEscapeFact(OriginID OID, const VarDecl *VDecl)
+ : OriginEscapesFact(OID, EscapeKind::Global), VDecl(VDecl) {}
+
+ static bool classof(const Fact *F) {
+ return F->getKind() == Kind::OriginEscapes &&
+ static_cast<const OriginEscapesFact *>(F)->getEscapeKind() ==
+ EscapeKind::Global;
+ }
+ const VarDecl *getVarDecl() const { return VDecl; };
+ void dump(llvm::raw_ostream &OS, const LoanManager &,
+ const OriginManager &OM) const override;
+};
+
class UseFact : public Fact {
const Expr *UseExpr;
const OriginList *OList;
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h
index d7aadf4cf04ca..3f16efcbb1817 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h
@@ -104,6 +104,10 @@ class LifetimeSafetySemaHelper {
// Reports misuse of [[clang::noescape]] when parameter escapes through field
virtual void reportNoescapeViolation(const ParmVarDecl *ParmWithNoescape,
const FieldDecl *EscapeField) {}
+ // Reports misuse of [[clang::noescape]] when parameter escapes through
+ // assignment to a global variable
+ virtual void reportNoescapeViolation(const ParmVarDecl *ParmWithNoescape,
+ const ValueDecl *EscapeGlobal) {}
// Suggests lifetime bound annotations for implicit this.
virtual void suggestLifetimeboundToImplicitThis(SuggestionScope Scope,
diff --git a/clang/lib/Analysis/LifetimeSafety/Checker.cpp b/clang/lib/Analysis/LifetimeSafety/Checker.cpp
index 78c2a6dba3eb6..3daeba353ca2a 100644
--- a/clang/lib/Analysis/LifetimeSafety/Checker.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Checker.cpp
@@ -270,6 +270,9 @@ class LifetimeChecker {
else if (const auto *FieldEscape = dyn_cast<FieldEscapeFact>(OEF))
SemaHelper->reportDanglingField(
IssueExpr, FieldEscape->getFieldDecl(), MovedExpr, ExpiryLoc);
+ else if (const auto *GlobalEscape = dyn_cast<GlobalEscapeFact>(OEF))
+ // Wire up
+ ;
else
llvm_unreachable("Unhandled OriginEscapesFact type");
} else
diff --git a/clang/lib/Analysis/LifetimeSafety/Facts.cpp b/clang/lib/Analysis/LifetimeSafety/Facts.cpp
index c963d9c45fa9d..fca10310824ea 100644
--- a/clang/lib/Analysis/LifetimeSafety/Facts.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Facts.cpp
@@ -8,6 +8,7 @@
#include "clang/Analysis/Analyses/LifetimeSafety/Facts.h"
#include "clang/AST/Decl.h"
+#include "clang/AST/DeclID.h"
#include "clang/Analysis/Analyses/PostOrderCFGView.h"
#include "clang/Analysis/FlowSensitive/DataflowWorklist.h"
#include "llvm/ADT/STLFunctionalExtras.h"
@@ -68,6 +69,13 @@ void FieldEscapeFact::dump(llvm::raw_ostream &OS, const LoanManager &,
OS << ", via Field)\n";
}
+void GlobalEscapeFact::dump(llvm::raw_ostream &OS, const LoanManager &,
+ const OriginManager &OM) const {
+ OS << "OriginEscapes (";
+ OM.dump(getEscapedOriginID(), OS);
+ OS << ", via Global)\n";
+}
+
void UseFact::dump(llvm::raw_ostream &OS, const LoanManager &,
const OriginManager &OM) const {
OS << "Use (";
diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index f39d677758393..686000897969a 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -9,6 +9,7 @@
#include <cassert>
#include <string>
+#include "clang/AST/Decl.h"
#include "clang/AST/DeclCXX.h"
#include "clang/AST/Expr.h"
#include "clang/AST/ExprCXX.h"
@@ -325,10 +326,17 @@ void FactsGenerator::handleAssignment(const Expr *LHSExpr,
const Expr *RHSExpr) {
LHSExpr = LHSExpr->IgnoreParenImpCasts();
OriginList *LHSList = nullptr;
+ const VarDecl *GlobalLHS = nullptr;
if (const auto *DRE_LHS = dyn_cast<DeclRefExpr>(LHSExpr)) {
LHSList = getOriginsList(*DRE_LHS);
assert(LHSList && "LHS is a DRE and should have an origin list");
+ // Check if we are assigning to a global variable
+ if (const VarDecl *VarD = dyn_cast<VarDecl>(DRE_LHS->getDecl())) {
+ if (VarD->hasGlobalStorage()) {
+ GlobalLHS = VarD;
+ };
+ };
}
// Handle assignment to member fields (e.g., `this->view = s` or `view = s`).
// This enables detection of dangling fields when local values escape to
@@ -336,6 +344,12 @@ void FactsGenerator::handleAssignment(const Expr *LHSExpr,
if (const auto *ME_LHS = dyn_cast<MemberExpr>(LHSExpr)) {
LHSList = getOriginsList(*ME_LHS);
assert(LHSList && "LHS is a MemberExpr and should have an origin list");
+ // Check if we are assigning to a static data member
+ if (const VarDecl *VarD = dyn_cast<VarDecl>(ME_LHS->getMemberDecl())) {
+ if (VarD->hasGlobalStorage()) {
+ GlobalLHS = VarD;
+ };
+ };
}
if (!LHSList)
return;
@@ -347,6 +361,13 @@ void FactsGenerator::handleAssignment(const Expr *LHSExpr,
// assigned.
RHSList = getRValueOrigins(RHSExpr, RHSList);
+ if (GlobalLHS) {
+ for (OriginList *L = RHSList; L != nullptr; L = L->peelOuterOrigin()) {
+ EscapesInCurrentBlock.push_back(FactMgr.createFact<GlobalEscapeFact>(
+ L->getOuterOriginID(), GlobalLHS));
+ }
+ };
+
if (const auto *DRE_LHS = dyn_cast<DeclRefExpr>(LHSExpr))
markUseAsWrite(DRE_LHS);
// Kill the old loans of the destination origin and flow the new loans
diff --git a/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp b/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp
index f210fb4d752d4..f818266fcda88 100644
--- a/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp
@@ -62,6 +62,8 @@ static SourceLocation GetFactLoc(CausingFactType F) {
return ReturnEsc->getReturnExpr()->getExprLoc();
if (auto *FieldEsc = dyn_cast<FieldEscapeFact>(OEF))
return FieldEsc->getFieldDecl()->getLocation();
+ if (auto *GlobalEsc = dyn_cast<GlobalEscapeFact>(OEF))
+ return GlobalEsc->getVarDecl()->getLocation();
}
llvm_unreachable("unhandled causing fact in PointerUnion");
}
>From 0df84c372a946b4559f0f683cb828a57093371f2 Mon Sep 17 00:00:00 2001
From: Abhinav Pradeep <abhinav.pradeep at oracle.com>
Date: Wed, 25 Feb 2026 21:30:34 +1000
Subject: [PATCH 2/6] Fixed where the fact is emitted and plugged into the
warning reporting
---
.../Analysis/Analyses/LifetimeSafety/Facts.h | 10 ++++---
.../Analyses/LifetimeSafety/LifetimeSafety.h | 9 ++++--
clang/include/clang/Basic/DiagnosticGroups.td | 18 ++++++++++-
.../clang/Basic/DiagnosticSemaKinds.td | 12 ++++++++
clang/lib/Analysis/LifetimeSafety/Checker.cpp | 12 ++++++--
clang/lib/Analysis/LifetimeSafety/Facts.cpp | 2 +-
.../LifetimeSafety/FactsGenerator.cpp | 30 ++++++-------------
.../Analysis/LifetimeSafety/LiveOrigins.cpp | 2 +-
clang/lib/Sema/AnalysisBasedWarnings.cpp | 28 +++++++++++++++++
.../Sema/warn-lifetime-safety-noescape.cpp | 5 ++--
10 files changed, 92 insertions(+), 36 deletions(-)
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
index f797e93fdd85a..e4fbe07b3e9bd 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
@@ -152,7 +152,7 @@ class OriginEscapesFact : public Fact {
enum class EscapeKind : uint8_t {
Return, /// Escapes via return statement.
Field, /// Escapes via assignment to a field.
- Global, /// Escapes via assignment to global storage.
+ Global, /// Escapes via assignment to global storage.
} EscKind;
static bool classof(const Fact *F) {
@@ -203,19 +203,21 @@ class FieldEscapeFact : public OriginEscapesFact {
const OriginManager &OM) const override;
};
+/// Represents that an origin escapes via assignment to global storage.
+/// Example: `global_storage = local_var;`
class GlobalEscapeFact : public OriginEscapesFact {
- const VarDecl *VDecl;
+ const VarDecl *Global;
public:
GlobalEscapeFact(OriginID OID, const VarDecl *VDecl)
- : OriginEscapesFact(OID, EscapeKind::Global), VDecl(VDecl) {}
+ : OriginEscapesFact(OID, EscapeKind::Global), Global(VDecl) {}
static bool classof(const Fact *F) {
return F->getKind() == Kind::OriginEscapes &&
static_cast<const OriginEscapesFact *>(F)->getEscapeKind() ==
EscapeKind::Global;
}
- const VarDecl *getVarDecl() const { return VDecl; };
+ const VarDecl *getGlobal() const { return Global; };
void dump(llvm::raw_ostream &OS, const LoanManager &,
const OriginManager &OM) const override;
};
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h
index 3f16efcbb1817..2bcd0ddd7a16c 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h
@@ -84,6 +84,11 @@ class LifetimeSafetySemaHelper {
const Expr *MovedExpr,
SourceLocation ExpiryLoc) {}
+ virtual void reportDanglingGlobal(const Expr *IssueExpr,
+ const VarDecl *DanglingGlobal,
+ const Expr *MovedExpr,
+ SourceLocation ExpiryLoc) {}
+
// Reports when a reference/iterator is used after the container operation
// that invalidated it.
virtual void reportUseAfterInvalidation(const Expr *IssueExpr,
@@ -104,10 +109,10 @@ class LifetimeSafetySemaHelper {
// Reports misuse of [[clang::noescape]] when parameter escapes through field
virtual void reportNoescapeViolation(const ParmVarDecl *ParmWithNoescape,
const FieldDecl *EscapeField) {}
- // Reports misuse of [[clang::noescape]] when parameter escapes through
+ // Reports misuse of [[clang::noescape]] when parameter escapes through
// assignment to a global variable
virtual void reportNoescapeViolation(const ParmVarDecl *ParmWithNoescape,
- const ValueDecl *EscapeGlobal) {}
+ const VarDecl *EscapeGlobal) {}
// Suggests lifetime bound annotations for implicit this.
virtual void suggestLifetimeboundToImplicitThis(SuggestionScope Scope,
diff --git a/clang/include/clang/Basic/DiagnosticGroups.td b/clang/include/clang/Basic/DiagnosticGroups.td
index 7df83d2a4011f..72a50f4141e27 100644
--- a/clang/include/clang/Basic/DiagnosticGroups.td
+++ b/clang/include/clang/Basic/DiagnosticGroups.td
@@ -595,6 +595,19 @@ This may contain false-positives, e.g. when the borrowed storage is potentially
}];
}
+def LifetimeSafetyDanglingGlobal : DiagGroup<"lifetime-safety-dangling-global"> {
+ code Documentation = [{
+Warning to detect dangling global references.
+ }];
+}
+
+def LifetimeSafetyDanglingGlobalMoved : DiagGroup<"lifetime-safety-dangling-global-moved"> {
+ code Documentation = [{
+Warning to detect dangling global references.
+This may contain false-positives, e.g. when the borrowed storage is potentially moved and is not destroyed at function exit.
+ }];
+}
+
def LifetimeSafetyInvalidation : DiagGroup<"lifetime-safety-invalidation"> {
code Documentation = [{
Warning to detect invalidation of references.
@@ -604,11 +617,14 @@ Warning to detect invalidation of references.
def LifetimeSafetyPermissive : DiagGroup<"lifetime-safety-permissive",
[LifetimeSafetyUseAfterScope,
LifetimeSafetyReturnStackAddr,
- LifetimeSafetyDanglingField]>;
+ LifetimeSafetyDanglingField,
+ LifetimeSafetyDanglingGlobal]>;
+
def LifetimeSafetyStrict : DiagGroup<"lifetime-safety-strict",
[LifetimeSafetyUseAfterScopeMoved,
LifetimeSafetyReturnStackAddrMoved,
LifetimeSafetyDanglingFieldMoved,
+ LifetimeSafetyDanglingGlobal,
LifetimeSafetyInvalidation]>;
def LifetimeSafety : DiagGroup<"lifetime-safety",
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index 68016ec4d58a3..2d791bf3ce447 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -10931,6 +10931,16 @@ def warn_lifetime_safety_dangling_field_moved
"Consider moving first and then aliasing later to resolve the issue">,
InGroup<LifetimeSafetyDanglingFieldMoved>,
DefaultIgnore;
+def warn_lifetime_safety_dangling_global
+ : Warning<"address of stack memory escapes to a global">,
+ InGroup<LifetimeSafetyDanglingGlobal>,
+ DefaultIgnore;
+def warn_lifetime_safety_dangling_global_moved
+ : Warning<"address of stack memory escapes to a global. "
+ "This could be a false positive as the storage may have been moved. "
+ "Consider moving first and then aliasing later to resolve the issue">,
+ InGroup<LifetimeSafetyDanglingGlobalMoved>,
+ DefaultIgnore;
def note_lifetime_safety_used_here : Note<"later used here">;
def note_lifetime_safety_invalidated_here : Note<"invalidated here">;
@@ -10938,7 +10948,9 @@ def note_lifetime_safety_destroyed_here : Note<"destroyed here">;
def note_lifetime_safety_returned_here : Note<"returned here">;
def note_lifetime_safety_moved_here : Note<"potentially moved here">;
def note_lifetime_safety_dangling_field_here: Note<"this field dangles">;
+def note_lifetime_safety_dangling_global_here: Note<"this global dangles">;
def note_lifetime_safety_escapes_to_field_here: Note<"escapes to this field">;
+def note_lifetime_safety_escapes_to_global_here: Note<"escapes to this global">;
def warn_lifetime_safety_intra_tu_param_suggestion
: Warning<"parameter in intra-TU function should be marked "
diff --git a/clang/lib/Analysis/LifetimeSafety/Checker.cpp b/clang/lib/Analysis/LifetimeSafety/Checker.cpp
index 3daeba353ca2a..7399fa1c2dbd2 100644
--- a/clang/lib/Analysis/LifetimeSafety/Checker.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Checker.cpp
@@ -25,6 +25,7 @@
#include "clang/Basic/SourceManager.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/DenseSet.h"
+#include "llvm/Support/Casting.h"
#include "llvm/Support/ErrorHandling.h"
#include "llvm/Support/TimeProfiler.h"
@@ -55,7 +56,8 @@ struct PendingWarning {
using AnnotationTarget =
llvm::PointerUnion<const ParmVarDecl *, const CXXMethodDecl *>;
-using EscapingTarget = llvm::PointerUnion<const Expr *, const FieldDecl *>;
+using EscapingTarget =
+ llvm::PointerUnion<const Expr *, const FieldDecl *, const VarDecl *>;
class LifetimeChecker {
private:
@@ -109,6 +111,8 @@ class LifetimeChecker {
NoescapeWarningsMap.try_emplace(PVD, ReturnEsc->getReturnExpr());
if (auto *FieldEsc = dyn_cast<FieldEscapeFact>(OEF))
NoescapeWarningsMap.try_emplace(PVD, FieldEsc->getFieldDecl());
+ if (auto *GlobalEsc = dyn_cast<GlobalEscapeFact>(OEF))
+ NoescapeWarningsMap.try_emplace(PVD, GlobalEsc->getGlobal());
return;
}
// Suggest lifetimebound for parameter escaping through return.
@@ -271,8 +275,8 @@ class LifetimeChecker {
SemaHelper->reportDanglingField(
IssueExpr, FieldEscape->getFieldDecl(), MovedExpr, ExpiryLoc);
else if (const auto *GlobalEscape = dyn_cast<GlobalEscapeFact>(OEF))
- // Wire up
- ;
+ SemaHelper->reportDanglingGlobal(IssueExpr, GlobalEscape->getGlobal(),
+ MovedExpr, ExpiryLoc);
else
llvm_unreachable("Unhandled OriginEscapesFact type");
} else
@@ -345,6 +349,8 @@ class LifetimeChecker {
SemaHelper->reportNoescapeViolation(PVD, E);
else if (const auto *FD = EscapeTarget.dyn_cast<const FieldDecl *>())
SemaHelper->reportNoescapeViolation(PVD, FD);
+ else if (const auto *G = EscapeTarget.dyn_cast<const VarDecl *>())
+ SemaHelper->reportNoescapeViolation(PVD, G);
else
llvm_unreachable("Unhandled EscapingTarget type");
}
diff --git a/clang/lib/Analysis/LifetimeSafety/Facts.cpp b/clang/lib/Analysis/LifetimeSafety/Facts.cpp
index fca10310824ea..4ffc8b4195949 100644
--- a/clang/lib/Analysis/LifetimeSafety/Facts.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Facts.cpp
@@ -70,7 +70,7 @@ void FieldEscapeFact::dump(llvm::raw_ostream &OS, const LoanManager &,
}
void GlobalEscapeFact::dump(llvm::raw_ostream &OS, const LoanManager &,
- const OriginManager &OM) const {
+ const OriginManager &OM) const {
OS << "OriginEscapes (";
OM.dump(getEscapedOriginID(), OS);
OS << ", via Global)\n";
diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index 686000897969a..8238cf69edfcd 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -326,17 +326,10 @@ void FactsGenerator::handleAssignment(const Expr *LHSExpr,
const Expr *RHSExpr) {
LHSExpr = LHSExpr->IgnoreParenImpCasts();
OriginList *LHSList = nullptr;
- const VarDecl *GlobalLHS = nullptr;
if (const auto *DRE_LHS = dyn_cast<DeclRefExpr>(LHSExpr)) {
LHSList = getOriginsList(*DRE_LHS);
assert(LHSList && "LHS is a DRE and should have an origin list");
- // Check if we are assigning to a global variable
- if (const VarDecl *VarD = dyn_cast<VarDecl>(DRE_LHS->getDecl())) {
- if (VarD->hasGlobalStorage()) {
- GlobalLHS = VarD;
- };
- };
}
// Handle assignment to member fields (e.g., `this->view = s` or `view = s`).
// This enables detection of dangling fields when local values escape to
@@ -344,12 +337,6 @@ void FactsGenerator::handleAssignment(const Expr *LHSExpr,
if (const auto *ME_LHS = dyn_cast<MemberExpr>(LHSExpr)) {
LHSList = getOriginsList(*ME_LHS);
assert(LHSList && "LHS is a MemberExpr and should have an origin list");
- // Check if we are assigning to a static data member
- if (const VarDecl *VarD = dyn_cast<VarDecl>(ME_LHS->getMemberDecl())) {
- if (VarD->hasGlobalStorage()) {
- GlobalLHS = VarD;
- };
- };
}
if (!LHSList)
return;
@@ -361,13 +348,6 @@ void FactsGenerator::handleAssignment(const Expr *LHSExpr,
// assigned.
RHSList = getRValueOrigins(RHSExpr, RHSList);
- if (GlobalLHS) {
- for (OriginList *L = RHSList; L != nullptr; L = L->peelOuterOrigin()) {
- EscapesInCurrentBlock.push_back(FactMgr.createFact<GlobalEscapeFact>(
- L->getOuterOriginID(), GlobalLHS));
- }
- };
-
if (const auto *DRE_LHS = dyn_cast<DeclRefExpr>(LHSExpr))
markUseAsWrite(DRE_LHS);
// Kill the old loans of the destination origin and flow the new loans
@@ -490,11 +470,19 @@ void FactsGenerator::handleFullExprCleanup(
}
void FactsGenerator::handleExitBlock() {
- // Creates FieldEscapeFacts for all field origins that remain live at exit.
for (const Origin &O : FactMgr.getOriginMgr().getOrigins())
if (auto *FD = dyn_cast_if_present<FieldDecl>(O.getDecl()))
+ // Create FieldEscapeFacts for all field origins that remain live at exit.
EscapesInCurrentBlock.push_back(
FactMgr.createFact<FieldEscapeFact>(O.ID, FD));
+ else if (auto *VD = dyn_cast_if_present<VarDecl>(O.getDecl())) {
+ // Create GlobalEscapeFacts for all origins with global-storage that
+ // remain live at exit.
+ if (VD->hasGlobalStorage()) {
+ EscapesInCurrentBlock.push_back(
+ FactMgr.createFact<GlobalEscapeFact>(O.ID, VD));
+ }
+ }
}
void FactsGenerator::handleGSLPointerConstruction(const CXXConstructExpr *CCE) {
diff --git a/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp b/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp
index f818266fcda88..fe20d779669c1 100644
--- a/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp
@@ -63,7 +63,7 @@ static SourceLocation GetFactLoc(CausingFactType F) {
if (auto *FieldEsc = dyn_cast<FieldEscapeFact>(OEF))
return FieldEsc->getFieldDecl()->getLocation();
if (auto *GlobalEsc = dyn_cast<GlobalEscapeFact>(OEF))
- return GlobalEsc->getVarDecl()->getLocation();
+ return GlobalEsc->getGlobal()->getLocation();
}
llvm_unreachable("unhandled causing fact in PointerUnion");
}
diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp
index 20c41096501fb..9769a6d678dcd 100644
--- a/clang/lib/Sema/AnalysisBasedWarnings.cpp
+++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp
@@ -2888,6 +2888,7 @@ class LifetimeSafetySemaHelperImpl : public LifetimeSafetySemaHelper {
S.Diag(ReturnExpr->getExprLoc(), diag::note_lifetime_safety_returned_here)
<< ReturnExpr->getSourceRange();
}
+
void reportDanglingField(const Expr *IssueExpr,
const FieldDecl *DanglingField,
const Expr *MovedExpr,
@@ -2904,6 +2905,22 @@ class LifetimeSafetySemaHelperImpl : public LifetimeSafetySemaHelper {
<< DanglingField->getEndLoc();
}
+ void reportDanglingGlobal(const Expr *IssueExpr,
+ const VarDecl *DanglingGlobal,
+ const Expr *MovedExpr,
+ SourceLocation ExpiryLoc) override {
+ S.Diag(IssueExpr->getExprLoc(),
+ MovedExpr ? diag::warn_lifetime_safety_dangling_global_moved
+ : diag::warn_lifetime_safety_dangling_global)
+ << IssueExpr->getSourceRange();
+ if (MovedExpr)
+ S.Diag(MovedExpr->getExprLoc(), diag::note_lifetime_safety_moved_here)
+ << MovedExpr->getSourceRange();
+ S.Diag(DanglingGlobal->getLocation(),
+ diag::note_lifetime_safety_dangling_global_here)
+ << DanglingGlobal->getEndLoc();
+ }
+
void reportUseAfterInvalidation(const Expr *IssueExpr, const Expr *UseExpr,
const Expr *InvalidationExpr) override {
S.Diag(IssueExpr->getExprLoc(), diag::warn_lifetime_safety_invalidation)
@@ -3005,6 +3022,17 @@ class LifetimeSafetySemaHelperImpl : public LifetimeSafetySemaHelper {
<< EscapeField->getEndLoc();
}
+ void reportNoescapeViolation(const ParmVarDecl *ParmWithNoescape,
+ const VarDecl *EscapeGlobal) override {
+ S.Diag(ParmWithNoescape->getBeginLoc(),
+ diag::warn_lifetime_safety_noescape_escapes)
+ << ParmWithNoescape->getSourceRange();
+
+ S.Diag(EscapeGlobal->getLocation(),
+ diag::note_lifetime_safety_escapes_to_global_here)
+ << EscapeGlobal->getEndLoc();
+ }
+
void addLifetimeBoundToImplicitThis(const CXXMethodDecl *MD) override {
S.addLifetimeBoundToImplicitThis(const_cast<CXXMethodDecl *>(MD));
}
diff --git a/clang/test/Sema/warn-lifetime-safety-noescape.cpp b/clang/test/Sema/warn-lifetime-safety-noescape.cpp
index ee661add0acc8..36542a2e15977 100644
--- a/clang/test/Sema/warn-lifetime-safety-noescape.cpp
+++ b/clang/test/Sema/warn-lifetime-safety-noescape.cpp
@@ -113,10 +113,9 @@ View escape_through_unannotated_call(const MyObj& in [[clang::noescape]]) { // e
return no_annotation_identity(in); // expected-note {{returned here}}
}
-View global_view;
+View global_view; // expected-note {{escapes to this global}}
-// FIXME: Escaping through a global variable is not detected.
-void escape_through_global_var(const MyObj& in [[clang::noescape]]) {
+void escape_through_global_var(const MyObj& in [[clang::noescape]]) { // expected-warning {{parameter is marked [[clang::noescape]] but escapes}}
global_view = in;
}
>From 5a555e880829f8e829298a47086c5b197e8ee339 Mon Sep 17 00:00:00 2001
From: Abhinav Pradeep <abhinav.pradeep at oracle.com>
Date: Fri, 27 Feb 2026 16:56:06 +1000
Subject: [PATCH 3/6] Added tests and rephrased reporting a bit
---
.../clang/Basic/DiagnosticSemaKinds.td | 2 +-
.../Sema/warn-lifetime-safety-noescape.cpp | 22 ++++++++++++++++++-
2 files changed, 22 insertions(+), 2 deletions(-)
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index 2d791bf3ce447..b3c44eb1a120a 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -10950,7 +10950,7 @@ def note_lifetime_safety_moved_here : Note<"potentially moved here">;
def note_lifetime_safety_dangling_field_here: Note<"this field dangles">;
def note_lifetime_safety_dangling_global_here: Note<"this global dangles">;
def note_lifetime_safety_escapes_to_field_here: Note<"escapes to this field">;
-def note_lifetime_safety_escapes_to_global_here: Note<"escapes to this global">;
+def note_lifetime_safety_escapes_to_global_here: Note<"escapes to this global storage">;
def warn_lifetime_safety_intra_tu_param_suggestion
: Warning<"parameter in intra-TU function should be marked "
diff --git a/clang/test/Sema/warn-lifetime-safety-noescape.cpp b/clang/test/Sema/warn-lifetime-safety-noescape.cpp
index 36542a2e15977..03f4c24a0956f 100644
--- a/clang/test/Sema/warn-lifetime-safety-noescape.cpp
+++ b/clang/test/Sema/warn-lifetime-safety-noescape.cpp
@@ -113,11 +113,31 @@ View escape_through_unannotated_call(const MyObj& in [[clang::noescape]]) { // e
return no_annotation_identity(in); // expected-note {{returned here}}
}
-View global_view; // expected-note {{escapes to this global}}
+View global_view; // expected-note {{escapes to this global storage}}
void escape_through_global_var(const MyObj& in [[clang::noescape]]) { // expected-warning {{parameter is marked [[clang::noescape]] but escapes}}
global_view = in;
}
+struct ObjWithStaticField {
+ static int *static_field; // expected-note {{escapes to this global storage}}
+};
+
+void escape_to_static_data_member(int *data [[clang::noescape]]) { // expected-warning {{parameter is marked [[clang::noescape]] but escapes}}
+ ObjWithStaticField::static_field = data;
+}
+
+
+
+void escape_through_static_local(int *data [[clang::noescape]]) { // expected-warning {{parameter is marked [[clang::noescape]] but escapes}}
+ static int *static_local; // expected-note {{escapes to this global storage}}
+ static_local = data;
+}
+
+thread_local int *thread_local_storage; // expected-note {{escapes to this global storage}}
+
+void escape_through_thread_local(int *data [[clang::noescape]]) { // expected-warning {{parameter is marked [[clang::noescape]] but escapes}}
+ thread_local_storage = data;
+}
struct ObjConsumer {
void escape_through_member(const MyObj& in [[clang::noescape]]) { // expected-warning {{parameter is marked [[clang::noescape]] but escapes}}
>From 0a8cbb523b9c04218f92750f40b58ba59eb7d28c Mon Sep 17 00:00:00 2001
From: Abhinav Pradeep <abhinav.pradeep at oracle.com>
Date: Tue, 3 Mar 2026 14:30:04 +1000
Subject: [PATCH 4/6] Added more dangling global tests.
---
.../warn-lifetime-safety-dangling-global.cpp | 44 +++++++++++++++++++
1 file changed, 44 insertions(+)
create mode 100644 clang/test/Sema/warn-lifetime-safety-dangling-global.cpp
diff --git a/clang/test/Sema/warn-lifetime-safety-dangling-global.cpp b/clang/test/Sema/warn-lifetime-safety-dangling-global.cpp
new file mode 100644
index 0000000000000..b40e7d23f90b9
--- /dev/null
+++ b/clang/test/Sema/warn-lifetime-safety-dangling-global.cpp
@@ -0,0 +1,44 @@
+// RUN: %clang_cc1 -fsyntax-only -Wlifetime-safety -Wno-dangling -verify %s
+
+int *global; // expected-note {{this global dangles}}
+int *global_backup; // expected-note {{this global dangles}}
+
+struct ObjWithStaticField {
+ static int *static_field; // expected-note {{this global dangles}}
+};
+
+void save_global() {
+ global_backup = global;
+}
+
+// Here, by action of save_global, we have that global_backup points to stack memory. This is currently not caught.
+void invoke_function_with_side_effects() {
+ int local;
+ global = &local;
+ save_global();
+ global = nullptr;
+}
+
+// We can however catch the inlined one of course!
+void inlined() {
+ int local;
+ global = &local; // expected-warning {{address of stack memory escapes to a global}}
+ global_backup = global;
+ global = nullptr;
+}
+
+void store_local_in_global() {
+ int local;
+ global = &local; // expected-warning {{address of stack memory escapes to a global}}
+}
+
+void store_then_clear() {
+ int local;
+ global = &local;
+ global = nullptr;
+}
+
+void dangling_static_field() {
+ int local;
+ ObjWithStaticField::static_field = &local; // expected-warning {{address of stack memory escapes to a global}}
+}
\ No newline at end of file
>From 95e39b77a76e608fba9a327ada9ea053aaec2d4f Mon Sep 17 00:00:00 2001
From: Abhinav Pradeep <abhinav.pradeep at oracle.com>
Date: Thu, 5 Mar 2026 21:33:45 +1000
Subject: [PATCH 5/6] Added FIXME and improved reporting.
---
.../clang/Basic/DiagnosticSemaKinds.td | 6 +++--
clang/lib/Sema/AnalysisBasedWarnings.cpp | 23 +++++++++++++------
.../warn-lifetime-safety-dangling-global.cpp | 8 +++----
.../Sema/warn-lifetime-safety-noescape.cpp | 7 ++++--
4 files changed, 29 insertions(+), 15 deletions(-)
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index b3c44eb1a120a..f866c8915e759 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -10932,11 +10932,11 @@ def warn_lifetime_safety_dangling_field_moved
InGroup<LifetimeSafetyDanglingFieldMoved>,
DefaultIgnore;
def warn_lifetime_safety_dangling_global
- : Warning<"address of stack memory escapes to a global">,
+ : Warning<"address of stack memory escapes to global or static storage">,
InGroup<LifetimeSafetyDanglingGlobal>,
DefaultIgnore;
def warn_lifetime_safety_dangling_global_moved
- : Warning<"address of stack memory escapes to a global. "
+ : Warning<"address of stack memory escapes to global or static storage. "
"This could be a false positive as the storage may have been moved. "
"Consider moving first and then aliasing later to resolve the issue">,
InGroup<LifetimeSafetyDanglingGlobalMoved>,
@@ -10949,8 +10949,10 @@ def note_lifetime_safety_returned_here : Note<"returned here">;
def note_lifetime_safety_moved_here : Note<"potentially moved here">;
def note_lifetime_safety_dangling_field_here: Note<"this field dangles">;
def note_lifetime_safety_dangling_global_here: Note<"this global dangles">;
+def note_lifetime_safety_dangling_static_here: Note<"this static storage dangles">;
def note_lifetime_safety_escapes_to_field_here: Note<"escapes to this field">;
def note_lifetime_safety_escapes_to_global_here: Note<"escapes to this global storage">;
+def note_lifetime_safety_escapes_to_static_storage_here: Note<"escapes to this static storage">;
def warn_lifetime_safety_intra_tu_param_suggestion
: Warning<"parameter in intra-TU function should be marked "
diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp
index 9769a6d678dcd..daa31074ab25b 100644
--- a/clang/lib/Sema/AnalysisBasedWarnings.cpp
+++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp
@@ -2916,9 +2916,14 @@ class LifetimeSafetySemaHelperImpl : public LifetimeSafetySemaHelper {
if (MovedExpr)
S.Diag(MovedExpr->getExprLoc(), diag::note_lifetime_safety_moved_here)
<< MovedExpr->getSourceRange();
- S.Diag(DanglingGlobal->getLocation(),
- diag::note_lifetime_safety_dangling_global_here)
- << DanglingGlobal->getEndLoc();
+ if (DanglingGlobal->isStaticLocal() || DanglingGlobal->isStaticDataMember())
+ S.Diag(DanglingGlobal->getLocation(),
+ diag::note_lifetime_safety_dangling_static_here)
+ << DanglingGlobal->getEndLoc();
+ else
+ S.Diag(DanglingGlobal->getLocation(),
+ diag::note_lifetime_safety_dangling_global_here)
+ << DanglingGlobal->getEndLoc();
}
void reportUseAfterInvalidation(const Expr *IssueExpr, const Expr *UseExpr,
@@ -3027,10 +3032,14 @@ class LifetimeSafetySemaHelperImpl : public LifetimeSafetySemaHelper {
S.Diag(ParmWithNoescape->getBeginLoc(),
diag::warn_lifetime_safety_noescape_escapes)
<< ParmWithNoescape->getSourceRange();
-
- S.Diag(EscapeGlobal->getLocation(),
- diag::note_lifetime_safety_escapes_to_global_here)
- << EscapeGlobal->getEndLoc();
+ if (EscapeGlobal->isStaticLocal() || EscapeGlobal->isStaticDataMember())
+ S.Diag(EscapeGlobal->getLocation(),
+ diag::note_lifetime_safety_escapes_to_static_storage_here)
+ << EscapeGlobal->getEndLoc();
+ else
+ S.Diag(EscapeGlobal->getLocation(),
+ diag::note_lifetime_safety_escapes_to_global_here)
+ << EscapeGlobal->getEndLoc();
}
void addLifetimeBoundToImplicitThis(const CXXMethodDecl *MD) override {
diff --git a/clang/test/Sema/warn-lifetime-safety-dangling-global.cpp b/clang/test/Sema/warn-lifetime-safety-dangling-global.cpp
index b40e7d23f90b9..ac40c7df5a8f5 100644
--- a/clang/test/Sema/warn-lifetime-safety-dangling-global.cpp
+++ b/clang/test/Sema/warn-lifetime-safety-dangling-global.cpp
@@ -4,7 +4,7 @@ int *global; // expected-note {{this global dangles}}
int *global_backup; // expected-note {{this global dangles}}
struct ObjWithStaticField {
- static int *static_field; // expected-note {{this global dangles}}
+ static int *static_field; // expected-note {{this static storage dangles}}
};
void save_global() {
@@ -22,14 +22,14 @@ void invoke_function_with_side_effects() {
// We can however catch the inlined one of course!
void inlined() {
int local;
- global = &local; // expected-warning {{address of stack memory escapes to a global}}
+ global = &local; // expected-warning {{address of stack memory escapes to global or static storage}}
global_backup = global;
global = nullptr;
}
void store_local_in_global() {
int local;
- global = &local; // expected-warning {{address of stack memory escapes to a global}}
+ global = &local; // expected-warning {{address of stack memory escapes to global or static storage}}
}
void store_then_clear() {
@@ -40,5 +40,5 @@ void store_then_clear() {
void dangling_static_field() {
int local;
- ObjWithStaticField::static_field = &local; // expected-warning {{address of stack memory escapes to a global}}
+ ObjWithStaticField::static_field = &local; // expected-warning {{address of stack memory escapes to global or static storage}}
}
\ No newline at end of file
diff --git a/clang/test/Sema/warn-lifetime-safety-noescape.cpp b/clang/test/Sema/warn-lifetime-safety-noescape.cpp
index 03f4c24a0956f..f233ec546faa5 100644
--- a/clang/test/Sema/warn-lifetime-safety-noescape.cpp
+++ b/clang/test/Sema/warn-lifetime-safety-noescape.cpp
@@ -119,7 +119,7 @@ void escape_through_global_var(const MyObj& in [[clang::noescape]]) { // expecte
global_view = in;
}
struct ObjWithStaticField {
- static int *static_field; // expected-note {{escapes to this global storage}}
+ static int *static_field; // expected-note {{escapes to this static storage}}
};
void escape_to_static_data_member(int *data [[clang::noescape]]) { // expected-warning {{parameter is marked [[clang::noescape]] but escapes}}
@@ -129,13 +129,16 @@ void escape_to_static_data_member(int *data [[clang::noescape]]) { // expected-w
void escape_through_static_local(int *data [[clang::noescape]]) { // expected-warning {{parameter is marked [[clang::noescape]] but escapes}}
- static int *static_local; // expected-note {{escapes to this global storage}}
+ static int *static_local; // expected-note {{escapes to this static storage}}
static_local = data;
}
thread_local int *thread_local_storage; // expected-note {{escapes to this global storage}}
void escape_through_thread_local(int *data [[clang::noescape]]) { // expected-warning {{parameter is marked [[clang::noescape]] but escapes}}
+ // FIXME: We might want to report whenever anything derived from a
+ // noescape parameter is assigned to a global, as this is likely
+ // undesired behavior in most cases.
thread_local_storage = data;
}
>From f89a641888141943b930c57ede0310de91fb2bf3 Mon Sep 17 00:00:00 2001
From: Abhinav Pradeep <abhinav.pradeep at oracle.com>
Date: Thu, 5 Mar 2026 21:57:18 +1000
Subject: [PATCH 6/6] Fixed comment
---
clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
index e4fbe07b3e9bd..21669bba7c84b 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
@@ -203,7 +203,7 @@ class FieldEscapeFact : public OriginEscapesFact {
const OriginManager &OM) const override;
};
-/// Represents that an origin escapes via assignment to global storage.
+/// Represents that an origin escapes via assignment to global or static storage.
/// Example: `global_storage = local_var;`
class GlobalEscapeFact : public OriginEscapesFact {
const VarDecl *Global;
More information about the cfe-commits
mailing list