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