https://github.com/usx95 updated https://github.com/llvm/llvm-project/pull/177363
>From 9cd030d7c943037eb2300f44540507290a66b43f Mon Sep 17 00:00:00 2001 From: Utkarsh Saxena <[email protected]> Date: Thu, 22 Jan 2026 15:48:44 +0000 Subject: [PATCH] [LifetimeSafety] Detect dangling fields --- clang/docs/ReleaseNotes.rst | 11 +- .../Analysis/Analyses/LifetimeSafety/Facts.h | 52 +++++- .../Analyses/LifetimeSafety/FactsGenerator.h | 4 +- .../Analyses/LifetimeSafety/LifetimeSafety.h | 9 +- clang/include/clang/Analysis/CFG.h | 5 + clang/include/clang/Basic/DiagnosticGroups.td | 11 +- .../clang/Basic/DiagnosticSemaKinds.td | 7 + clang/lib/Analysis/CFG.cpp | 20 +- .../lib/Analysis/FlowSensitive/AdornedCFG.cpp | 1 + clang/lib/Analysis/LifetimeSafety/Checker.cpp | 70 ++++--- clang/lib/Analysis/LifetimeSafety/Facts.cpp | 13 +- .../LifetimeSafety/FactsGenerator.cpp | 67 +++++-- .../Analysis/LifetimeSafety/LiveOrigins.cpp | 9 +- clang/lib/Analysis/LifetimeSafety/Origins.cpp | 25 ++- clang/lib/Sema/AnalysisBasedWarnings.cpp | 29 ++- clang/test/Analysis/lifetime-cfg-output.cpp | 28 --- clang/test/Analysis/scopes-cfg-output.cpp | 2 - .../Sema/warn-lifetime-analysis-nocfg.cpp | 26 ++- .../warn-lifetime-safety-dangling-field.cpp | 175 ++++++++++++++++++ .../Sema/warn-lifetime-safety-dataflow.cpp | 2 +- .../Sema/warn-lifetime-safety-noescape.cpp | 5 +- 21 files changed, 459 insertions(+), 112 deletions(-) create mode 100644 clang/test/Sema/warn-lifetime-safety-dangling-field.cpp diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst index f0d3d81f14e43..99ed9e2e64b53 100644 --- a/clang/docs/ReleaseNotes.rst +++ b/clang/docs/ReleaseNotes.rst @@ -140,7 +140,7 @@ Improvements to Clang's diagnostics a CFG-based intra-procedural analysis that detects use-after-free and related temporal safety bugs. See the `RFC <https://discourse.llvm.org/t/rfc-intra-procedural-lifetime-analysis-in-clang/86291>`_ - for more details. By design, this warning is enabled in ``-Wall``. To disable + for more details. By design, this warning is enabled in ``-Weverything``. To disable the analysis, use ``-Wno-lifetime-safety`` or ``-fno-lifetime-safety``. - Added ``-Wlifetime-safety-suggestions`` to enable lifetime annotation suggestions. @@ -181,6 +181,15 @@ Improvements to Clang's diagnostics note: returned here int* p(int *in [[clang::noescape]]) { return in; } ^~ +- Added ``-Wlifetime-safety-dangling-field`` to detect dangling field references + when stack memory escapes to class fields. This is part of ``-Wlifetime-safety`` + and detects cases where local variables or parameters are stored in fields but + outlive their scope. For example: + .. code-block:: c++ + struct DanglingView { + std::string_view view; + DanglingView(std::string s) : view(s) {} // warning: address of stack memory escapes to a field + }; Improvements to Clang's time-trace ---------------------------------- diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h index 1bb34e6986857..d948965af34d5 100644 --- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h +++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h @@ -136,19 +136,63 @@ class OriginFlowFact : public Fact { const OriginManager &OM) const override; }; +/// Represents that an origin escapes the current scope through various means. +/// This is the base class for different escape scenarios. class OriginEscapesFact : public Fact { OriginID OID; - const Expr *EscapeExpr; public: + /// The way an origin can escape the current scope. + 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). + } EscKind; + static bool classof(const Fact *F) { return F->getKind() == Kind::OriginEscapes; } - OriginEscapesFact(OriginID OID, const Expr *EscapeExpr) - : Fact(Kind::OriginEscapes), OID(OID), EscapeExpr(EscapeExpr) {} + OriginEscapesFact(OriginID OID, EscapeKind EscKind) + : Fact(Kind::OriginEscapes), OID(OID), EscKind(EscKind) {} OriginID getEscapedOriginID() const { return OID; } - const Expr *getEscapeExpr() const { return EscapeExpr; }; + EscapeKind getEscapeKind() const { return EscKind; } +}; + +/// Represents that an origin escapes via a return statement. +class ReturnEscapeFact : public OriginEscapesFact { + const Expr *ReturnExpr; + +public: + ReturnEscapeFact(OriginID OID, const Expr *ReturnExpr) + : OriginEscapesFact(OID, EscapeKind::Return), ReturnExpr(ReturnExpr) {} + + static bool classof(const Fact *F) { + return F->getKind() == Kind::OriginEscapes && + static_cast<const OriginEscapesFact *>(F)->getEscapeKind() == + EscapeKind::Return; + } + const Expr *getReturnExpr() const { return ReturnExpr; }; + void dump(llvm::raw_ostream &OS, const LoanManager &, + const OriginManager &OM) const override; +}; + +/// Represents that an origin escapes via assignment to a field. +/// Example: `this->view = local_var;` where local_var outlives the assignment +/// but not the object containing the field. +class FieldEscapeFact : public OriginEscapesFact { + const FieldDecl *FDecl; + +public: + FieldEscapeFact(OriginID OID, const FieldDecl *FDecl) + : OriginEscapesFact(OID, EscapeKind::Field), FDecl(FDecl) {} + + static bool classof(const Fact *F) { + return F->getKind() == Kind::OriginEscapes && + static_cast<const OriginEscapesFact *>(F)->getEscapeKind() == + EscapeKind::Field; + } + const FieldDecl *getFieldDecl() const { return FDecl; }; void dump(llvm::raw_ostream &OS, const LoanManager &, const OriginManager &OM) const override; }; diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h index a47505ee9f159..e4487b0d1dbc7 100644 --- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h +++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h @@ -58,10 +58,12 @@ class FactsGenerator : public ConstStmtVisitor<FactsGenerator> { void handleAssignment(const Expr *LHSExpr, const Expr *RHSExpr); + void handleCXXCtorInitializer(const CXXCtorInitializer *CII); void handleLifetimeEnds(const CFGLifetimeEnds &LifetimeEnds); - void handleTemporaryDtor(const CFGTemporaryDtor &TemporaryDtor); + void handleExitBlock(); + void handleGSLPointerConstruction(const CXXConstructExpr *CCE); /// Checks if a call-like expression creates a borrow by passing a value to a diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h index 2ab60d918c8d1..9f22db20e79b1 100644 --- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h +++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h @@ -62,10 +62,14 @@ class LifetimeSafetySemaHelper { Confidence Confidence) {} virtual void reportUseAfterReturn(const Expr *IssueExpr, - const Expr *EscapeExpr, + const Expr *ReturnExpr, SourceLocation ExpiryLoc, Confidence Confidence) {} + virtual void reportDanglingField(const Expr *IssueExpr, + const FieldDecl *Field, + SourceLocation ExpiryLoc) {} + // Suggests lifetime bound annotations for function paramters. virtual void suggestLifetimeboundToParmVar(SuggestionScope Scope, const ParmVarDecl *ParmToAnnotate, @@ -74,6 +78,9 @@ class LifetimeSafetySemaHelper { // Reports misuse of [[clang::noescape]] when parameter escapes through return virtual void reportNoescapeViolation(const ParmVarDecl *ParmWithNoescape, const Expr *EscapeExpr) {} + // Reports misuse of [[clang::noescape]] when parameter escapes through field + virtual void reportNoescapeViolation(const ParmVarDecl *ParmWithNoescape, + const FieldDecl *EscapeField) {} // Suggests lifetime bound annotations for implicit this. virtual void suggestLifetimeboundToImplicitThis(SuggestionScope Scope, diff --git a/clang/include/clang/Analysis/CFG.h b/clang/include/clang/Analysis/CFG.h index a4bafd4927df0..e675935d9230a 100644 --- a/clang/include/clang/Analysis/CFG.h +++ b/clang/include/clang/Analysis/CFG.h @@ -1236,6 +1236,11 @@ class CFG { bool AddInitializers = false; bool AddImplicitDtors = false; bool AddLifetime = false; + // Add lifetime markers for function parameters. In principle, function + // parameters are constructed and destructed in the caller context but + // analyses could still choose to include these in the callee's CFG to + // represent the lifetime ends of parameters on function exit. + bool AddParameterLifetimes = false; bool AddLoopExit = false; bool AddTemporaryDtors = false; bool AddScopes = false; diff --git a/clang/include/clang/Basic/DiagnosticGroups.td b/clang/include/clang/Basic/DiagnosticGroups.td index 488f3a94c4fb6..f7ec24922a50b 100644 --- a/clang/include/clang/Basic/DiagnosticGroups.td +++ b/clang/include/clang/Basic/DiagnosticGroups.td @@ -533,14 +533,21 @@ def Dangling : DiagGroup<"dangling", [DanglingAssignment, DanglingGsl, ReturnStackAddress]>; -def LifetimeSafetyPermissive : DiagGroup<"lifetime-safety-permissive">; +def LifetimeSafetyDanglingField : DiagGroup<"lifetime-safety-dangling-field"> { + code Documentation = [{Warning to detect dangling field references.}]; +} + +def LifetimeSafetyPermissive : DiagGroup<"lifetime-safety-permissive", + [LifetimeSafetyDanglingField]>; def LifetimeSafetyStrict : DiagGroup<"lifetime-safety-strict">; + def LifetimeSafety : DiagGroup<"lifetime-safety", [LifetimeSafetyPermissive, LifetimeSafetyStrict]> { code Documentation = [{ - Experimental warnings to detect use-after-free and related temporal safety bugs based on lifetime safety analysis. + Warnings to detect use-after-free and related temporal safety bugs based on lifetime safety analysis. }]; } + def LifetimeSafetyCrossTUSuggestions : DiagGroup<"lifetime-safety-cross-tu-suggestions">; def LifetimeSafetyIntraTUSuggestions diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index c786eb4486829..807440c107897 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -10826,9 +10826,16 @@ def warn_lifetime_safety_return_stack_addr_strict InGroup<LifetimeSafetyStrict>, DefaultIgnore; +def warn_lifetime_safety_dangling_field + : Warning<"address of stack memory escapes to a field">, + InGroup<LifetimeSafetyDanglingField>, + DefaultIgnore; + def note_lifetime_safety_used_here : Note<"later used here">; def note_lifetime_safety_destroyed_here : Note<"destroyed here">; def note_lifetime_safety_returned_here : Note<"returned here">; +def note_lifetime_safety_dangling_field_here: Note<"this field dangles">; +def note_lifetime_safety_escapes_to_field_here: Note<"escapes to this field">; def warn_lifetime_safety_intra_tu_param_suggestion : Warning<"parameter in intra-TU function should be marked " diff --git a/clang/lib/Analysis/CFG.cpp b/clang/lib/Analysis/CFG.cpp index a9c7baa00543c..8001a67a5e158 100644 --- a/clang/lib/Analysis/CFG.cpp +++ b/clang/lib/Analysis/CFG.cpp @@ -1667,12 +1667,20 @@ std::unique_ptr<CFG> CFGBuilder::buildCFG(const Decl *D, Stmt *Statement) { assert(Succ == &cfg->getExit()); Block = nullptr; // the EXIT block is empty. Create all other blocks lazily. - // Add parameters to the initial scope to handle their dtos and lifetime ends. - LocalScope *paramScope = nullptr; - if (const auto *FD = dyn_cast_or_null<FunctionDecl>(D)) - for (ParmVarDecl *PD : FD->parameters()) - paramScope = addLocalScopeForVarDecl(PD, paramScope); - + if (BuildOpts.AddLifetime && BuildOpts.AddParameterLifetimes) { + // Add parameters to the initial scope to handle lifetime ends. + LocalScope *paramScope = nullptr; + if (const auto *FD = dyn_cast_or_null<FunctionDecl>(D)) + for (ParmVarDecl *PD : FD->parameters()) { + paramScope = addLocalScopeForVarDecl(PD, paramScope); + } + if (auto *C = dyn_cast<CompoundStmt>(Statement)) + if (C->body_empty() || !isa<ReturnStmt>(*C->body_rbegin())) + // If the body ends with a ReturnStmt, the dtors will be added in + // VisitReturnStmt. + addAutomaticObjHandling(ScopePos, LocalScope::const_iterator(), + Statement); + } if (BuildOpts.AddImplicitDtors) if (const CXXDestructorDecl *DD = dyn_cast_or_null<CXXDestructorDecl>(D)) addImplicitDtorsForDestructor(DD); diff --git a/clang/lib/Analysis/FlowSensitive/AdornedCFG.cpp b/clang/lib/Analysis/FlowSensitive/AdornedCFG.cpp index 6c4847c7c23fb..6f4c3e6531e9b 100644 --- a/clang/lib/Analysis/FlowSensitive/AdornedCFG.cpp +++ b/clang/lib/Analysis/FlowSensitive/AdornedCFG.cpp @@ -162,6 +162,7 @@ llvm::Expected<AdornedCFG> AdornedCFG::build(const Decl &D, Stmt &S, Options.AddInitializers = true; Options.AddCXXDefaultInitExprInCtors = true; Options.AddLifetime = true; + Options.AddParameterLifetimes = true; // Ensure that all sub-expressions in basic blocks are evaluated. Options.setAllAlwaysAdd(); diff --git a/clang/lib/Analysis/LifetimeSafety/Checker.cpp b/clang/lib/Analysis/LifetimeSafety/Checker.cpp index c6368786f34fe..c954a9b14bcdf 100644 --- a/clang/lib/Analysis/LifetimeSafety/Checker.cpp +++ b/clang/lib/Analysis/LifetimeSafety/Checker.cpp @@ -12,6 +12,7 @@ //===----------------------------------------------------------------------===// #include "clang/Analysis/Analyses/LifetimeSafety/Checker.h" +#include "clang/AST/Decl.h" #include "clang/AST/Expr.h" #include "clang/Analysis/Analyses/LifetimeSafety/Facts.h" #include "clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h" @@ -51,12 +52,13 @@ struct PendingWarning { using AnnotationTarget = llvm::PointerUnion<const ParmVarDecl *, const CXXMethodDecl *>; +using EscapingTarget = llvm::PointerUnion<const Expr *, const FieldDecl *>; class LifetimeChecker { private: llvm::DenseMap<LoanID, PendingWarning> FinalWarningsMap; llvm::DenseMap<AnnotationTarget, const Expr *> AnnotationWarningsMap; - llvm::DenseMap<const ParmVarDecl *, const Expr *> NoescapeWarningsMap; + llvm::DenseMap<const ParmVarDecl *, EscapingTarget> NoescapeWarningsMap; const LoanPropagationAnalysis &LoanPropagation; const LiveOriginsAnalysis &LiveOrigins; const FactManager &FactMgr; @@ -92,22 +94,36 @@ class LifetimeChecker { void checkAnnotations(const OriginEscapesFact *OEF) { OriginID EscapedOID = OEF->getEscapedOriginID(); LoanSet EscapedLoans = LoanPropagation.getLoans(EscapedOID, OEF); + auto CheckParam = [&](const ParmVarDecl *PVD) { + // 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()); + return; + } + // Suggest lifetimebound for parameter escaping through return. + if (!PVD->hasAttr<LifetimeBoundAttr>()) + if (auto *ReturnEsc = dyn_cast<ReturnEscapeFact>(OEF)) + AnnotationWarningsMap.try_emplace(PVD, ReturnEsc->getReturnExpr()); + // TODO: Suggest lifetime_capture_by(this) for parameter escaping to a + // field! + }; + auto CheckImplicitThis = [&](const CXXMethodDecl *MD) { + if (!implicitObjectParamIsLifetimeBound(MD)) + if (auto *ReturnEsc = dyn_cast<ReturnEscapeFact>(OEF)) + AnnotationWarningsMap.try_emplace(MD, ReturnEsc->getReturnExpr()); + }; for (LoanID LID : EscapedLoans) { const Loan *L = FactMgr.getLoanMgr().getLoan(LID); - if (const auto *PL = dyn_cast<PlaceholderLoan>(L)) { - if (const auto *PVD = PL->getParmVarDecl()) { - if (PVD->hasAttr<NoEscapeAttr>()) { - NoescapeWarningsMap.try_emplace(PVD, OEF->getEscapeExpr()); - continue; - } - if (PVD->hasAttr<LifetimeBoundAttr>()) - continue; - AnnotationWarningsMap.try_emplace(PVD, OEF->getEscapeExpr()); - } else if (const auto *MD = PL->getMethodDecl()) { - if (!implicitObjectParamIsLifetimeBound(MD)) - AnnotationWarningsMap.try_emplace(MD, OEF->getEscapeExpr()); - } - } + const auto *PL = dyn_cast<PlaceholderLoan>(L); + if (!PL) + continue; + if (const auto *PVD = PL->getParmVarDecl()) + CheckParam(PVD); + else if (const auto *MD = PL->getMethodDecl()) + CheckImplicitThis(MD); } } @@ -169,10 +185,16 @@ class LifetimeChecker { SemaHelper->reportUseAfterFree(IssueExpr, UF->getUseExpr(), ExpiryLoc, Confidence); else if (const auto *OEF = - CausingFact.dyn_cast<const OriginEscapesFact *>()) - SemaHelper->reportUseAfterReturn(IssueExpr, OEF->getEscapeExpr(), - ExpiryLoc, Confidence); - else + CausingFact.dyn_cast<const OriginEscapesFact *>()) { + if (const auto *RetEscape = dyn_cast<ReturnEscapeFact>(OEF)) + SemaHelper->reportUseAfterReturn( + IssueExpr, RetEscape->getReturnExpr(), ExpiryLoc, Confidence); + else if (const auto *FieldEscape = dyn_cast<FieldEscapeFact>(OEF)) + SemaHelper->reportDanglingField( + IssueExpr, FieldEscape->getFieldDecl(), ExpiryLoc); + else + llvm_unreachable("Unhandled OriginEscapesFact type"); + } else llvm_unreachable("Unhandled CausingFact type"); } } @@ -237,8 +259,14 @@ class LifetimeChecker { } void reportNoescapeViolations() { - for (auto [PVD, EscapeExpr] : NoescapeWarningsMap) - SemaHelper->reportNoescapeViolation(PVD, EscapeExpr); + 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 + llvm_unreachable("Unhandled EscapingTarget type"); + } } void inferAnnotations() { diff --git a/clang/lib/Analysis/LifetimeSafety/Facts.cpp b/clang/lib/Analysis/LifetimeSafety/Facts.cpp index 2673ce5ba354b..1fc72aa0a4259 100644 --- a/clang/lib/Analysis/LifetimeSafety/Facts.cpp +++ b/clang/lib/Analysis/LifetimeSafety/Facts.cpp @@ -45,11 +45,18 @@ void OriginFlowFact::dump(llvm::raw_ostream &OS, const LoanManager &, OS << "\n"; } -void OriginEscapesFact::dump(llvm::raw_ostream &OS, const LoanManager &, - const OriginManager &OM) const { +void ReturnEscapeFact::dump(llvm::raw_ostream &OS, const LoanManager &, + const OriginManager &OM) const { OS << "OriginEscapes ("; OM.dump(getEscapedOriginID(), OS); - OS << ")\n"; + OS << ", via Return)\n"; +} + +void FieldEscapeFact::dump(llvm::raw_ostream &OS, const LoanManager &, + const OriginManager &OM) const { + OS << "OriginEscapes ("; + OM.dump(getEscapedOriginID(), OS); + OS << ", via Field)\n"; } void UseFact::dump(llvm::raw_ostream &OS, const LoanManager &, diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp index c982b255d54ea..c1b8322c5ec55 100644 --- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp +++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp @@ -10,6 +10,7 @@ #include <string> #include "clang/AST/OperationKinds.h" +#include "clang/Analysis/Analyses/LifetimeSafety/Facts.h" #include "clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h" #include "clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h" #include "clang/Analysis/Analyses/LifetimeSafety/Origins.h" @@ -107,6 +108,9 @@ void FactsGenerator::run() { const CFGElement &Element = Block->Elements[I]; if (std::optional<CFGStmt> CS = Element.getAs<CFGStmt>()) Visit(CS->getStmt()); + else if (std::optional<CFGInitializer> Initializer = + Element.getAs<CFGInitializer>()) + handleCXXCtorInitializer(Initializer->getInitializer()); else if (std::optional<CFGLifetimeEnds> LifetimeEnds = Element.getAs<CFGLifetimeEnds>()) handleLifetimeEnds(*LifetimeEnds); @@ -114,6 +118,9 @@ void FactsGenerator::run() { Element.getAs<CFGTemporaryDtor>()) handleTemporaryDtor(*TemporaryDtor); } + if (Block == &Cfg.getExit()) + handleExitBlock(); + CurrentBlockFacts.append(EscapesInCurrentBlock.begin(), EscapesInCurrentBlock.end()); FactMgr.addBlockFacts(Block, CurrentBlockFacts); @@ -180,6 +187,13 @@ void FactsGenerator::VisitCXXConstructExpr(const CXXConstructExpr *CCE) { } } +void FactsGenerator::handleCXXCtorInitializer(const CXXCtorInitializer *CII) { + // Flows origins from the initializer expression to the field. + // Example: `MyObj(std::string s) : view(s) {}` + if (const FieldDecl *FD = CII->getAnyMember()) + killAndFlowOrigin(*FD, *CII->getInit()); +} + void FactsGenerator::VisitCXXMemberCallExpr(const CXXMemberCallExpr *MCE) { // Specifically for conversion operators, // like `std::string_view p = std::string{};` @@ -317,31 +331,42 @@ void FactsGenerator::VisitReturnStmt(const ReturnStmt *RS) { if (const Expr *RetExpr = RS->getRetValue()) { if (OriginList *List = getOriginsList(*RetExpr)) for (OriginList *L = List; L != nullptr; L = L->peelOuterOrigin()) - EscapesInCurrentBlock.push_back(FactMgr.createFact<OriginEscapesFact>( + EscapesInCurrentBlock.push_back(FactMgr.createFact<ReturnEscapeFact>( L->getOuterOriginID(), RetExpr)); } } void FactsGenerator::handleAssignment(const Expr *LHSExpr, const Expr *RHSExpr) { - if (const auto *DRE_LHS = - dyn_cast<DeclRefExpr>(LHSExpr->IgnoreParenImpCasts())) { - OriginList *LHSList = getOriginsList(*DRE_LHS); - assert(LHSList && "LHS is a DRE and should have an origin list"); - OriginList *RHSList = getOriginsList(*RHSExpr); - - // For operator= with reference parameters (e.g., - // `View& operator=(const View&)`), the RHS argument stays an lvalue, - // unlike built-in assignment where LValueToRValue cast strips the outer - // lvalue origin. Strip it manually to get the actual value origins being - // assigned. - RHSList = getRValueOrigins(RHSExpr, RHSList); + LHSExpr = LHSExpr->IgnoreParenImpCasts(); + OriginList *LHSList = nullptr; - markUseAsWrite(DRE_LHS); - // Kill the old loans of the destination origin and flow the new loans - // from the source origin. - flow(LHSList->peelOuterOrigin(), RHSList, /*Kill=*/true); + 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"); + } + // Handle assignment to member fields (e.g., `this->view = s` or `view = s`). + // This enables detection of dangling fields when local values escape to + // fields. + 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"); } + if (!LHSList) + return; + OriginList *RHSList = getOriginsList(*RHSExpr); + // For operator= with reference parameters (e.g., + // `View& operator=(const View&)`), the RHS argument stays an lvalue, + // unlike built-in assignment where LValueToRValue cast strips the outer + // lvalue origin. Strip it manually to get the actual value origins being + // assigned. + RHSList = getRValueOrigins(RHSExpr, RHSList); + + 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 + // from the source origin. + flow(LHSList->peelOuterOrigin(), RHSList, /*Kill=*/true); } void FactsGenerator::VisitBinaryOperator(const BinaryOperator *BO) { @@ -464,6 +489,14 @@ void FactsGenerator::handleTemporaryDtor( } } +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())) + EscapesInCurrentBlock.push_back( + FactMgr.createFact<FieldEscapeFact>(O.ID, FD)); +} + void FactsGenerator::handleGSLPointerConstruction(const CXXConstructExpr *CCE) { assert(isGslPointerType(CCE->getType())); if (CCE->getNumArgs() != 1) diff --git a/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp b/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp index 862dca256280f..f210fb4d752d4 100644 --- a/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp +++ b/clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp @@ -8,6 +8,7 @@ #include "clang/Analysis/Analyses/LifetimeSafety/LiveOrigins.h" #include "Dataflow.h" +#include "clang/Analysis/Analyses/LifetimeSafety/Facts.h" #include "llvm/Support/ErrorHandling.h" namespace clang::lifetimes::internal { @@ -56,8 +57,12 @@ struct Lattice { static SourceLocation GetFactLoc(CausingFactType F) { if (const auto *UF = F.dyn_cast<const UseFact *>()) return UF->getUseExpr()->getExprLoc(); - if (const auto *OEF = F.dyn_cast<const OriginEscapesFact *>()) - return OEF->getEscapeExpr()->getExprLoc(); + if (const auto *OEF = F.dyn_cast<const OriginEscapesFact *>()) { + if (auto *ReturnEsc = dyn_cast<ReturnEscapeFact>(OEF)) + return ReturnEsc->getReturnExpr()->getExprLoc(); + if (auto *FieldEsc = dyn_cast<FieldEscapeFact>(OEF)) + return FieldEsc->getFieldDecl()->getLocation(); + } llvm_unreachable("unhandled causing fact in PointerUnion"); } diff --git a/clang/lib/Analysis/LifetimeSafety/Origins.cpp b/clang/lib/Analysis/LifetimeSafety/Origins.cpp index bf539303695b1..9141859a81345 100644 --- a/clang/lib/Analysis/LifetimeSafety/Origins.cpp +++ b/clang/lib/Analysis/LifetimeSafety/Origins.cpp @@ -9,6 +9,7 @@ #include "clang/Analysis/Analyses/LifetimeSafety/Origins.h" #include "clang/AST/ASTContext.h" #include "clang/AST/Attr.h" +#include "clang/AST/Decl.h" #include "clang/AST/DeclCXX.h" #include "clang/AST/DeclTemplate.h" #include "clang/AST/Expr.h" @@ -150,25 +151,33 @@ OriginList *OriginManager::getOrCreateList(const Expr *E) { return *ThisOrigins; } - // Special handling for DeclRefExpr to share origins with the underlying decl. - if (auto *DRE = dyn_cast<DeclRefExpr>(E)) { + // Special handling for expressions referring to a decl to share origins with + // the underlying decl. + const ValueDecl *ReferencedDecl = nullptr; + if (auto *DRE = dyn_cast<DeclRefExpr>(E)) + ReferencedDecl = DRE->getDecl(); + else if (auto *ME = dyn_cast<MemberExpr>(E)) + if (auto *Field = dyn_cast<FieldDecl>(ME->getMemberDecl()); + Field && isa<CXXThisExpr>(ME->getBase())) + ReferencedDecl = Field; + if (ReferencedDecl) { OriginList *Head = nullptr; - // For non-reference declarations (e.g., `int* p`), the DeclRefExpr is an + // For non-reference declarations (e.g., `int* p`), the expression is an // lvalue (addressable) that can be borrowed, so we create an outer origin // for the lvalue itself, with the pointee being the declaration's list. // This models taking the address: `&p` borrows the storage of `p`, not what // `p` points to. - if (doesDeclHaveStorage(DRE->getDecl())) { - Head = createNode(DRE, QualType{}); - // This ensures origin sharing: multiple DeclRefExprs to the same + if (doesDeclHaveStorage(ReferencedDecl)) { + Head = createNode(E, QualType{}); + // This ensures origin sharing: multiple expressions to the same // declaration share the same underlying origins. - Head->setInnerOriginList(getOrCreateList(DRE->getDecl())); + Head->setInnerOriginList(getOrCreateList(ReferencedDecl)); } else { // For reference-typed declarations (e.g., `int& r = p`) which have no // storage, the DeclRefExpr directly reuses the declaration's list since // references don't add an extra level of indirection at the expression // level. - Head = getOrCreateList(DRE->getDecl()); + Head = getOrCreateList(ReferencedDecl); } return ExprToList[E] = Head; } diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index 913962dc0c3e0..0c96b0afef1a7 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -2891,16 +2891,24 @@ class LifetimeSafetySemaHelperImpl : public LifetimeSafetySemaHelper { << UseExpr->getSourceRange(); } - void reportUseAfterReturn(const Expr *IssueExpr, const Expr *EscapeExpr, + void reportUseAfterReturn(const Expr *IssueExpr, const Expr *ReturnExpr, SourceLocation ExpiryLoc, Confidence C) override { S.Diag(IssueExpr->getExprLoc(), C == Confidence::Definite ? diag::warn_lifetime_safety_return_stack_addr_permissive : diag::warn_lifetime_safety_return_stack_addr_strict) << IssueExpr->getSourceRange(); - - S.Diag(EscapeExpr->getExprLoc(), diag::note_lifetime_safety_returned_here) - << EscapeExpr->getSourceRange(); + S.Diag(ReturnExpr->getExprLoc(), diag::note_lifetime_safety_returned_here) + << ReturnExpr->getSourceRange(); + } + void reportDanglingField(const Expr *IssueExpr, + const FieldDecl *DanglingField, + SourceLocation ExpiryLoc) override { + S.Diag(IssueExpr->getExprLoc(), diag::warn_lifetime_safety_dangling_field) + << IssueExpr->getSourceRange(); + S.Diag(DanglingField->getLocation(), + diag::note_lifetime_safety_dangling_field_here) + << DanglingField->getEndLoc(); } void suggestLifetimeboundToParmVar(SuggestionScope Scope, @@ -2951,6 +2959,17 @@ class LifetimeSafetySemaHelperImpl : public LifetimeSafetySemaHelper { << EscapeExpr->getSourceRange(); } + void reportNoescapeViolation(const ParmVarDecl *ParmWithNoescape, + const FieldDecl *EscapeField) override { + S.Diag(ParmWithNoescape->getBeginLoc(), + diag::warn_lifetime_safety_noescape_escapes) + << ParmWithNoescape->getSourceRange(); + + S.Diag(EscapeField->getLocation(), + diag::note_lifetime_safety_escapes_to_field_here) + << EscapeField->getEndLoc(); + } + void addLifetimeBoundToImplicitThis(const CXXMethodDecl *MD) override { S.addLifetimeBoundToImplicitThis(const_cast<CXXMethodDecl *>(MD)); } @@ -2979,6 +2998,7 @@ LifetimeSafetyTUAnalysis(Sema &S, TranslationUnitDecl *TU, AnalysisDeclContext AC(nullptr, FD); AC.getCFGBuildOptions().PruneTriviallyFalseEdges = false; AC.getCFGBuildOptions().AddLifetime = true; + AC.getCFGBuildOptions().AddParameterLifetimes = true; AC.getCFGBuildOptions().AddImplicitDtors = true; AC.getCFGBuildOptions().AddTemporaryDtors = true; AC.getCFGBuildOptions().setAllAlwaysAdd(); @@ -3087,6 +3107,7 @@ void clang::sema::AnalysisBasedWarnings::IssueWarnings( AC.getCFGBuildOptions().AddEHEdges = false; AC.getCFGBuildOptions().AddInitializers = true; AC.getCFGBuildOptions().AddImplicitDtors = true; + AC.getCFGBuildOptions().AddParameterLifetimes = true; AC.getCFGBuildOptions().AddTemporaryDtors = true; AC.getCFGBuildOptions().AddCXXNewAllocator = false; AC.getCFGBuildOptions().AddCXXDefaultInitExprInCtors = true; diff --git a/clang/test/Analysis/lifetime-cfg-output.cpp b/clang/test/Analysis/lifetime-cfg-output.cpp index 36b36eddc440c..0a75c5bcc0bcc 100644 --- a/clang/test/Analysis/lifetime-cfg-output.cpp +++ b/clang/test/Analysis/lifetime-cfg-output.cpp @@ -935,31 +935,3 @@ int backpatched_goto() { goto label; i++; } - -// CHECK: [B2 (ENTRY)] -// CHECK-NEXT: Succs (1): B1 -// CHECK: [B1] -// CHECK-NEXT: 1: a -// CHECK-NEXT: 2: [B1.1] (ImplicitCastExpr, LValueToRValue, int) -// CHECK-NEXT: 3: b -// CHECK-NEXT: 4: [B1.3] (ImplicitCastExpr, LValueToRValue, int) -// CHECK-NEXT: 5: [B1.2] + [B1.4] -// CHECK-NEXT: 6: c -// CHECK-NEXT: 7: [B1.6] (ImplicitCastExpr, LValueToRValue, int) -// CHECK-NEXT: 8: [B1.5] + [B1.7] -// CHECK-NEXT: 9: int res = a + b + c; -// CHECK-NEXT: 10: res -// CHECK-NEXT: 11: [B1.10] (ImplicitCastExpr, LValueToRValue, int) -// CHECK-NEXT: 12: return [B1.11]; -// CHECK-NEXT: 13: [B1.9] (Lifetime ends) -// CHECK-NEXT: 14: [Parm: c] (Lifetime ends) -// CHECK-NEXT: 15: [Parm: b] (Lifetime ends) -// CHECK-NEXT: 16: [Parm: a] (Lifetime ends) -// CHECK-NEXT: Preds (1): B2 -// CHECK-NEXT: Succs (1): B0 -// CHECK: [B0 (EXIT)] -// CHECK-NEXT: Preds (1): B1 -int test_param_scope_end_order(int a, int b, int c) { - int res = a + b + c; - return res; -} diff --git a/clang/test/Analysis/scopes-cfg-output.cpp b/clang/test/Analysis/scopes-cfg-output.cpp index 9c75492c33a42..6ed6f3638f75b 100644 --- a/clang/test/Analysis/scopes-cfg-output.cpp +++ b/clang/test/Analysis/scopes-cfg-output.cpp @@ -1437,14 +1437,12 @@ void test_cleanup_functions() { // CHECK-NEXT: 4: return; // CHECK-NEXT: 5: CleanupFunction (cleanup_int) // CHECK-NEXT: 6: CFGScopeEnd(i) -// CHECK-NEXT: 7: CFGScopeEnd(m) // CHECK-NEXT: Preds (1): B3 // CHECK-NEXT: Succs (1): B0 // CHECK: [B2] // CHECK-NEXT: 1: return; // CHECK-NEXT: 2: CleanupFunction (cleanup_int) // CHECK-NEXT: 3: CFGScopeEnd(i) -// CHECK-NEXT: 4: CFGScopeEnd(m) // CHECK-NEXT: Preds (1): B3 // CHECK-NEXT: Succs (1): B0 // CHECK: [B3] diff --git a/clang/test/Sema/warn-lifetime-analysis-nocfg.cpp b/clang/test/Sema/warn-lifetime-analysis-nocfg.cpp index 29554f5a98029..c82cf41b07361 100644 --- a/clang/test/Sema/warn-lifetime-analysis-nocfg.cpp +++ b/clang/test/Sema/warn-lifetime-analysis-nocfg.cpp @@ -1,5 +1,7 @@ // RUN: %clang_cc1 -fsyntax-only -Wdangling -Wdangling-field -Wreturn-stack-address -verify %s -// RUN: %clang_cc1 -fsyntax-only -Wlifetime-safety -Wno-dangling -verify=cfg %s +// RUN: %clang_cc1 -fsyntax-only -Wlifetime-safety -Wno-dangling -verify=cfg,cfg-field %s + +// FIXME: cfg-field should be detected in end-of-TU analysis but it doesn't work for constructors! // RUN: %clang_cc1 -fsyntax-only -flifetime-safety-inference -fexperimental-lifetime-safety-tu-analysis -Wlifetime-safety -Wno-dangling -verify=cfg %s #include "Inputs/lifetime-analysis.h" @@ -80,11 +82,18 @@ void dangligGslPtrFromTemporary() { } struct DanglingGslPtrField { - MyIntPointer p; // expected-note {{pointer member declared here}} - MyLongPointerFromConversion p2; // expected-note {{pointer member declared here}} - DanglingGslPtrField(int i) : p(&i) {} // TODO - DanglingGslPtrField() : p2(MyLongOwnerWithConversion{}) {} // expected-warning {{initializing pointer member 'p2' to point to a temporary object whose lifetime is shorter than the lifetime of the constructed object}} - DanglingGslPtrField(double) : p(MyIntOwner{}) {} // expected-warning {{initializing pointer member 'p' to point to a temporary object whose lifetime is shorter than the lifetime of the constructed object}} + MyIntPointer p; // expected-note {{pointer member declared here}} \ + // cfg-field-note 3 {{this field dangles}} + MyLongPointerFromConversion p2; // expected-note {{pointer member declared here}} \ + // cfg-field-note 2 {{this field dangles}} + + DanglingGslPtrField(int i) : p(&i) {} // cfg-field-warning {{address of stack memory escapes to a field}} + DanglingGslPtrField() : p2(MyLongOwnerWithConversion{}) {} // expected-warning {{initializing pointer member 'p2' to point to a temporary object whose lifetime is shorter than the lifetime of the constructed object}} \ + // cfg-field-warning {{address of stack memory escapes to a field}} + DanglingGslPtrField(double) : p(MyIntOwner{}) {} // expected-warning {{initializing pointer member 'p' to point to a temporary object whose lifetime is shorter than the lifetime of the constructed object}} \ + // cfg-field-warning {{address of stack memory escapes to a field}} + DanglingGslPtrField(MyIntOwner io) : p(io) {} // cfg-field-warning {{address of stack memory escapes to a field}} + DanglingGslPtrField(MyLongOwnerWithConversion lo) : p2(lo) {} // cfg-field-warning {{address of stack memory escapes to a field}} }; MyIntPointer danglingGslPtrFromLocal() { @@ -1099,10 +1108,11 @@ struct Foo2 { }; struct Test { - Test(Foo2 foo) : bar(foo.bar.get()), // OK + Test(Foo2 foo) : bar(foo.bar.get()), // OK \ + // FIXME: cfg-field-warning {{address of stack memory escapes to a field}} storage(std::move(foo.bar)) {}; - Bar* bar; + Bar* bar; // cfg-field-note {{this field dangles}} std::unique_ptr<Bar> storage; }; diff --git a/clang/test/Sema/warn-lifetime-safety-dangling-field.cpp b/clang/test/Sema/warn-lifetime-safety-dangling-field.cpp new file mode 100644 index 0000000000000..dfc43e49906f2 --- /dev/null +++ b/clang/test/Sema/warn-lifetime-safety-dangling-field.cpp @@ -0,0 +1,175 @@ +// RUN: %clang_cc1 -fsyntax-only -Wlifetime-safety -Wno-dangling -verify %s + +#include "Inputs/lifetime-analysis.h" + +template<int N> struct Dummy {}; +static std::string kGlobal = "GLOBAL"; +void takeString(std::string&& s); + +std::string_view construct_view(const std::string& str [[clang::lifetimebound]]); + +struct CtorInit { + std::string_view view; // expected-note {{this field dangles}} + CtorInit(std::string s) : view(s) {} // expected-warning {{address of stack memory escapes to a field}} +}; + +struct CtorSet { + std::string_view view; // expected-note {{this field dangles}} + CtorSet(std::string s) { view = s; } // expected-warning {{address of stack memory escapes to a field}} +}; + +struct CtorInitLifetimeBound { + std::string_view view; // expected-note {{this field dangles}} + CtorInitLifetimeBound(std::string s) : view(construct_view(s)) {} // expected-warning {{address of stack memory escapes to a field}} +}; + +struct CtorInitButMoved { + std::string_view view; + CtorInitButMoved(std::string s) : view(s) { takeString(std::move(s)); } +}; + +struct CtorInitButMovedOwned { + std::string owned; + std::string_view view; + CtorInitButMovedOwned(std::string s) : view(s), owned(std::move(s)) {} + CtorInitButMovedOwned(Dummy<1>, std::string s) : owned(std::move(s)), view(owned) {} +}; + +struct CtorInitMultipleViews { + std::string_view view1; // expected-note {{this field dangles}} + std::string_view view2; // expected-note {{this field dangles}} + CtorInitMultipleViews(std::string s) : view1(s), // expected-warning {{address of stack memory escapes to a field}} + view2(s) {} // expected-warning {{address of stack memory escapes to a field}} +}; + +struct CtorInitMultipleParams { + std::string_view view1; // expected-note {{this field dangles}} + std::string_view view2; // expected-note {{this field dangles}} + CtorInitMultipleParams(std::string s1, std::string s2) : view1(s1), // expected-warning {{address of stack memory escapes to a field}} + view2(s2) {} // expected-warning {{address of stack memory escapes to a field}} +}; + +struct CtorRefField { + const std::string& str; // expected-note {{this field dangles}} + const std::string_view& view; // expected-note {{this field dangles}} + CtorRefField(std::string s, std::string_view v) : str(s), // expected-warning {{address of stack memory escapes to a field}} + view(v) {} // expected-warning {{address of stack memory escapes to a field}} + CtorRefField(Dummy<1> ok, const std::string& s, const std::string_view& v): str(s), view(v) {} +}; + +struct CtorPointerField { + const char* ptr; // expected-note {{this field dangles}} + CtorPointerField(std::string s) : ptr(s.data()) {} // expected-warning {{address of stack memory escapes to a field}} + CtorPointerField(Dummy<1> ok, const std::string& s) : ptr(s.data()) {} + CtorPointerField(Dummy<2> ok, std::string_view view) : ptr(view.data()) {} +}; + +struct MemberSetters { + std::string_view view; // expected-note 5 {{this field dangles}} + const char* p; // expected-note 5 {{this field dangles}} + + void setWithParam(std::string s) { + view = s; // expected-warning {{address of stack memory escapes to a field}} + p = s.data(); // expected-warning {{address of stack memory escapes to a field}} + } + + void setWithParamAndReturn(std::string s) { + view = s; // expected-warning {{address of stack memory escapes to a field}} + p = s.data(); // expected-warning {{address of stack memory escapes to a field}} + return; + } + + void setWithParamOk(const std::string& s) { + view = s; + p = s.data(); + } + + void setWithParamOkAndReturn(const std::string& s) { + view = s; + p = s.data(); + return; + } + + void setWithLocal() { + std::string s; + view = s; // expected-warning {{address of stack memory escapes to a field}} + p = s.data(); // expected-warning {{address of stack memory escapes to a field}} + } + + void setWithLocalButMoved() { + std::string s; + view = s; + p = s.data(); + takeString(std::move(s)); + } + + void setWithGlobal() { + view = kGlobal; + p = kGlobal.data(); + } + + void setWithLocalThenWithGlobal() { + std::string local; + view = local; + p = local.data(); + + view = kGlobal; + p = kGlobal.data(); + } + + void setWithGlobalThenWithLocal() { + view = kGlobal; + p = kGlobal.data(); + + std::string local; + view = local; // expected-warning {{address of stack memory escapes to a field}} + p = local.data(); // expected-warning {{address of stack memory escapes to a field}} + } + + void use_after_scope() { + { + std::string local; + view = local; // expected-warning {{address of stack memory escapes to a field}} + p = local.data(); // expected-warning {{address of stack memory escapes to a field}} + } + (void)view; + (void)p; + } + + void use_after_scope_saved_after_reassignment() { + { + std::string local; + view = local; + p = local.data(); + } + (void)view; + (void)p; + + view = kGlobal; + p = kGlobal.data(); + } +}; + +// FIXME: Detect escape to field of field. +struct IndirectEscape{ + struct { + const char *p; + } b; + + void foo() { + std::string s; + b.p = s.data(); + } +}; + +// FIXME: Detect escape to field of field. +struct IndirectEscape2 { + struct { + const char *p; + }; + + void foo() { + std::string s; + p = s.data(); + } +}; diff --git a/clang/test/Sema/warn-lifetime-safety-dataflow.cpp b/clang/test/Sema/warn-lifetime-safety-dataflow.cpp index a45100feb3f28..7e2215b8deedc 100644 --- a/clang/test/Sema/warn-lifetime-safety-dataflow.cpp +++ b/clang/test/Sema/warn-lifetime-safety-dataflow.cpp @@ -27,7 +27,7 @@ MyObj* return_local_addr() { // CHECK-NEXT: Src: [[O_P]] (Decl: p, Type : MyObj *) // CHECK: Expire ([[L_X]] (Path: x)) // CHECK: Expire ({{[0-9]+}} (Path: p)) -// CHECK: OriginEscapes ([[O_RET_VAL]] (Expr: ImplicitCastExpr, Type : MyObj *)) +// CHECK: OriginEscapes ([[O_RET_VAL]] (Expr: ImplicitCastExpr, Type : MyObj *), via Return) } // Loan Expiration (Automatic Variable, C++) diff --git a/clang/test/Sema/warn-lifetime-safety-noescape.cpp b/clang/test/Sema/warn-lifetime-safety-noescape.cpp index 91edd2e33edf8..ee661add0acc8 100644 --- a/clang/test/Sema/warn-lifetime-safety-noescape.cpp +++ b/clang/test/Sema/warn-lifetime-safety-noescape.cpp @@ -120,13 +120,12 @@ void escape_through_global_var(const MyObj& in [[clang::noescape]]) { global_view = in; } -// FIXME: Escaping through a member variable is not detected. struct ObjConsumer { - void escape_through_member(const MyObj& in [[clang::noescape]]) { + void escape_through_member(const MyObj& in [[clang::noescape]]) { // expected-warning {{parameter is marked [[clang::noescape]] but escapes}} member_view = in; } - View member_view; + View member_view; // expected-note {{escapes to this field}} }; // FIXME: Escaping through another param is not detected. _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
