[clang] [LifetimeSafety] Handle escape through function call (PR #186126)
Abhinav Pradeep via cfe-commits
cfe-commits at lists.llvm.org
Tue Jul 21 04:37:19 PDT 2026
https://github.com/AbhinavPradeep updated https://github.com/llvm/llvm-project/pull/186126
>From caad4ff4e30b40f84e1baf6fa847f0663000bb61 Mon Sep 17 00:00:00 2001
From: Abhinav Pradeep <abhinav.pradeep at oracle.com>
Date: Fri, 13 Mar 2026 00:21:23 +1000
Subject: [PATCH 1/7] Added new CallEscapeFact.
---
.../Analysis/Analyses/LifetimeSafety/Facts.h | 37 +++++++++++++++++++
clang/lib/Analysis/LifetimeSafety/Facts.cpp | 7 ++++
2 files changed, 44 insertions(+)
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
index 94db2a7f311ae..ae981ccf8a2d1 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
@@ -15,11 +15,13 @@
#define LLVM_CLANG_ANALYSIS_ANALYSES_LIFETIMESAFETY_FACTS_H
#include "clang/AST/Decl.h"
+#include "clang/AST/Expr.h"
#include "clang/Analysis/Analyses/LifetimeSafety/Loans.h"
#include "clang/Analysis/Analyses/LifetimeSafety/Origins.h"
#include "clang/Analysis/Analyses/LifetimeSafety/Utils.h"
#include "clang/Analysis/AnalysisDeclContext.h"
#include "clang/Analysis/CFG.h"
+#include "llvm/ADT/PointerUnion.h"
#include "llvm/ADT/STLFunctionalExtras.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/Support/Debug.h"
@@ -171,6 +173,7 @@ class OriginEscapesFact : public Fact {
Return, /// Escapes via return statement.
Field, /// Escapes via assignment to a field.
Global, /// Escapes via assignment to global storage.
+ Call, /// Escapes as argument to a function call.
} EscKind;
static bool classof(const Fact *F) {
@@ -240,6 +243,40 @@ class GlobalEscapeFact : public OriginEscapesFact {
const LoanPropagationAnalysis *LPA = nullptr) const override;
};
+/// Represents escape of an origin through a function call.
+/// Example:
+/// void f(int *i);
+/// void g(int *j[[clang::noescape]]) {f(j)};
+/// This fact enables us to catch that the noescape parameter j escapes through
+/// the call to function f
+class CallEscapeFact : public OriginEscapesFact {
+ // Currently the analysis handles the following call-like expressions:
+ // - VisitCXXOperatorCallExpr to handle CXXOperatorCallExpr, a sub-class of
+ // CallExpr.
+ // - VisitCXXMemberCallExpr to handle CXXMemberCallExpr, a sub-class of
+ // CallExpr.
+ // - VisitCXXConstructExpr and handleGSLPointerConstruction deal with
+ // CXXConstructExpr. Whilst call like, it is not a sub-class of CallExpr.
+ // Therefore, this type is taken to be the union of CallExpr * and
+ // CXXConstructExpr *:
+ using CallLikeExprPtr = llvm::PointerUnion<CallExpr *, CXXConstructExpr *>;
+ const CallLikeExprPtr Call;
+ const unsigned ArgumentIndex;
+
+public:
+ CallEscapeFact(OriginID OID, const CallLikeExprPtr Call, const unsigned Index)
+ : OriginEscapesFact(OID, EscapeKind::Call), Call(Call),
+ ArgumentIndex(Index) {}
+ static bool classof(const Fact *F) {
+ return F->getKind() == Kind::OriginEscapes &&
+ static_cast<const OriginEscapesFact *>(F)->getEscapeKind() ==
+ EscapeKind::Call;
+ }
+ const CallLikeExprPtr getCall() const { return Call; };
+ 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/lib/Analysis/LifetimeSafety/Facts.cpp b/clang/lib/Analysis/LifetimeSafety/Facts.cpp
index ec2d42e10206a..53bf38bb0b4dc 100644
--- a/clang/lib/Analysis/LifetimeSafety/Facts.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Facts.cpp
@@ -98,6 +98,13 @@ void GlobalEscapeFact::dump(llvm::raw_ostream &OS, const LoanManager &,
OS << ", via Global)\n";
}
+void CallEscapeFact::dump(llvm::raw_ostream &OS, const LoanManager &,
+ const OriginManager &OM) const {
+ OS << "CallEscapes (";
+ OM.dump(getEscapedOriginID(), OS);
+ OS << ", via Call)\n";
+}
+
void UseFact::dump(llvm::raw_ostream &OS, const LoanManager &,
const OriginManager &OM,
const LoanPropagationAnalysis *) const {
>From 7cd724acb1a9b1cdeb09d44163fead5c550d298e Mon Sep 17 00:00:00 2001
From: Abhinav Pradeep <abhinav.pradeep at oracle.com>
Date: Sat, 21 Mar 2026 22:52:08 +1000
Subject: [PATCH 2/7] Fixed up CallEscapeFact and ensured that it does not
participate in liveness analysis. Added no-escape checking which uses a
placeholder error message.
---
.../Analysis/Analyses/LifetimeSafety/Facts.h | 22 ++++------
clang/lib/Analysis/LifetimeSafety/Checker.cpp | 4 ++
.../LifetimeSafety/FactsGenerator.cpp | 40 +++++++++++++++++--
.../Analysis/LifetimeSafety/LiveOrigins.cpp | 6 +++
.../LifetimeSafety/noescape-violation.cpp | 9 +++--
5 files changed, 60 insertions(+), 21 deletions(-)
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
index ae981ccf8a2d1..fb6763b227edc 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
@@ -250,29 +250,23 @@ class GlobalEscapeFact : public OriginEscapesFact {
/// This fact enables us to catch that the noescape parameter j escapes through
/// the call to function f
class CallEscapeFact : public OriginEscapesFact {
- // Currently the analysis handles the following call-like expressions:
- // - VisitCXXOperatorCallExpr to handle CXXOperatorCallExpr, a sub-class of
- // CallExpr.
- // - VisitCXXMemberCallExpr to handle CXXMemberCallExpr, a sub-class of
- // CallExpr.
- // - VisitCXXConstructExpr and handleGSLPointerConstruction deal with
- // CXXConstructExpr. Whilst call like, it is not a sub-class of CallExpr.
- // Therefore, this type is taken to be the union of CallExpr * and
- // CXXConstructExpr *:
- using CallLikeExprPtr = llvm::PointerUnion<CallExpr *, CXXConstructExpr *>;
- const CallLikeExprPtr Call;
+ const Expr *Call;
+ const Expr *Argument;
const unsigned ArgumentIndex;
public:
- CallEscapeFact(OriginID OID, const CallLikeExprPtr Call, const unsigned Index)
+ CallEscapeFact(OriginID OID, const Expr *Call, const unsigned Index,
+ const Expr *Argument)
: OriginEscapesFact(OID, EscapeKind::Call), Call(Call),
- ArgumentIndex(Index) {}
+ Argument(Argument), ArgumentIndex(Index) {}
static bool classof(const Fact *F) {
return F->getKind() == Kind::OriginEscapes &&
static_cast<const OriginEscapesFact *>(F)->getEscapeKind() ==
EscapeKind::Call;
}
- const CallLikeExprPtr getCall() const { return Call; };
+ const Expr *getCall() const { return Call; };
+ unsigned getArgumentIndex() const { return ArgumentIndex; };
+ const Expr *getArgument() const { return Argument; };
void dump(llvm::raw_ostream &OS, const LoanManager &,
const OriginManager &OM) const override;
};
diff --git a/clang/lib/Analysis/LifetimeSafety/Checker.cpp b/clang/lib/Analysis/LifetimeSafety/Checker.cpp
index 53e5077131147..46b6a2b5c721e 100644
--- a/clang/lib/Analysis/LifetimeSafety/Checker.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Checker.cpp
@@ -131,6 +131,10 @@ class LifetimeChecker {
NoescapeWarningsMap.try_emplace(PVD, FieldEsc->getFieldDecl());
if (auto *GlobalEsc = dyn_cast<GlobalEscapeFact>(OEF))
NoescapeWarningsMap.try_emplace(PVD, GlobalEsc->getGlobal());
+ if (auto *CallEsc = dyn_cast<CallEscapeFact>(OEF))
+ // Currently this triggers the wrong reporting. Will fix with next
+ // commit!
+ NoescapeWarningsMap.try_emplace(PVD, CallEsc->getArgument());
return;
}
// Skip annotation suggestion for moved loans, as ownership transfer
diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index dd2bffe22f4f5..24cd5f1c094d1 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -14,6 +14,7 @@
#include "clang/AST/Expr.h"
#include "clang/AST/ExprCXX.h"
#include "clang/AST/OperationKinds.h"
+#include "clang/AST/Stmt.h"
#include "clang/Analysis/Analyses/LifetimeSafety/Facts.h"
#include "clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h"
#include "clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h"
@@ -21,6 +22,7 @@
#include "clang/Analysis/Analyses/PostOrderCFGView.h"
#include "clang/Analysis/CFG.h"
#include "clang/Basic/OperatorKinds.h"
+#include "clang/Basic/SourceManager.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/Support/Casting.h"
@@ -1077,13 +1079,45 @@ void FactsGenerator::handleFunctionCall(const Expr *Call,
ArrayRef<const Expr *> Args,
bool IsGslConstruction) {
OriginList *CallList = getOriginsList(*Call);
+ SourceManager &SM = AC.getASTContext().getSourceManager();
+ // To avoid over-reporting, we assume the following are noescape:
+ // - All parameters to functions declared in the system headers
+ // - The implicit `this` parameter for member functions
+ auto IsArgNoEscape = [FD, &SM](unsigned I) -> bool {
+ const ParmVarDecl *PVD = nullptr;
+ if (const auto *Method = dyn_cast<CXXMethodDecl>(FD);
+ Method && Method->isInstance()) {
+ // There is currently no way to declare 'this' is noescape for member
+ // functions We therefore return true as the user cannot do anything via
+ // annotation, so we make the conservative approximation
+ if (I == 0) {
+ return true;
+ }
+ if ((I - 1) < Method->getNumParams()) {
+ PVD = Method->getParamDecl(I - 1);
+ }
+ } else if (I < FD->getNumParams()) {
+ PVD = FD->getParamDecl(I);
+ }
+ if (PVD && !SM.isInSystemHeader(PVD->getLocation()))
+ return PVD->hasAttr<clang::NoEscapeAttr>();
+ return true;
+ };
+ // All arguments to a function are a use of the corresponding expressions.
+ for (unsigned I = 0; I < Args.size(); ++I) {
+ handleUse(Args[I]);
+ OriginList *ArgList = getOriginsList(*Args[I]);
+ if (!IsArgNoEscape(I)) {
+ for (OriginList *L = ArgList; L; L = L->peelOuterOrigin()) {
+ EscapesInCurrentBlock.push_back(FactMgr.createFact<CallEscapeFact>(
+ L->getOuterOriginID(), Call, I, Args[I]));
+ }
+ }
+ }
// Ignore functions returning values with no origin.
FD = getDeclWithMergedLifetimeBoundAttrs(FD);
if (!FD)
return;
- // All arguments to a function are a use of the corresponding expressions.
- for (const Expr *Arg : Args)
- handleUse(Arg);
handleInvalidatingCall(Call, FD, Args);
handleDestructiveCall(Call, FD, Args);
handleMovedArgsInCall(FD, Args);
diff --git a/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp b/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp
index 69b903c813555..b28c5f0f91f90 100644
--- a/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp
@@ -9,6 +9,7 @@
#include "clang/Analysis/Analyses/LifetimeSafety/LiveOrigins.h"
#include "Dataflow.h"
#include "clang/Analysis/Analyses/LifetimeSafety/Facts.h"
+#include "llvm/Support/Casting.h"
#include "llvm/Support/ErrorHandling.h"
namespace clang::lifetimes::internal {
@@ -64,6 +65,8 @@ static SourceLocation GetFactLoc(CausingFactType F) {
return FieldEsc->getFieldDecl()->getLocation();
if (auto *GlobalEsc = dyn_cast<GlobalEscapeFact>(OEF))
return GlobalEsc->getGlobal()->getLocation();
+ if (auto *CallEsc = dyn_cast<CallEscapeFact>(OEF))
+ return CallEsc->getArgument()->getExprLoc();
}
llvm_unreachable("unhandled causing fact in PointerUnion");
}
@@ -148,6 +151,9 @@ class AnalysisImpl
/// An escaping origin (e.g., via return) makes the origin live with definite
/// confidence, as it dominates this program point.
Lattice transfer(Lattice In, const OriginEscapesFact &OEF) {
+ // CallEscapeFact should not affect liveness
+ if (isa<CallEscapeFact>(&OEF))
+ return In;
OriginID OID = OEF.getEscapedOriginID();
return Lattice(Factory.add(In.LiveOrigins, OID,
LivenessInfo(&OEF, LivenessKind::Must)));
diff --git a/clang/test/Sema/LifetimeSafety/noescape-violation.cpp b/clang/test/Sema/LifetimeSafety/noescape-violation.cpp
index 048c500239b4f..43022e7d60195 100644
--- a/clang/test/Sema/LifetimeSafety/noescape-violation.cpp
+++ b/clang/test/Sema/LifetimeSafety/noescape-violation.cpp
@@ -8,9 +8,10 @@ struct [[gsl::Owner]] MyObj {
};
struct [[gsl::Pointer()]] View {
- View(const MyObj&); // Borrows from MyObj
+ View(const MyObj& obj [[clang::noescape]]); // Borrows from MyObj
View();
void use() const;
+ void let_parameter_escape(const MyObj& obj) const;
};
View return_noescape_directly(const MyObj& in [[clang::noescape]]) { // expected-warning {{parameter is marked [[clang::noescape]] but escapes}}
@@ -150,9 +151,9 @@ struct ObjConsumer {
View member_view; // expected-note {{escapes to this field}}
};
-// FIXME: Escaping through another param is not detected.
-void escape_through_param(const MyObj& in, std::vector<View> &v) {
- v.push_back(in);
+void escape_through_param(const MyObj& in [[clang::noescape]], std::vector<View> &v) { // expected-warning {{parameter is marked [[clang::noescape]] but escapes}}
+ // Has wrong reporting by virtue of how the reportNoescapeViolations is written. Will fix in the next commit!
+ v.push_back(in); // expected-note {{returned here}}
}
View reassign_to_second(
>From 96dd5242ad5d1292ac499114dcdbbc60eac18d19 Mon Sep 17 00:00:00 2001
From: Abhinav Pradeep <abhinav.pradeep at oracle.com>
Date: Mon, 8 Jun 2026 17:55:20 +1000
Subject: [PATCH 3/7] Rebased and fixed up reporting.
---
.../Analyses/LifetimeSafety/LifetimeSafety.h | 5 +++
.../clang/Basic/DiagnosticSemaKinds.td | 1 +
clang/lib/Analysis/LifetimeSafety/Checker.cpp | 32 ++++++++-----------
clang/lib/Sema/SemaLifetimeSafety.h | 11 +++++++
.../LifetimeSafety/noescape-violation.cpp | 7 ++--
5 files changed, 33 insertions(+), 23 deletions(-)
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h
index a51ef2f7cc0ba..1dad873cf4faf 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h
@@ -124,6 +124,11 @@ class LifetimeSafetySemaHelper {
// assignment to a global variable
virtual void reportNoescapeViolation(const ParmVarDecl *ParmWithNoescape,
const VarDecl *EscapeGlobal) {}
+ // Reports misuse of [[clang::noescape]] when parameter escapes through
+ // a function call.
+ virtual void
+ reportNoescapeViolationThroughCall(const ParmVarDecl *ParmWithNoescape,
+ const Expr *EscapeCall) {}
// Reports misuse of [[clang::lifetimebound]] when parameter doesn't escape
// through return.
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index 89e2f956971b3..9c731c1ef3b78 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -11102,6 +11102,7 @@ def note_lifetime_safety_escapes_to_global_here: Note<"escapes to this global st
def note_lifetime_safety_escapes_to_static_storage_here: Note<"escapes to this static storage">;
def note_lifetime_safety_lifetimebound_here: Note<"'lifetimebound' attribute appears here on the definition">;
def note_lifetime_safety_aliases_storage : Note<"%0 aliases the storage of %1">;
+def note_lifetime_safety_escapes_through_call_here: Note<"escapes through this call">;
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 46b6a2b5c721e..af71e7c531007 100644
--- a/clang/lib/Analysis/LifetimeSafety/Checker.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Checker.cpp
@@ -59,7 +59,8 @@ class LifetimeChecker {
private:
llvm::DenseMap<LoanID, PendingWarning> FinalWarningsMap;
llvm::DenseMap<AnnotationTarget, EscapingTarget> AnnotationWarningsMap;
- llvm::DenseMap<const ParmVarDecl *, EscapingTarget> NoescapeWarningsMap;
+ llvm::DenseMap<const ParmVarDecl *, const OriginEscapesFact *>
+ NoescapeWarningsMap;
llvm::DenseSet<const Decl *> VerifiedLiftimeboundEscapes;
const LoanPropagationAnalysis &LoanPropagation;
const MovedLoansAnalysis &MovedLoans;
@@ -125,16 +126,7 @@ class LifetimeChecker {
auto CheckParam = [&](const ParmVarDecl *PVD, bool IsMoved) {
// NoEscape param should not escape.
if (PVD->hasAttr<NoEscapeAttr>()) {
- if (auto *ReturnEsc = dyn_cast<ReturnEscapeFact>(OEF))
- 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());
- if (auto *CallEsc = dyn_cast<CallEscapeFact>(OEF))
- // Currently this triggers the wrong reporting. Will fix with next
- // commit!
- NoescapeWarningsMap.try_emplace(PVD, CallEsc->getArgument());
+ NoescapeWarningsMap.try_emplace(PVD, OEF);
return;
}
// Skip annotation suggestion for moved loans, as ownership transfer
@@ -425,15 +417,17 @@ class LifetimeChecker {
}
void reportNoescapeViolations() {
- for (auto [PVD, EscapeTarget] : NoescapeWarningsMap) {
- if (const auto *E = EscapeTarget.dyn_cast<const Expr *>())
- 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);
+ for (auto [PVD, OEF] : NoescapeWarningsMap) {
+ if (const auto *ReturnEsc = dyn_cast<ReturnEscapeFact>(OEF))
+ SemaHelper->reportNoescapeViolation(PVD, ReturnEsc->getReturnExpr());
+ else if (const auto *FieldEsc = dyn_cast<FieldEscapeFact>(OEF))
+ SemaHelper->reportNoescapeViolation(PVD, FieldEsc->getFieldDecl());
+ else if (const auto *GlobalEsc = dyn_cast<GlobalEscapeFact>(OEF))
+ SemaHelper->reportNoescapeViolation(PVD, GlobalEsc->getGlobal());
+ else if (const auto *CallEsc = dyn_cast<CallEscapeFact>(OEF))
+ SemaHelper->reportNoescapeViolationThroughCall(PVD, CallEsc->getCall());
else
- llvm_unreachable("Unhandled EscapingTarget type");
+ llvm_unreachable("Unhandled escape fact kind");
}
}
diff --git a/clang/lib/Sema/SemaLifetimeSafety.h b/clang/lib/Sema/SemaLifetimeSafety.h
index 1d9f94be7e22d..e1c0e67dd1ccb 100644
--- a/clang/lib/Sema/SemaLifetimeSafety.h
+++ b/clang/lib/Sema/SemaLifetimeSafety.h
@@ -454,6 +454,17 @@ class LifetimeSafetySemaHelperImpl : public LifetimeSafetySemaHelper {
<< EscapeGlobal->getEndLoc();
}
+ void reportNoescapeViolationThroughCall(const ParmVarDecl *ParmWithNoescape,
+ const Expr *EscapeCall) override {
+ S.Diag(ParmWithNoescape->getBeginLoc(),
+ diag::warn_lifetime_safety_noescape_escapes)
+ << ParmWithNoescape->getSourceRange();
+
+ S.Diag(EscapeCall->getBeginLoc(),
+ diag::note_lifetime_safety_escapes_through_call_here)
+ << EscapeCall->getSourceRange();
+ }
+
void addLifetimeBoundToImplicitThis(const CXXMethodDecl *MD) override {
S.addLifetimeBoundToImplicitThis(const_cast<CXXMethodDecl *>(MD));
}
diff --git a/clang/test/Sema/LifetimeSafety/noescape-violation.cpp b/clang/test/Sema/LifetimeSafety/noescape-violation.cpp
index 43022e7d60195..7ae0dcbcfcdb3 100644
--- a/clang/test/Sema/LifetimeSafety/noescape-violation.cpp
+++ b/clang/test/Sema/LifetimeSafety/noescape-violation.cpp
@@ -105,13 +105,13 @@ View identity_lifetimebound(View v [[clang::lifetimebound]]) { return v; }
View escape_through_lifetimebound_call(
const MyObj& in [[clang::noescape]]) { // expected-warning {{parameter is marked [[clang::noescape]] but escapes}}
- return identity_lifetimebound(in); // expected-note {{returned here}}
+ return identity_lifetimebound(in); // expected-note {{escapes through this call}}
}
View no_annotation_identity(View v) { return v; }
View escape_through_unannotated_call(const MyObj& in [[clang::noescape]]) { // expected-warning {{parameter is marked [[clang::noescape]] but escapes}}
- return no_annotation_identity(in); // expected-note {{returned here}}
+ return no_annotation_identity(in); // expected-note {{escapes through this call}}
}
View global_view; // expected-note {{escapes to this global storage}}
@@ -152,8 +152,7 @@ struct ObjConsumer {
};
void escape_through_param(const MyObj& in [[clang::noescape]], std::vector<View> &v) { // expected-warning {{parameter is marked [[clang::noescape]] but escapes}}
- // Has wrong reporting by virtue of how the reportNoescapeViolations is written. Will fix in the next commit!
- v.push_back(in); // expected-note {{returned here}}
+ v.push_back(in); // expected-note {{escapes through this call}}
}
View reassign_to_second(
>From 4e70b2f7a5ac99da7809e6f6c9aa82902042a8ce Mon Sep 17 00:00:00 2001
From: Abhinav Pradeep <abhinav.pradeep at oracle.com>
Date: Mon, 20 Jul 2026 15:39:56 +1000
Subject: [PATCH 4/7] Added guard against indirect function calls
---
clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index 24cd5f1c094d1..d52da19de18bf 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -1079,6 +1079,9 @@ void FactsGenerator::handleFunctionCall(const Expr *Call,
ArrayRef<const Expr *> Args,
bool IsGslConstruction) {
OriginList *CallList = getOriginsList(*Call);
+ FD = getDeclWithMergedLifetimeBoundAttrs(FD);
+ if (!FD)
+ return;
SourceManager &SM = AC.getASTContext().getSourceManager();
// To avoid over-reporting, we assume the following are noescape:
// - All parameters to functions declared in the system headers
@@ -1114,10 +1117,6 @@ void FactsGenerator::handleFunctionCall(const Expr *Call,
}
}
}
- // Ignore functions returning values with no origin.
- FD = getDeclWithMergedLifetimeBoundAttrs(FD);
- if (!FD)
- return;
handleInvalidatingCall(Call, FD, Args);
handleDestructiveCall(Call, FD, Args);
handleMovedArgsInCall(FD, Args);
>From 8fe828b97e072b2ec7324fe235957c434f294328 Mon Sep 17 00:00:00 2001
From: Abhinav Pradeep <abhinav.pradeep at oracle.com>
Date: Tue, 21 Jul 2026 14:53:58 +1000
Subject: [PATCH 5/7] Added strict noescape warning group and moved function
call escape to it.
---
clang/include/clang/Basic/DiagnosticGroups.td | 11 +++++++
.../clang/Basic/DiagnosticSemaKinds.td | 5 ++++
.../LifetimeSafety/FactsGenerator.cpp | 9 +++++-
clang/lib/Sema/SemaLifetimeSafety.h | 2 +-
.../LifetimeSafety/noescape-violation.cpp | 17 -----------
.../warn-lifetime-safety-noescape-strict.cpp | 30 +++++++++++++++++++
6 files changed, 55 insertions(+), 19 deletions(-)
create mode 100644 clang/test/Sema/warn-lifetime-safety-noescape-strict.cpp
diff --git a/clang/include/clang/Basic/DiagnosticGroups.td b/clang/include/clang/Basic/DiagnosticGroups.td
index 79583534b9bbd..db32d607a6479 100644
--- a/clang/include/clang/Basic/DiagnosticGroups.td
+++ b/clang/include/clang/Basic/DiagnosticGroups.td
@@ -707,6 +707,17 @@ def LifetimeSafetyAnnotationPlacement
: DiagGroup<"lifetime-safety-annotation-placement",
[LifetimeSafetyInapplicableLifetimebound,
LifetimeSafetyMisplacedLifetimebound]> {
+def LifetimeSafetyNoescapeStrict
+ : DiagGroup<"lifetime-safety-noescape-strict", [LifetimeSafetyNoescape]> {
+ code Documentation = [{
+Enables stricter detection of [[clang::noescape]] annotation misuse (for example, through function calls).
+ }];
+}
+
+def LifetimeSafetyValidations : DiagGroup<"lifetime-safety-validations",
+ [LifetimeSafetyNoescape,
+ LifetimeSafetyLifetimeboundViolation,
+ LifetimeSafetyMisplacedLifetimebound]> {
code Documentation = [{
Validates that lifetime annotations are placed on appropriate types and in appropriate locations.
}];
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index 9c731c1ef3b78..e68a9077c23c6 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -11147,6 +11147,11 @@ def warn_lifetime_safety_noescape_escapes
InGroup<LifetimeSafetyNoescape>,
DefaultIgnore;
+def warn_lifetime_safety_noescape_escapes_through_call
+ : Warning<"parameter is marked [[clang::noescape]] but escapes">,
+ InGroup<LifetimeSafetyNoescapeStrict>,
+ DefaultIgnore;
+
// For non-floating point, expressions of the form x == x or x != x
// should result in a warning, since these always evaluate to a constant.
// Array comparisons have similar warnings
diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index d52da19de18bf..d6757f18e651a 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -21,6 +21,7 @@
#include "clang/Analysis/Analyses/LifetimeSafety/Origins.h"
#include "clang/Analysis/Analyses/PostOrderCFGView.h"
#include "clang/Analysis/CFG.h"
+#include "clang/Basic/DiagnosticSema.h"
#include "clang/Basic/OperatorKinds.h"
#include "clang/Basic/SourceManager.h"
#include "llvm/ADT/ArrayRef.h"
@@ -1082,6 +1083,12 @@ void FactsGenerator::handleFunctionCall(const Expr *Call,
FD = getDeclWithMergedLifetimeBoundAttrs(FD);
if (!FD)
return;
+ // Check to see if we need to report for escape through function call.
+ // Used to gate generation of CallEscapeFact.
+ const bool EnableNoescapeCallEscapes =
+ !AC.getASTContext().getDiagnostics().isIgnored(
+ diag::warn_lifetime_safety_noescape_escapes_through_call,
+ Call->getBeginLoc());
SourceManager &SM = AC.getASTContext().getSourceManager();
// To avoid over-reporting, we assume the following are noescape:
// - All parameters to functions declared in the system headers
@@ -1110,7 +1117,7 @@ void FactsGenerator::handleFunctionCall(const Expr *Call,
for (unsigned I = 0; I < Args.size(); ++I) {
handleUse(Args[I]);
OriginList *ArgList = getOriginsList(*Args[I]);
- if (!IsArgNoEscape(I)) {
+ if (EnableNoescapeCallEscapes && !IsArgNoEscape(I)) {
for (OriginList *L = ArgList; L; L = L->peelOuterOrigin()) {
EscapesInCurrentBlock.push_back(FactMgr.createFact<CallEscapeFact>(
L->getOuterOriginID(), Call, I, Args[I]));
diff --git a/clang/lib/Sema/SemaLifetimeSafety.h b/clang/lib/Sema/SemaLifetimeSafety.h
index e1c0e67dd1ccb..69b7fadfa2c4c 100644
--- a/clang/lib/Sema/SemaLifetimeSafety.h
+++ b/clang/lib/Sema/SemaLifetimeSafety.h
@@ -457,7 +457,7 @@ class LifetimeSafetySemaHelperImpl : public LifetimeSafetySemaHelper {
void reportNoescapeViolationThroughCall(const ParmVarDecl *ParmWithNoescape,
const Expr *EscapeCall) override {
S.Diag(ParmWithNoescape->getBeginLoc(),
- diag::warn_lifetime_safety_noescape_escapes)
+ diag::warn_lifetime_safety_noescape_escapes_through_call)
<< ParmWithNoescape->getSourceRange();
S.Diag(EscapeCall->getBeginLoc(),
diff --git a/clang/test/Sema/LifetimeSafety/noescape-violation.cpp b/clang/test/Sema/LifetimeSafety/noescape-violation.cpp
index 7ae0dcbcfcdb3..c69db73205b12 100644
--- a/clang/test/Sema/LifetimeSafety/noescape-violation.cpp
+++ b/clang/test/Sema/LifetimeSafety/noescape-violation.cpp
@@ -101,19 +101,6 @@ View both_noescape_and_lifetimebound(
return in; // expected-note {{returned here}}
}
-View identity_lifetimebound(View v [[clang::lifetimebound]]) { return v; }
-
-View escape_through_lifetimebound_call(
- const MyObj& in [[clang::noescape]]) { // expected-warning {{parameter is marked [[clang::noescape]] but escapes}}
- return identity_lifetimebound(in); // expected-note {{escapes through this call}}
-}
-
-View no_annotation_identity(View v) { return v; }
-
-View escape_through_unannotated_call(const MyObj& in [[clang::noescape]]) { // expected-warning {{parameter is marked [[clang::noescape]] but escapes}}
- return no_annotation_identity(in); // expected-note {{escapes through this call}}
-}
-
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}}
@@ -151,10 +138,6 @@ struct ObjConsumer {
View member_view; // expected-note {{escapes to this field}}
};
-void escape_through_param(const MyObj& in [[clang::noescape]], std::vector<View> &v) { // expected-warning {{parameter is marked [[clang::noescape]] but escapes}}
- v.push_back(in); // expected-note {{escapes through this call}}
-}
-
View reassign_to_second(
const MyObj& a [[clang::noescape]],
const MyObj& b [[clang::noescape]]) { // expected-warning {{parameter is marked [[clang::noescape]] but escapes}}
diff --git a/clang/test/Sema/warn-lifetime-safety-noescape-strict.cpp b/clang/test/Sema/warn-lifetime-safety-noescape-strict.cpp
new file mode 100644
index 0000000000000..1aeaddd262c7e
--- /dev/null
+++ b/clang/test/Sema/warn-lifetime-safety-noescape-strict.cpp
@@ -0,0 +1,30 @@
+// RUN: %clang_cc1 -fsyntax-only -flifetime-safety-inference -Wlifetime-safety-noescape-strict -verify %s
+
+#include "Inputs/lifetime-analysis.h"
+
+struct [[gsl::Owner]] MyObj {
+ int id;
+ ~MyObj() {}
+};
+
+struct [[gsl::Pointer()]] View {
+ View(const MyObj& obj [[clang::noescape]]);
+};
+
+View identity_lifetimebound(View v [[clang::lifetimebound]]) { return v; }
+
+View escape_through_lifetimebound_call(
+ const MyObj& in [[clang::noescape]]) { // expected-warning {{parameter is marked [[clang::noescape]] but escapes}}
+ return identity_lifetimebound(in); // expected-note {{escapes through this call}}
+}
+
+View no_annotation_identity(View v) { return v; }
+
+View escape_through_unannotated_call(const MyObj& in [[clang::noescape]]) { // expected-warning {{parameter is marked [[clang::noescape]] but escapes}}
+ return no_annotation_identity(in); // expected-note {{escapes through this call}}
+}
+
+void escape_through_param(const MyObj& in [[clang::noescape]], // expected-warning {{parameter is marked [[clang::noescape]] but escapes}}
+ std::vector<View> &v) {
+ v.push_back(in); // expected-note {{escapes through this call}}
+}
>From 5ce3454b62367f29de062e6c98942f15700d229e Mon Sep 17 00:00:00 2001
From: Abhinav Pradeep <abhinav.pradeep at oracle.com>
Date: Tue, 21 Jul 2026 15:40:06 +1000
Subject: [PATCH 6/7] Add to documentation
---
clang/docs/LifetimeSafety.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/clang/docs/LifetimeSafety.md b/clang/docs/LifetimeSafety.md
index 0159db898bb73..3623915d56a2f 100644
--- a/clang/docs/LifetimeSafety.md
+++ b/clang/docs/LifetimeSafety.md
@@ -518,6 +518,8 @@ enables only the high-confidence subset of these checks.
- `-Wlifetime-safety-noescape`: Warns when a parameter marked with `[[clang::noescape]]` escapes the function.
- `-Wlifetime-safety-lifetimebound-violation`: Warns when the analysis cannot verify that the return value can be lifetime bound to a parameter marked with `[[clang::lifetimebound]]`.
+- `-Wlifetime-safety-noescape-strict`: More strict version of `-Wlifetime-safety-noescape` that also warns when an marked parameter escapes through an unannotated function call.
+
## Limitations
### Move Semantics
>From 791828dcbd9a22bad151930a71e49674667b5155 Mon Sep 17 00:00:00 2001
From: Abhinav Pradeep <abhinav.pradeep at oracle.com>
Date: Tue, 21 Jul 2026 21:36:18 +1000
Subject: [PATCH 7/7] Fix rebase issues
---
.../clang/Analysis/Analyses/LifetimeSafety/Facts.h | 4 ++--
clang/include/clang/Basic/DiagnosticGroups.td | 12 ++++--------
clang/lib/Analysis/LifetimeSafety/Facts.cpp | 3 ++-
.../noescape-strict.cpp} | 0
4 files changed, 8 insertions(+), 11 deletions(-)
rename clang/test/Sema/{warn-lifetime-safety-noescape-strict.cpp => LifetimeSafety/noescape-strict.cpp} (100%)
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
index fb6763b227edc..1788958655f7b 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
@@ -267,8 +267,8 @@ class CallEscapeFact : public OriginEscapesFact {
const Expr *getCall() const { return Call; };
unsigned getArgumentIndex() const { return ArgumentIndex; };
const Expr *getArgument() const { return Argument; };
- void dump(llvm::raw_ostream &OS, const LoanManager &,
- const OriginManager &OM) const override;
+ void dump(llvm::raw_ostream &OS, const LoanManager &, const OriginManager &OM,
+ const LoanPropagationAnalysis *LPA = nullptr) const override;
};
class UseFact : public Fact {
diff --git a/clang/include/clang/Basic/DiagnosticGroups.td b/clang/include/clang/Basic/DiagnosticGroups.td
index db32d607a6479..e8a2e2174cf1f 100644
--- a/clang/include/clang/Basic/DiagnosticGroups.td
+++ b/clang/include/clang/Basic/DiagnosticGroups.td
@@ -703,10 +703,6 @@ Detects misuse of [[clang::noescape]] annotation where the parameter escapes (fo
}];
}
-def LifetimeSafetyAnnotationPlacement
- : DiagGroup<"lifetime-safety-annotation-placement",
- [LifetimeSafetyInapplicableLifetimebound,
- LifetimeSafetyMisplacedLifetimebound]> {
def LifetimeSafetyNoescapeStrict
: DiagGroup<"lifetime-safety-noescape-strict", [LifetimeSafetyNoescape]> {
code Documentation = [{
@@ -714,10 +710,10 @@ Enables stricter detection of [[clang::noescape]] annotation misuse (for example
}];
}
-def LifetimeSafetyValidations : DiagGroup<"lifetime-safety-validations",
- [LifetimeSafetyNoescape,
- LifetimeSafetyLifetimeboundViolation,
- LifetimeSafetyMisplacedLifetimebound]> {
+def LifetimeSafetyAnnotationPlacement
+ : DiagGroup<"lifetime-safety-annotation-placement",
+ [LifetimeSafetyInapplicableLifetimebound,
+ LifetimeSafetyMisplacedLifetimebound]> {
code Documentation = [{
Validates that lifetime annotations are placed on appropriate types and in appropriate locations.
}];
diff --git a/clang/lib/Analysis/LifetimeSafety/Facts.cpp b/clang/lib/Analysis/LifetimeSafety/Facts.cpp
index 53bf38bb0b4dc..bbd6baa89d735 100644
--- a/clang/lib/Analysis/LifetimeSafety/Facts.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Facts.cpp
@@ -99,7 +99,8 @@ void GlobalEscapeFact::dump(llvm::raw_ostream &OS, const LoanManager &,
}
void CallEscapeFact::dump(llvm::raw_ostream &OS, const LoanManager &,
- const OriginManager &OM) const {
+ const OriginManager &OM,
+ const LoanPropagationAnalysis *) const {
OS << "CallEscapes (";
OM.dump(getEscapedOriginID(), OS);
OS << ", via Call)\n";
diff --git a/clang/test/Sema/warn-lifetime-safety-noescape-strict.cpp b/clang/test/Sema/LifetimeSafety/noescape-strict.cpp
similarity index 100%
rename from clang/test/Sema/warn-lifetime-safety-noescape-strict.cpp
rename to clang/test/Sema/LifetimeSafety/noescape-strict.cpp
More information about the cfe-commits
mailing list