https://github.com/AbhinavPradeep updated https://github.com/llvm/llvm-project/pull/186126
>From d269ace78b5eb433121a3524a0f772c0b0917103 Mon Sep 17 00:00:00 2001 From: Abhinav Pradeep <[email protected]> Date: Fri, 13 Mar 2026 00:21:23 +1000 Subject: [PATCH 1/3] 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 88b509e1b94df..8a5b24eae9b90 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" @@ -165,6 +167,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) { @@ -234,6 +237,40 @@ class GlobalEscapeFact : public OriginEscapesFact { const OriginManager &OM) 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 3d7fbcdacc830..158abb5868186 100644 --- a/clang/lib/Analysis/LifetimeSafety/Facts.cpp +++ b/clang/lib/Analysis/LifetimeSafety/Facts.cpp @@ -77,6 +77,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 { OS << "Use ("; >From 021cc16b0cdcf8c8252a14116e95aa05d2765370 Mon Sep 17 00:00:00 2001 From: Abhinav Pradeep <[email protected]> Date: Sat, 21 Mar 2026 22:52:08 +1000 Subject: [PATCH 2/3] 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 +++ .../Sema/warn-lifetime-safety-noescape.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 8a5b24eae9b90..5bed7cf3a70c0 100644 --- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h +++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h @@ -244,29 +244,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 2a71b5eb9a934..7209a237d4273 100644 --- a/clang/lib/Analysis/LifetimeSafety/Checker.cpp +++ b/clang/lib/Analysis/LifetimeSafety/Checker.cpp @@ -126,6 +126,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 633f9ae57930b..0893b6e8ab3f2 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" @@ -943,13 +945,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 cfbcacf04b1b0..87640dbdca925 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/warn-lifetime-safety-noescape.cpp b/clang/test/Sema/warn-lifetime-safety-noescape.cpp index 4bb57e6b9df95..7b944136ff1cb 100644 --- a/clang/test/Sema/warn-lifetime-safety-noescape.cpp +++ b/clang/test/Sema/warn-lifetime-safety-noescape.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 54d5a633f6358e3d1f21336b3f1a9b49819e5919 Mon Sep 17 00:00:00 2001 From: Abhinav Pradeep <[email protected]> Date: Mon, 8 Jun 2026 17:55:20 +1000 Subject: [PATCH 3/3] 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 +++++++ .../Sema/warn-lifetime-safety-noescape.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 398cce1395854..9a082f236180f 100644 --- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h +++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h @@ -118,6 +118,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 4e3585b7b8191..099c584162a4e 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -11051,6 +11051,7 @@ 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 note_lifetime_safety_lifetimebound_here: Note<"'lifetimebound' attribute appears here on the definition">; +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 7209a237d4273..dbad69548393f 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; @@ -120,16 +121,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 @@ -410,15 +402,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 6da4953dea56d..d4e219006118b 100644 --- a/clang/lib/Sema/SemaLifetimeSafety.h +++ b/clang/lib/Sema/SemaLifetimeSafety.h @@ -398,6 +398,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/warn-lifetime-safety-noescape.cpp b/clang/test/Sema/warn-lifetime-safety-noescape.cpp index 7b944136ff1cb..fcf58d4a54740 100644 --- a/clang/test/Sema/warn-lifetime-safety-noescape.cpp +++ b/clang/test/Sema/warn-lifetime-safety-noescape.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( _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
