https://github.com/usx95 updated https://github.com/llvm/llvm-project/pull/159991
>From 8a7ec9fe4b3e098b629102219cf52a116472e9a2 Mon Sep 17 00:00:00 2001 From: Utkarsh Saxena <u...@google.com> Date: Sun, 21 Sep 2025 16:30:28 +0000 Subject: [PATCH] liveness-based-lifetime-policy --- .../clang/Analysis/Analyses/LifetimeSafety.h | 18 +- clang/lib/Analysis/LifetimeSafety.cpp | 461 +++++++++------- .../test/Analysis/LifetimeSafety/benchmark.py | 82 ++- clang/test/Sema/warn-lifetime-safety.cpp | 138 +++-- .../unittests/Analysis/LifetimeSafetyTest.cpp | 501 +++++++++--------- 5 files changed, 713 insertions(+), 487 deletions(-) diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety.h index 512cb76cd6349..6d3e3f61446e0 100644 --- a/clang/include/clang/Analysis/Analyses/LifetimeSafety.h +++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety.h @@ -29,7 +29,7 @@ namespace clang::lifetimes { /// Enum to track the confidence level of a potential error. -enum class Confidence { +enum class Confidence : uint8_t { None, Maybe, // Reported as a potential error (-Wlifetime-safety-strict) Definite // Reported as a definite error (-Wlifetime-safety-permissive) @@ -55,6 +55,7 @@ class Fact; class FactManager; class LoanPropagationAnalysis; class ExpiredLoansAnalysis; +class LiveOriginAnalysis; struct LifetimeFactory; /// A generic, type-safe wrapper for an ID, distinguished by its `Tag` type. @@ -89,6 +90,7 @@ inline llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, OriginID ID) { // TODO(opt): Consider using a bitset to represent the set of loans. using LoanSet = llvm::ImmutableSet<LoanID>; using OriginSet = llvm::ImmutableSet<OriginID>; +using OriginLoanMap = llvm::ImmutableMap<OriginID, LoanSet>; /// A `ProgramPoint` identifies a location in the CFG by pointing to a specific /// `Fact`. identified by a lifetime-related event (`Fact`). @@ -110,8 +112,16 @@ class LifetimeSafetyAnalysis { /// Returns the set of loans an origin holds at a specific program point. LoanSet getLoansAtPoint(OriginID OID, ProgramPoint PP) const; - /// Returns the set of loans that have expired at a specific program point. - std::vector<LoanID> getExpiredLoansAtPoint(ProgramPoint PP) const; + /// Returns the set of origins that are live at a specific program point, + /// along with the confidence level of their liveness. + /// + /// An origin is considered live if there are potential future uses of that + /// origin after the given program point. The confidence level indicates + /// whether the origin is definitely live (Definite) due to being domintated + /// by a set of uses or only possibly live (Maybe) only on some but not all + /// control flow paths. + std::vector<std::pair<OriginID, Confidence>> + getLiveOriginsAtPoint(ProgramPoint PP) const; /// Finds the OriginID for a given declaration. /// Returns a null optional if not found. @@ -138,7 +148,7 @@ class LifetimeSafetyAnalysis { std::unique_ptr<LifetimeFactory> Factory; std::unique_ptr<FactManager> FactMgr; std::unique_ptr<LoanPropagationAnalysis> LoanPropagation; - std::unique_ptr<ExpiredLoansAnalysis> ExpiredLoans; + std::unique_ptr<LiveOriginAnalysis> LiveOrigins; }; } // namespace internal } // namespace clang::lifetimes diff --git a/clang/lib/Analysis/LifetimeSafety.cpp b/clang/lib/Analysis/LifetimeSafety.cpp index 14fb945382e65..7d880ea26833b 100644 --- a/clang/lib/Analysis/LifetimeSafety.cpp +++ b/clang/lib/Analysis/LifetimeSafety.cpp @@ -22,6 +22,7 @@ #include "llvm/ADT/SmallBitVector.h" #include "llvm/ADT/SmallVector.h" #include "llvm/Support/Debug.h" +#include "llvm/Support/ErrorHandling.h" #include "llvm/Support/TimeProfiler.h" #include <cstdint> #include <memory> @@ -865,29 +866,30 @@ class DataflowAnalysis { std::conditional_t<Dir == Direction::Forward, ForwardDataflowWorklist, BackwardDataflowWorklist>; Worklist W(Cfg, AC); + llvm::SmallBitVector Seen(Cfg.getNumBlockIDs() + 1); const CFGBlock *Start = isForward() ? &Cfg.getEntry() : &Cfg.getExit(); InStates[Start] = D.getInitialState(); + Seen.set(Start->getBlockID()); W.enqueueBlock(Start); - llvm::SmallBitVector Visited(Cfg.getNumBlockIDs() + 1); - while (const CFGBlock *B = W.dequeue()) { Lattice StateIn = getInState(B); Lattice StateOut = transferBlock(B, StateIn); OutStates[B] = StateOut; - Visited.set(B->getBlockID()); for (const CFGBlock *AdjacentB : isForward() ? B->succs() : B->preds()) { if (!AdjacentB) continue; Lattice OldInState = getInState(AdjacentB); - Lattice NewInState = D.join(OldInState, StateOut); + bool SeeingFirstTime = !Seen.test(AdjacentB->getBlockID()); + Lattice NewInState = + SeeingFirstTime ? StateOut : D.join(OldInState, StateOut); // Enqueue the adjacent block if its in-state has changed or if we have - // never visited it. - if (!Visited.test(AdjacentB->getBlockID()) || - NewInState != OldInState) { + // never seen it. + if (SeeingFirstTime || NewInState != OldInState) { InStates[AdjacentB] = NewInState; W.enqueueBlock(AdjacentB); + Seen.set(AdjacentB->getBlockID()); } } } @@ -972,42 +974,39 @@ static llvm::ImmutableSet<T> join(llvm::ImmutableSet<T> A, return A; } -/// Checks if set A is a subset of set B. -template <typename T> -static bool isSubsetOf(const llvm::ImmutableSet<T> &A, - const llvm::ImmutableSet<T> &B) { - // Empty set is a subset of all sets. - if (A.isEmpty()) - return true; - - for (const T &Elem : A) - if (!B.contains(Elem)) - return false; - return true; -} - /// Computes the key-wise union of two ImmutableMaps. +/// \param SymmetricJoin If true, the join is symmetric, applying JoinValues to +/// keys that are unique to either of the input maps. If false, the join is +/// asymmetric: keys unique to the first map are preserved as-is, while keys +/// unique to the second map are processed by JoinValues. // TODO(opt): This key-wise join is a performance bottleneck. A more // efficient merge could be implemented using a Patricia Trie or HAMT // instead of the current AVL-tree-based ImmutableMap. template <typename K, typename V, typename Joiner> static llvm::ImmutableMap<K, V> -join(llvm::ImmutableMap<K, V> A, llvm::ImmutableMap<K, V> B, - typename llvm::ImmutableMap<K, V>::Factory &F, Joiner JoinValues) { +join(const llvm::ImmutableMap<K, V> &A, const llvm::ImmutableMap<K, V> &B, + typename llvm::ImmutableMap<K, V>::Factory &F, Joiner JoinValues, + bool SymmetricJoin) { if (A.getHeight() < B.getHeight()) - std::swap(A, B); + return join(B, A, F, JoinValues, SymmetricJoin); // For each element in B, join it with the corresponding element in A // (or with an empty value if it doesn't exist in A). + llvm::ImmutableMap<K, V> Res = A; for (const auto &Entry : B) { const K &Key = Entry.first; const V &ValB = Entry.second; - if (const V *ValA = A.lookup(Key)) - A = F.add(A, Key, JoinValues(*ValA, ValB)); - else - A = F.add(A, Key, ValB); + Res = F.add(Res, Key, JoinValues(A.lookup(Key), &ValB)); + } + if (SymmetricJoin) { + for (const auto &Entry : A) { + const K &Key = Entry.first; + const V &ValA = Entry.second; + if (!B.contains(Key)) + Res = F.add(Res, Key, JoinValues(&ValA, nullptr)); + } } - return A; + return Res; } } // namespace utils @@ -1015,19 +1014,6 @@ join(llvm::ImmutableMap<K, V> A, llvm::ImmutableMap<K, V> B, // Loan Propagation Analysis // ========================================================================= // -using OriginLoanMap = llvm::ImmutableMap<OriginID, LoanSet>; -using ExpiredLoanMap = llvm::ImmutableMap<LoanID, const ExpireFact *>; - -/// An object to hold the factories for immutable collections, ensuring -/// that all created states share the same underlying memory management. -struct LifetimeFactory { - llvm::BumpPtrAllocator Allocator; - OriginLoanMap::Factory OriginMapFactory{Allocator, /*canonicalize=*/false}; - LoanSet::Factory LoanSetFactory{Allocator, /*canonicalize=*/false}; - ExpiredLoanMap::Factory ExpiredLoanMapFactory{Allocator, - /*canonicalize=*/false}; -}; - /// Represents the dataflow lattice for loan propagation. /// /// This lattice tracks which loans each origin may hold at a given program @@ -1071,10 +1057,10 @@ class LoanPropagationAnalysis public: LoanPropagationAnalysis(const CFG &C, AnalysisDeclContext &AC, FactManager &F, - LifetimeFactory &LFactory) - : DataflowAnalysis(C, AC, F), - OriginLoanMapFactory(LFactory.OriginMapFactory), - LoanSetFactory(LFactory.LoanSetFactory) {} + OriginLoanMap::Factory &OriginLoanMapFactory, + LoanSet::Factory &LoanSetFactory) + : DataflowAnalysis(C, AC, F), OriginLoanMapFactory(OriginLoanMapFactory), + LoanSetFactory(LoanSetFactory) {} using Base::transfer; @@ -1085,11 +1071,19 @@ class LoanPropagationAnalysis /// Merges two lattices by taking the union of loans for each origin. // TODO(opt): Keep the state small by removing origins which become dead. Lattice join(Lattice A, Lattice B) { - OriginLoanMap JoinedOrigins = - utils::join(A.Origins, B.Origins, OriginLoanMapFactory, - [&](LoanSet S1, LoanSet S2) { - return utils::join(S1, S2, LoanSetFactory); - }); + OriginLoanMap JoinedOrigins = utils::join( + A.Origins, B.Origins, OriginLoanMapFactory, + [&](const LoanSet *S1, const LoanSet *S2) { + assert((S1 || S2) && "unexpectedly merging 2 empty sets"); + if (!S1) + return *S2; + if (!S2) + return *S1; + return utils::join(*S1, *S2, LoanSetFactory); + }, + // Asymmetric join is a performance win. For origins present only on one + // branch, the loan set can be carried over as-is. + /*SymmetricJoin=*/false); return Lattice(JoinedOrigins); } @@ -1118,12 +1112,12 @@ class LoanPropagationAnalysis OriginLoanMapFactory.add(In.Origins, DestOID, MergedLoans)); } - LoanSet getLoans(OriginID OID, ProgramPoint P) { + LoanSet getLoans(OriginID OID, ProgramPoint P) const { return getLoans(getState(P), OID); } private: - LoanSet getLoans(Lattice L, OriginID OID) { + LoanSet getLoans(Lattice L, OriginID OID) const { if (auto *Loans = L.Origins.lookup(OID)) return *Loans; return LoanSetFactory.getEmptySet(); @@ -1131,96 +1125,181 @@ class LoanPropagationAnalysis }; // ========================================================================= // -// Expired Loans Analysis +// Live Origins Analysis // ========================================================================= // -/// The dataflow lattice for tracking the set of expired loans. -struct ExpiredLattice { - /// Map from an expired `LoanID` to the `ExpireFact` that made it expire. - ExpiredLoanMap Expired; +/// Information about why an origin is live at a program point. +struct LivenessInfo { + /// The use that makes the origin live. If liveness is propagated from + /// multiple uses along different paths, this will point to the use appearing + /// before in the translation unit. + const UseFact *CausingUseFact; + /// The confidence in the liveness of the origin. + /// `Definite`: The origin is live on all control-flow paths from the current + /// point to the function's exit (i.e. the current point is dominated by a set + /// of uses). + /// `Maybe`: indicates it is live on some but not all paths. + Confidence ConfidenceLevel; - ExpiredLattice() : Expired(nullptr) {}; - explicit ExpiredLattice(ExpiredLoanMap M) : Expired(M) {} + LivenessInfo() : CausingUseFact(nullptr), ConfidenceLevel(Confidence::None) {} + LivenessInfo(const UseFact *UF, Confidence C) + : CausingUseFact(UF), ConfidenceLevel(C) {} - bool operator==(const ExpiredLattice &Other) const { - return Expired == Other.Expired; + bool operator==(const LivenessInfo &Other) const { + return CausingUseFact == Other.CausingUseFact && + ConfidenceLevel == Other.ConfidenceLevel; } - bool operator!=(const ExpiredLattice &Other) const { + bool operator!=(const LivenessInfo &Other) const { return !(*this == Other); } + + void Profile(llvm::FoldingSetNodeID &IDBuilder) const { + IDBuilder.AddPointer(CausingUseFact); + IDBuilder.Add(ConfidenceLevel); + } +}; + +using LivenessMap = llvm::ImmutableMap<OriginID, LivenessInfo>; + +/// The dataflow lattice for origin liveness analysis. +/// It tracks which origins are live, why they're live (which UseFact), +/// and the confidence level of that liveness. +struct LivenessLattice { + LivenessMap LiveOrigins; + + LivenessLattice() : LiveOrigins(nullptr) {}; + + explicit LivenessLattice(LivenessMap L) : LiveOrigins(L) {} + + bool operator==(const LivenessLattice &Other) const { + return LiveOrigins == Other.LiveOrigins; + } + + bool operator!=(const LivenessLattice &Other) const { return !(*this == Other); } - void dump(llvm::raw_ostream &OS) const { - OS << "ExpiredLattice State:\n"; - if (Expired.isEmpty()) + void dump(llvm::raw_ostream &OS, const OriginManager &OM) const { + if (LiveOrigins.isEmpty()) OS << " <empty>\n"; - for (const auto &[ID, _] : Expired) - OS << " Loan " << ID << " is expired\n"; + for (const auto &Entry : LiveOrigins) { + OriginID OID = Entry.first; + const LivenessInfo &Info = Entry.second; + OS << " "; + OM.dump(OID, OS); + OS << " is "; + switch (Info.ConfidenceLevel) { + case Confidence::Definite: + OS << "definitely"; + break; + case Confidence::Maybe: + OS << "maybe"; + break; + case Confidence::None: + llvm_unreachable("liveness condidence should not be none."); + } + OS << " live at this point\n"; + } } }; -/// The analysis that tracks which loans have expired. -class ExpiredLoansAnalysis - : public DataflowAnalysis<ExpiredLoansAnalysis, ExpiredLattice, - Direction::Forward> { - - ExpiredLoanMap::Factory &Factory; +/// The analysis that tracks which origins are live, with granular information +/// about the causing use fact and confidence level. This is a backward +/// analysis. +class LiveOriginAnalysis + : public DataflowAnalysis<LiveOriginAnalysis, LivenessLattice, + Direction::Backward> { + FactManager &FactMgr; + LivenessMap::Factory &Factory; public: - ExpiredLoansAnalysis(const CFG &C, AnalysisDeclContext &AC, FactManager &F, - LifetimeFactory &Factory) - : DataflowAnalysis(C, AC, F), Factory(Factory.ExpiredLoanMapFactory) {} - - using Base::transfer; + LiveOriginAnalysis(const CFG &C, AnalysisDeclContext &AC, FactManager &F, + LivenessMap::Factory &SF) + : DataflowAnalysis(C, AC, F), FactMgr(F), Factory(SF) {} + using DataflowAnalysis<LiveOriginAnalysis, Lattice, + Direction::Backward>::transfer; - StringRef getAnalysisName() const { return "ExpiredLoans"; } + StringRef getAnalysisName() const { return "LiveOrigins"; } Lattice getInitialState() { return Lattice(Factory.getEmptyMap()); } - /// Merges two lattices by taking the union of the two expired loans. - Lattice join(Lattice L1, Lattice L2) { - return Lattice( - utils::join(L1.Expired, L2.Expired, Factory, - // Take the last expiry fact to make this hermetic. - [](const ExpireFact *F1, const ExpireFact *F2) { - return F1->getExpiryLoc() > F2->getExpiryLoc() ? F1 : F2; - })); - } - - Lattice transfer(Lattice In, const ExpireFact &F) { - return Lattice(Factory.add(In.Expired, F.getLoanID(), &F)); - } - - // Removes the loan from the set of expired loans. - // - // When a loan is re-issued (e.g., in a loop), it is no longer considered - // expired. A loan can be in the expired set at the point of issue due to - // the dataflow state from a previous loop iteration being propagated along - // a backedge in the CFG. - // - // Note: This has a subtle false-negative though where a loan from previous - // iteration is not overwritten by a reissue. This needs careful tracking - // of loans "across iterations" which can be considered for future - // enhancements. - // - // void foo(int safe) { - // int* p = &safe; - // int* q = &safe; - // while (condition()) { - // int x = 1; - // p = &x; // A loan to 'x' is issued to 'p' in every iteration. - // if (condition()) { - // q = p; - // } - // (void)*p; // OK — 'p' points to 'x' from new iteration. - // (void)*q; // UaF - 'q' still points to 'x' from previous iteration - // // which is now destroyed. - // } - // } - Lattice transfer(Lattice In, const IssueFact &F) { - return Lattice(Factory.remove(In.Expired, F.getLoanID())); + /// Merges two lattices by combining liveness information. + /// When the same origin has different confidence levels, we take the lower + /// one. + Lattice join(Lattice L1, Lattice L2) const { + LivenessMap Merged = L1.LiveOrigins; + // Take the earliest UseFact to make the join hermetic. + auto CombineUseFact = [](const UseFact *A, + const UseFact *B) -> const UseFact * { + return A->getUseExpr()->getExprLoc() < B->getUseExpr()->getExprLoc() ? A + : B; + }; + auto CombineConfidence = [](Confidence C1, Confidence C2) -> Confidence { + assert(C1 != Confidence::None && + "liveness confidence should not be none."); + assert(C2 != Confidence::None && + "liveness confidence should not be none."); + // Only return Definite if both paths are Definite, otherwise Maybe. + if (C1 == Confidence::Definite && C2 == Confidence::Definite) + return Confidence::Definite; + return Confidence::Maybe; + }; + auto CombineLivenessInfo = [&](const LivenessInfo *L1, + const LivenessInfo *L2) -> LivenessInfo { + assert((L1 || L2) && "unexpectedly merging 2 empty sets"); + if (!L1) + return LivenessInfo(L2->CausingUseFact, Confidence::Maybe); + if (!L2) + return LivenessInfo(L1->CausingUseFact, Confidence::Maybe); + return LivenessInfo( + CombineUseFact(L1->CausingUseFact, L2->CausingUseFact), + CombineConfidence(L1->ConfidenceLevel, L2->ConfidenceLevel)); + }; + return Lattice(utils::join( + L1.LiveOrigins, L2.LiveOrigins, Factory, CombineLivenessInfo, + // A symmetric join is required here. If an origin is live on one + // branch but not the other, its confidence must be demoted to `Maybe`. + /*SymmetricJoin=*/true)); + } + + /// A read operation makes the origin live with definite confidence, as it + /// dominates this program point. A write operation kills the liveness of + /// the origin since it overwrites the value. + Lattice transfer(Lattice In, const UseFact &UF) { + OriginID OID = UF.getUsedOrigin(FactMgr.getOriginMgr()); + // Write kills liveness. + if (UF.isWritten()) + return Lattice(Factory.remove(In.LiveOrigins, OID)); + // Read makes origin live with definite confidence (dominates this point). + return Lattice(Factory.add(In.LiveOrigins, OID, + LivenessInfo(&UF, Confidence::Definite))); + } + + /// Issuing a new loan to an origin kills its liveness. + Lattice transfer(Lattice In, const IssueFact &IF) { + return Lattice(Factory.remove(In.LiveOrigins, IF.getOriginID())); } - ExpiredLoanMap getExpiredLoans(ProgramPoint P) { return getState(P).Expired; } + /// An OriginFlow kills the liveness of the destination origin if `KillDest` + /// is true. Otherwise, it propagates liveness from destination to source. + Lattice transfer(Lattice In, const OriginFlowFact &OF) { + if (!OF.getKillDest()) + return In; + return Lattice(Factory.remove(In.LiveOrigins, OF.getDestOriginID())); + } + + LivenessMap getLiveOrigins(ProgramPoint P) const { + return getState(P).LiveOrigins; + } + + // Dump liveness values on all test points in the program. + void dump(llvm::raw_ostream &OS, const LifetimeSafetyAnalysis &LSA) const { + llvm::dbgs() << "==========================================\n"; + llvm::dbgs() << getAnalysisName() << " results:\n"; + llvm::dbgs() << "==========================================\n"; + for (const auto &Entry : LSA.getTestPoints()) { + OS << "TestPoint: " << Entry.getKey() << "\n"; + getState(Entry.getValue()).dump(OS, FactMgr.getOriginMgr()); + } + } }; // ========================================================================= // @@ -1238,84 +1317,62 @@ class LifetimeChecker { private: llvm::DenseMap<LoanID, PendingWarning> FinalWarningsMap; LoanPropagationAnalysis &LoanPropagation; - ExpiredLoansAnalysis &ExpiredLoans; + LiveOriginAnalysis &LiveOrigins; FactManager &FactMgr; AnalysisDeclContext &ADC; LifetimeSafetyReporter *Reporter; public: - LifetimeChecker(LoanPropagationAnalysis &LPA, ExpiredLoansAnalysis &ELA, + LifetimeChecker(LoanPropagationAnalysis &LPA, LiveOriginAnalysis &LOA, FactManager &FM, AnalysisDeclContext &ADC, LifetimeSafetyReporter *Reporter) - : LoanPropagation(LPA), ExpiredLoans(ELA), FactMgr(FM), ADC(ADC), + : LoanPropagation(LPA), LiveOrigins(LOA), FactMgr(FM), ADC(ADC), Reporter(Reporter) {} void run() { llvm::TimeTraceScope TimeProfile("LifetimeChecker"); for (const CFGBlock *B : *ADC.getAnalysis<PostOrderCFGView>()) for (const Fact *F : FactMgr.getFacts(B)) - if (const auto *UF = F->getAs<UseFact>()) - checkUse(UF); + if (const auto *EF = F->getAs<ExpireFact>()) + checkExpiry(EF); issuePendingWarnings(); } - /// Checks for use-after-free errors for a given use of an Origin. + /// Checks for use-after-free errors when a loan expires. /// - /// This method is called for each 'UseFact' identified in the control flow - /// graph. It determines if the loans held by the used origin have expired - /// at the point of use. - void checkUse(const UseFact *UF) { - if (UF->isWritten()) - return; - OriginID O = UF->getUsedOrigin(FactMgr.getOriginMgr()); - - // Get the set of loans that the origin might hold at this program point. - LoanSet HeldLoans = LoanPropagation.getLoans(O, UF); - - // Get the set of all loans that have expired at this program point. - ExpiredLoanMap AllExpiredLoans = ExpiredLoans.getExpiredLoans(UF); - - // If the pointer holds no loans or no loans have expired, there's nothing - // to check. - if (HeldLoans.isEmpty() || AllExpiredLoans.isEmpty()) - return; - - // Identify loans that which have expired but are held by the pointer. Using - // them is a use-after-free. - llvm::SmallVector<LoanID> DefaultedLoans; - // A definite UaF error occurs if all loans the origin might hold have - // expired. - bool IsDefiniteError = true; - for (LoanID L : HeldLoans) { - if (AllExpiredLoans.contains(L)) - DefaultedLoans.push_back(L); - else - // If at least one loan is not expired, this use is not a definite UaF. - IsDefiniteError = false; - } - // If there are no defaulted loans, the use is safe. - if (DefaultedLoans.empty()) - return; - - // Determine the confidence level of the error (definite or maybe). - Confidence CurrentConfidence = - IsDefiniteError ? Confidence::Definite : Confidence::Maybe; - - // For each expired loan, create a pending warning. - for (LoanID DefaultedLoan : DefaultedLoans) { - // If we already have a warning for this loan with a higher or equal - // confidence, skip this one. - if (FinalWarningsMap.count(DefaultedLoan) && - CurrentConfidence <= FinalWarningsMap[DefaultedLoan].ConfidenceLevel) + /// This method examines all live origins at the expiry point and determines + /// if any of them hold the expiring loan. If so, it creates a pending + /// warning with the appropriate confidence level based on the liveness + /// information. The confidence reflects whether the origin is definitely + /// or maybe live at this point. + /// + /// Note: This implementation considers only the confidence of origin + /// liveness. Future enhancements could also consider the confidence of loan + /// propagation (e.g., a loan may only be held on some execution paths). + void checkExpiry(const ExpireFact *EF) { + LoanID ExpiredLoan = EF->getLoanID(); + LivenessMap Origins = LiveOrigins.getLiveOrigins(EF); + Confidence CurConfidence = Confidence::None; + const UseFact *BadUse = nullptr; + for (auto &[OID, Info] : Origins) { + LoanSet HeldLoans = LoanPropagation.getLoans(OID, EF); + if (!HeldLoans.contains(ExpiredLoan)) continue; - - auto *EF = AllExpiredLoans.lookup(DefaultedLoan); - assert(EF && "Could not find ExpireFact for an expired loan."); - - FinalWarningsMap[DefaultedLoan] = {/*ExpiryLoc=*/(*EF)->getExpiryLoc(), - /*UseExpr=*/UF->getUseExpr(), - /*ConfidenceLevel=*/CurrentConfidence}; + // Loan is defaulted. + if (CurConfidence < Info.ConfidenceLevel) { + CurConfidence = Info.ConfidenceLevel; + BadUse = Info.CausingUseFact; + } } + if (!BadUse) + return; + // We have a use-after-free. + Confidence LastConf = FinalWarningsMap.lookup(ExpiredLoan).ConfidenceLevel; + if (LastConf >= CurConfidence) + return; + FinalWarningsMap[ExpiredLoan] = {/*ExpiryLoc=*/EF->getExpiryLoc(), + /*UseExpr=*/BadUse->getUseExpr(), + /*ConfidenceLevel=*/CurConfidence}; } void issuePendingWarnings() { @@ -1334,6 +1391,15 @@ class LifetimeChecker { // LifetimeSafetyAnalysis Class Implementation // ========================================================================= // +/// An object to hold the factories for immutable collections, ensuring +/// that all created states share the same underlying memory management. +struct LifetimeFactory { + llvm::BumpPtrAllocator Allocator; + OriginLoanMap::Factory OriginMapFactory{Allocator, /*canonicalize=*/false}; + LoanSet::Factory LoanSetFactory{Allocator, /*canonicalize=*/false}; + LivenessMap::Factory LivenessMapFactory{Allocator, /*canonicalize=*/false}; +}; + // We need this here for unique_ptr with forward declared class. LifetimeSafetyAnalysis::~LifetimeSafetyAnalysis() = default; @@ -1364,15 +1430,16 @@ void LifetimeSafetyAnalysis::run() { /// the analysis. /// 3. Collapse ExpireFacts belonging to same source location into a single /// Fact. - LoanPropagation = - std::make_unique<LoanPropagationAnalysis>(Cfg, AC, *FactMgr, *Factory); + LoanPropagation = std::make_unique<LoanPropagationAnalysis>( + Cfg, AC, *FactMgr, Factory->OriginMapFactory, Factory->LoanSetFactory); LoanPropagation->run(); - ExpiredLoans = - std::make_unique<ExpiredLoansAnalysis>(Cfg, AC, *FactMgr, *Factory); - ExpiredLoans->run(); + LiveOrigins = std::make_unique<LiveOriginAnalysis>( + Cfg, AC, *FactMgr, Factory->LivenessMapFactory); + LiveOrigins->run(); + DEBUG_WITH_TYPE("LiveOrigins", LiveOrigins->dump(llvm::dbgs(), *this)); - LifetimeChecker Checker(*LoanPropagation, *ExpiredLoans, *FactMgr, AC, + LifetimeChecker Checker(*LoanPropagation, *LiveOrigins, *FactMgr, AC, Reporter); Checker.run(); } @@ -1383,15 +1450,6 @@ LoanSet LifetimeSafetyAnalysis::getLoansAtPoint(OriginID OID, return LoanPropagation->getLoans(OID, PP); } -std::vector<LoanID> -LifetimeSafetyAnalysis::getExpiredLoansAtPoint(ProgramPoint PP) const { - assert(ExpiredLoans && "ExpiredLoansAnalysis has not been run."); - std::vector<LoanID> Result; - for (const auto &pair : ExpiredLoans->getExpiredLoans(PP)) - Result.push_back(pair.first); - return Result; -} - std::optional<OriginID> LifetimeSafetyAnalysis::getOriginIDForDecl(const ValueDecl *D) const { assert(FactMgr && "FactManager not initialized"); @@ -1411,6 +1469,15 @@ LifetimeSafetyAnalysis::getLoanIDForVar(const VarDecl *VD) const { return Result; } +std::vector<std::pair<OriginID, Confidence>> +LifetimeSafetyAnalysis::getLiveOriginsAtPoint(ProgramPoint PP) const { + assert(LiveOrigins && "LiveOriginAnalysis has not been run."); + std::vector<std::pair<OriginID, Confidence>> Result; + for (auto &[OID, Info] : LiveOrigins->getLiveOrigins(PP)) + Result.push_back({OID, Info.ConfidenceLevel}); + return Result; +} + llvm::StringMap<ProgramPoint> LifetimeSafetyAnalysis::getTestPoints() const { assert(FactMgr && "FactManager not initialized"); llvm::StringMap<ProgramPoint> AnnotationToPointMap; diff --git a/clang/test/Analysis/LifetimeSafety/benchmark.py b/clang/test/Analysis/LifetimeSafety/benchmark.py index d2e5f0b2122a3..cd5b30818a4a8 100644 --- a/clang/test/Analysis/LifetimeSafety/benchmark.py +++ b/clang/test/Analysis/LifetimeSafety/benchmark.py @@ -145,6 +145,60 @@ def generate_cpp_nested_loop_test(n: int) -> str: return cpp_code +def generate_cpp_switch_fan_out_test(n: int) -> str: + """ + Generates C++ code with a switch statement with N branches. + Each branch 'i' 'uses' (reads) a single, unique pointer 'pi'. + This pattern creates a "fan-in" join point for the backward + liveness analysis, stressing the LivenessMap::join operation + by forcing it to merge N disjoint, single-element sets of live origins. + The resulting complexity for LiveOrigins should be O(n log n) or higher. + + Example (n=3): + struct MyObj { int id; ~MyObj() {} }; + + void switch_fan_out_3(int condition) { + MyObj v1{1}; MyObj v2{1}; MyObj v3{1}; + MyObj* p1 = &v1; MyObj* p2 = &v2; MyObj* p3 = &v3; + + switch (condition % 3) { + case 0: + p1->id = 1; + break; + case 1: + p2->id = 1; + break; + case 2: + p3->id = 1; + break; + } + } + """ + if n <= 0: + return "// Number of variables must be positive." + + cpp_code = "struct MyObj { int id; ~MyObj() {} };\n\n" + cpp_code += f"void switch_fan_out{n}(int condition) {{\n" + # Generate N distinct objects + for i in range(1, n + 1): + cpp_code += f" MyObj v{i}{{1}};\n" + cpp_code += "\n" + # Generate N distinct pointers, each as a separate variable + for i in range(1, n + 1): + cpp_code += f" MyObj* p{i} = &v{i};\n" + cpp_code += "\n" + + cpp_code += f" switch (condition % {n}) {{\n" + for case_num in range(n): + cpp_code += f" case {case_num}:\n" + cpp_code += f" p{case_num + 1}->id = 1;\n" + cpp_code += " break;\n" + + cpp_code += " }\n}\n" + cpp_code += f"\nint main() {{ switch_fan_out{n}(0); return 0; }}\n" + return cpp_code + + def analyze_trace_file(trace_path: str) -> dict: """ Parses the -ftime-trace JSON output to find durations for the lifetime @@ -156,14 +210,14 @@ def analyze_trace_file(trace_path: str) -> dict: "total_us": 0.0, "fact_gen_us": 0.0, "loan_prop_us": 0.0, - "expired_loans_us": 0.0, + "live_origins_us": 0.0, } event_name_map = { "LifetimeSafetyAnalysis": "lifetime_us", "ExecuteCompiler": "total_us", "FactGenerator": "fact_gen_us", "LoanPropagation": "loan_prop_us", - "ExpiredLoans": "expired_loans_us", + "LiveOrigins": "live_origins_us", } try: with open(trace_path, "r") as f: @@ -227,7 +281,7 @@ def generate_markdown_report(results: dict) -> str: # Table header report.append( - "| N (Input Size) | Total Time | Analysis Time (%) | Fact Generator (%) | Loan Propagation (%) | Expired Loans (%) |" + "| N (Input Size) | Total Time | Analysis Time (%) | Fact Generator (%) | Loan Propagation (%) | Live Origins (%) |" ) report.append( "|:---------------|-----------:|------------------:|-------------------:|---------------------:|------------------:|" @@ -247,7 +301,7 @@ def generate_markdown_report(results: dict) -> str: f"{data['lifetime_ms'][i] / total_t * 100:>17.2f}% |", f"{data['fact_gen_ms'][i] / total_t * 100:>18.2f}% |", f"{data['loan_prop_ms'][i] / total_t * 100:>20.2f}% |", - f"{data['expired_loans_ms'][i] / total_t * 100:>17.2f}% |", + f"{data['live_origins_ms'][i] / total_t * 100:>17.2f}% |", ] report.append(" ".join(row)) @@ -259,7 +313,7 @@ def generate_markdown_report(results: dict) -> str: "Total Analysis": data["lifetime_ms"], "FactGenerator": data["fact_gen_ms"], "LoanPropagation": data["loan_prop_ms"], - "ExpiredLoans": data["expired_loans_ms"], + "LiveOrigins": data["live_origins_ms"], } for phase_name, y_data in analysis_phases.items(): @@ -302,7 +356,13 @@ def run_single_test( source_file, ] - result = subprocess.run(clang_command, capture_output=True, text=True, timeout=60) + try: + result = subprocess.run( + clang_command, capture_output=True, text=True, timeout=60 + ) + except subprocess.TimeoutExpired: + print(f"Compilation timed out for N={n}!", file=sys.stderr) + return {} if result.returncode != 0: print(f"Compilation failed for N={n}!", file=sys.stderr) @@ -354,6 +414,12 @@ def run_single_test( "generator_func": generate_cpp_nested_loop_test, "n_values": [50, 100, 150, 200], }, + { + "name": "switch_fan_out", + "title": "Switch Fan-out", + "generator_func": generate_cpp_switch_fan_out_test, + "n_values": [500, 1000, 2000, 4000], + }, ] results = {} @@ -368,7 +434,7 @@ def run_single_test( "total_ms": [], "fact_gen_ms": [], "loan_prop_ms": [], - "expired_loans_ms": [], + "live_origins_ms": [], } for n in config["n_values"]: durations_ms = run_single_test( @@ -387,7 +453,7 @@ def run_single_test( f" Total Analysis: {human_readable_time(durations_ms['lifetime_ms'])} | " f"FactGen: {human_readable_time(durations_ms['fact_gen_ms'])} | " f"LoanProp: {human_readable_time(durations_ms['loan_prop_ms'])} | " - f"ExpiredLoans: {human_readable_time(durations_ms['expired_loans_ms'])}" + f"LiveOrigins: {human_readable_time(durations_ms['live_origins_ms'])}" ) print("\n\n" + "=" * 80) diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp index f6bec6e66c365..4f234f0ac6e2d 100644 --- a/clang/test/Sema/warn-lifetime-safety.cpp +++ b/clang/test/Sema/warn-lifetime-safety.cpp @@ -126,11 +126,15 @@ void definite_single_pointer_multiple_loans_gsl(bool cond) { v.use(); // expected-note 2 {{later used here}} } - -//===----------------------------------------------------------------------===// -// Potential (Maybe) Use-After-Free (-W...strict) -// These are cases where the pointer *may* become dangling, depending on the path taken. -//===----------------------------------------------------------------------===// +void definite_if_branch(bool cond) { + MyObj safe; + MyObj* p = &safe; + if (cond) { + MyObj temp; + p = &temp; // expected-warning {{object whose reference is captured does not live long enough}} + } // expected-note {{destroyed here}} + (void)*p; // expected-note {{later used here}} +} void potential_if_branch(bool cond) { MyObj safe; @@ -139,15 +143,18 @@ void potential_if_branch(bool cond) { MyObj temp; p = &temp; // expected-warning {{object whose reference is captured may not live long enough}} } // expected-note {{destroyed here}} - (void)*p; // expected-note {{later used here}} + if (!cond) + (void)*p; // expected-note {{later used here}} + else + p = &safe; } -void potential_if_branch_gsl(bool cond) { +void definite_if_branch_gsl(bool cond) { MyObj safe; View v = safe; if (cond) { MyObj temp; - v = temp; // expected-warning {{object whose reference is captured may not live long enough}} + v = temp; // expected-warning {{object whose reference is captured does not live long enough}} } // expected-note {{destroyed here}} v.use(); // expected-note {{later used here}} } @@ -159,13 +166,14 @@ void definite_potential_together(bool cond) { { MyObj s; - p_definite = &s; // expected-warning {{does not live long enough}} - if (cond) { - p_maybe = &s; // expected-warning {{may not live long enough}} - } - } // expected-note 2 {{destroyed here}} - (void)*p_definite; // expected-note {{later used here}} - (void)*p_maybe; // expected-note {{later used here}} + if (cond) + p_definite = &s; // expected-warning {{does not live long enough}} + if (cond) + p_maybe = &s; // expected-warning {{may not live long enough}} + } // expected-note 2 {{destroyed here}} + (void)*p_definite; // expected-note {{later used here}} + if (!cond) + (void)*p_maybe; // expected-note {{later used here}} } void definite_overrides_potential(bool cond) { @@ -189,10 +197,19 @@ void definite_overrides_potential(bool cond) { (void)*q; } - -//===----------------------------------------------------------------------===// -// Control Flow Tests -//===----------------------------------------------------------------------===// +void potential_due_to_conditional_killing(bool cond) { + MyObj safe; + MyObj* q; + { + MyObj s; + q = &s; // expected-warning {{may not live long enough}} + } // expected-note {{destroyed here}} + if (cond) { + // 'q' is conditionally "rescued". 'p' is not. + q = &safe; + } + (void)*q; // expected-note {{later used here}} +} void potential_for_loop_use_after_loop_body(MyObj safe) { MyObj* p = &safe; @@ -215,34 +232,35 @@ void potential_for_loop_gsl() { void potential_for_loop_use_before_loop_body(MyObj safe) { MyObj* p = &safe; + // Prefer the earlier use for diagnsotics. for (int i = 0; i < 1; ++i) { (void)*p; // expected-note {{later used here}} MyObj s; - p = &s; // expected-warning {{may not live long enough}} + p = &s; // expected-warning {{does not live long enough}} } // expected-note {{destroyed here}} (void)*p; } -void potential_loop_with_break(bool cond) { +void definite_loop_with_break(bool cond) { MyObj safe; MyObj* p = &safe; for (int i = 0; i < 10; ++i) { if (cond) { MyObj temp; - p = &temp; // expected-warning {{may not live long enough}} + p = &temp; // expected-warning {{does not live long enough}} break; // expected-note {{destroyed here}} } } (void)*p; // expected-note {{later used here}} } -void potential_loop_with_break_gsl(bool cond) { +void definite_loop_with_break_gsl(bool cond) { MyObj safe; View v = safe; for (int i = 0; i < 10; ++i) { if (cond) { MyObj temp; - v = temp; // expected-warning {{object whose reference is captured may not live long enough}} + v = temp; // expected-warning {{object whose reference is captured does not live long enough}} break; // expected-note {{destroyed here}} } } @@ -250,37 +268,52 @@ void potential_loop_with_break_gsl(bool cond) { } void potential_multiple_expiry_of_same_loan(bool cond) { - // Choose the last expiry location for the loan. + // Choose the last expiry location for the loan (e.g., through scope-ends and break statements). MyObj safe; MyObj* p = &safe; for (int i = 0; i < 10; ++i) { MyObj unsafe; if (cond) { - p = &unsafe; // expected-warning {{may not live long enough}} - break; + p = &unsafe; // expected-warning {{does not live long enough}} + break; // expected-note {{destroyed here}} } - } // expected-note {{destroyed here}} + } (void)*p; // expected-note {{later used here}} p = &safe; for (int i = 0; i < 10; ++i) { MyObj unsafe; if (cond) { - p = &unsafe; // expected-warning {{may not live long enough}} + p = &unsafe; // expected-warning {{does not live long enough}} if (cond) - break; + break; // expected-note {{destroyed here}} } - } // expected-note {{destroyed here}} + } (void)*p; // expected-note {{later used here}} p = &safe; for (int i = 0; i < 10; ++i) { if (cond) { MyObj unsafe2; - p = &unsafe2; // expected-warning {{may not live long enough}} + p = &unsafe2; // expected-warning {{does not live long enough}} break; // expected-note {{destroyed here}} } } + + // TODO: This can be argued to be a "maybe" warning. This is because + // we only check for confidence of liveness and not the confidence of + // the loan contained in an origin. To deal with this, we can introduce + // a confidence in loan propagation analysis as well like liveness. + (void)*p; // expected-note {{later used here}} + + p = &safe; + for (int i = 0; i < 10; ++i) { + MyObj unsafe; + if (cond) + p = &unsafe; // expected-warning {{does not live long enough}} + if (cond) + break; // expected-note {{destroyed here}} + } (void)*p; // expected-note {{later used here}} } @@ -298,13 +331,14 @@ void potential_switch(int mode) { break; } } - (void)*p; // expected-note {{later used here}} + if (mode == 2) + (void)*p; // expected-note {{later used here}} } void definite_switch(int mode) { MyObj safe; MyObj* p = &safe; - // All cases are UaF --> Definite error. + // A use domintates all the loan expires --> all definite error. switch (mode) { case 1: { MyObj temp1; @@ -347,6 +381,21 @@ void definite_switch_gsl(int mode) { v.use(); // expected-note 3 {{later used here}} } +void loan_from_previous_iteration(MyObj safe, bool condition) { + MyObj* p = &safe; + MyObj* q = &safe; + + while (condition) { + MyObj x; + p = &x; // expected-warning {{may not live long enough}} + + if (condition) + q = p; + (void)*p; + (void)*q; // expected-note {{later used here}} + } // expected-note {{destroyed here}} +} + //===----------------------------------------------------------------------===// // No-Error Cases //===----------------------------------------------------------------------===// @@ -372,6 +421,19 @@ void no_error_if_dangle_then_rescue_gsl() { v.use(); // This is safe. } +void no_error_loan_from_current_iteration(bool cond) { + // See https://github.com/llvm/llvm-project/issues/156959. + MyObj b; + while (cond) { + MyObj a; + View p = b; + if (cond) { + p = a; + } + (void)p; + } +} + //===----------------------------------------------------------------------===// // Lifetimebound Attribute Tests @@ -415,9 +477,9 @@ void lifetimebound_multiple_args_potential(bool cond) { MyObj obj1; if (cond) { MyObj obj2; - v = Choose(true, - obj1, // expected-warning {{object whose reference is captured may not live long enough}} - obj2); // expected-warning {{object whose reference is captured may not live long enough}} + v = Choose(true, + obj1, // expected-warning {{object whose reference is captured does not live long enough}} + obj2); // expected-warning {{object whose reference is captured does not live long enough}} } // expected-note {{destroyed here}} } // expected-note {{destroyed here}} v.use(); // expected-note 2 {{later used here}} @@ -488,7 +550,7 @@ void lifetimebound_partial_safety(bool cond) { MyObj temp_obj; v = Choose(true, safe_obj, - temp_obj); // expected-warning {{object whose reference is captured may not live long enough}} + temp_obj); // expected-warning {{object whose reference is captured does not live long enough}} } // expected-note {{destroyed here}} v.use(); // expected-note {{later used here}} } diff --git a/clang/unittests/Analysis/LifetimeSafetyTest.cpp b/clang/unittests/Analysis/LifetimeSafetyTest.cpp index 3821015f07fb1..41783f7c99352 100644 --- a/clang/unittests/Analysis/LifetimeSafetyTest.cpp +++ b/clang/unittests/Analysis/LifetimeSafetyTest.cpp @@ -126,12 +126,12 @@ class LifetimeTestHelper { return Analysis.getLoansAtPoint(OID, PP); } - std::optional<std::vector<LoanID>> - getExpiredLoansAtPoint(llvm::StringRef Annotation) { + std::optional<std::vector<std::pair<OriginID, Confidence>>> + getLiveOriginsAtPoint(llvm::StringRef Annotation) { ProgramPoint PP = Runner.getProgramPoint(Annotation); if (!PP) return std::nullopt; - return Analysis.getExpiredLoansAtPoint(PP); + return Analysis.getLiveOriginsAtPoint(PP); } private: @@ -180,6 +180,15 @@ class OriginInfo { LifetimeTestHelper &Helper; }; +// A helper class to represent a set of origins, identified by variable names. +class OriginsInfo { +public: + OriginsInfo(const std::vector<std::string> &Vars, LifetimeTestHelper &H) + : OriginVars(Vars), Helper(H) {} + std::vector<std::string> OriginVars; + LifetimeTestHelper &Helper; +}; + /// Matcher to verify the set of loans held by an origin at a specific /// program point. /// @@ -221,14 +230,15 @@ MATCHER_P2(HasLoansToImpl, LoanVars, Annotation, "") { std::sort(ExpectedLoans.begin(), ExpectedLoans.end()); std::sort(ActualLoans.begin(), ActualLoans.end()); if (ExpectedLoans != ActualLoans) { - *result_listener << "Expected: "; + *result_listener << "Expected: {"; for (const auto &LoanID : ExpectedLoans) { *result_listener << LoanID.Value << ", "; } - *result_listener << "Actual: "; + *result_listener << "} Actual: {"; for (const auto &LoanID : ActualLoans) { *result_listener << LoanID.Value << ", "; } + *result_listener << "}"; return false; } @@ -236,32 +246,71 @@ MATCHER_P2(HasLoansToImpl, LoanVars, Annotation, "") { ActualLoans, result_listener); } -/// Matcher to verify that the complete set of expired loans at a program point -/// matches the expected loan set. -MATCHER_P(AreExpiredAt, Annotation, "") { - const LoanSetInfo &Info = arg; - auto &Helper = Info.Helper; +enum class ConfidenceFilter { Maybe, Definite, All }; - auto ActualExpiredSetOpt = Helper.getExpiredLoansAtPoint(Annotation); - if (!ActualExpiredSetOpt) { - *result_listener << "could not get a valid expired loan set at point '" +/// Matcher to verify the complete set of live origins at a program point. +MATCHER_P2(AreLiveAtImpl, Annotation, ConfFilter, "") { + const OriginsInfo &Info = arg; + auto &Helper = Info.Helper; + auto ActualLiveSetOpt = Helper.getLiveOriginsAtPoint(Annotation); + if (!ActualLiveSetOpt) { + *result_listener << "could not get a valid live origin set at point '" << Annotation << "'"; return false; } - std::vector<LoanID> ActualExpiredLoans = *ActualExpiredSetOpt; - std::vector<LoanID> ExpectedExpiredLoans; - for (const auto &VarName : Info.LoanVars) { - auto LoanIDs = Helper.getLoansForVar(VarName); - if (LoanIDs.empty()) { - *result_listener << "could not find a loan for variable '" << VarName + std::vector<OriginID> ActualLiveOrigins; + for (const auto [OID, ActualConfidence] : ActualLiveSetOpt.value()) { + if (ConfFilter == ConfidenceFilter::All) + ActualLiveOrigins.push_back(OID); + if (ActualConfidence == Confidence::Maybe && + ConfFilter == ConfidenceFilter::Maybe) + ActualLiveOrigins.push_back(OID); + if (ActualConfidence == Confidence::Definite && + ConfFilter == ConfidenceFilter::Definite) + ActualLiveOrigins.push_back(OID); + } + + std::vector<OriginID> ExpectedLiveOrigins; + for (const auto &VarName : Info.OriginVars) { + auto OriginIDOpt = Helper.getOriginForDecl(VarName); + if (!OriginIDOpt) { + *result_listener << "could not find an origin for variable '" << VarName << "'"; return false; } - ExpectedExpiredLoans.insert(ExpectedExpiredLoans.end(), LoanIDs.begin(), - LoanIDs.end()); + ExpectedLiveOrigins.push_back(*OriginIDOpt); } - return ExplainMatchResult(UnorderedElementsAreArray(ExpectedExpiredLoans), - ActualExpiredLoans, result_listener); + std::sort(ExpectedLiveOrigins.begin(), ExpectedLiveOrigins.end()); + std::sort(ActualLiveOrigins.begin(), ActualLiveOrigins.end()); + if (ExpectedLiveOrigins != ActualLiveOrigins) { + *result_listener << "Expected: {"; + for (const auto &OriginID : ExpectedLiveOrigins) { + *result_listener << OriginID.Value << ", "; + } + *result_listener << "} Actual: {"; + for (const auto &OriginID : ActualLiveOrigins) { + *result_listener << OriginID.Value << ", "; + } + *result_listener << "}"; + return false; + } + return true; +} + +MATCHER_P(AreDefinitelyLiveAt, Annotation, "") { + return ExplainMatchResult( + AreLiveAtImpl(Annotation, ConfidenceFilter::Definite), arg, + result_listener); +} + +MATCHER_P(AreMaybeLiveAt, Annotation, "") { + return ExplainMatchResult(AreLiveAtImpl(Annotation, ConfidenceFilter::Maybe), + arg, result_listener); +} + +MATCHER_P(AreLiveAt, Annotation, "") { + return ExplainMatchResult(AreLiveAtImpl(Annotation, ConfidenceFilter::All), + arg, result_listener); } // Base test fixture to manage the runner and helper. @@ -276,6 +325,13 @@ class LifetimeAnalysisTest : public ::testing::Test { return OriginInfo(OriginVar, *Helper); } + /// Factory function that hides the std::vector creation. + OriginsInfo Origins(std::initializer_list<std::string> OriginVars) { + return OriginsInfo({OriginVars}, *Helper); + } + + OriginsInfo NoOrigins() { return Origins({}); } + /// Factory function that hides the std::vector creation. LoanSetInfo LoansTo(std::initializer_list<std::string> LoanVars) { return LoanSetInfo({LoanVars}, *Helper); @@ -428,29 +484,6 @@ TEST_F(LifetimeAnalysisTest, AssignInSwitch) { EXPECT_THAT(Origin("p"), HasLoansTo({"s1", "s2", "s3"}, "after_switch")); } -TEST_F(LifetimeAnalysisTest, LoanInLoop) { - SetupTest(R"( - void target(bool condition) { - MyObj* p = nullptr; - while (condition) { - POINT(start_loop); - MyObj inner; - p = &inner; - POINT(end_loop); - } - POINT(after_loop); - } - )"); - EXPECT_THAT(Origin("p"), HasLoansTo({"inner"}, "start_loop")); - EXPECT_THAT(LoansTo({"inner"}), AreExpiredAt("start_loop")); - - EXPECT_THAT(Origin("p"), HasLoansTo({"inner"}, "end_loop")); - EXPECT_THAT(NoLoans(), AreExpiredAt("end_loop")); - - EXPECT_THAT(Origin("p"), HasLoansTo({"inner"}, "after_loop")); - EXPECT_THAT(LoansTo({"inner"}), AreExpiredAt("after_loop")); -} - TEST_F(LifetimeAnalysisTest, LoopWithBreak) { SetupTest(R"( void target(int count) { @@ -528,20 +561,16 @@ TEST_F(LifetimeAnalysisTest, PointersAndExpirationInACycle) { )"); EXPECT_THAT(Origin("p1"), HasLoansTo({"v1"}, "before_while")); EXPECT_THAT(Origin("p2"), HasLoansTo({"v2"}, "before_while")); - EXPECT_THAT(NoLoans(), AreExpiredAt("before_while")); EXPECT_THAT(Origin("p1"), HasLoansTo({"v1", "v2", "temp"}, "in_loop_before_temp")); EXPECT_THAT(Origin("p2"), HasLoansTo({"v2", "temp"}, "in_loop_before_temp")); - EXPECT_THAT(LoansTo({"temp"}), AreExpiredAt("in_loop_before_temp")); EXPECT_THAT(Origin("p1"), HasLoansTo({"temp"}, "in_loop_after_temp")); EXPECT_THAT(Origin("p2"), HasLoansTo({"v2", "temp"}, "in_loop_after_temp")); - EXPECT_THAT(NoLoans(), AreExpiredAt("in_loop_after_temp")); EXPECT_THAT(Origin("p1"), HasLoansTo({"v1", "v2", "temp"}, "after_loop")); EXPECT_THAT(Origin("p2"), HasLoansTo({"v2", "temp"}, "after_loop")); - EXPECT_THAT(LoansTo({"temp"}), AreExpiredAt("after_loop")); } TEST_F(LifetimeAnalysisTest, InfiniteLoopPrunesEdges) { @@ -585,178 +614,6 @@ TEST_F(LifetimeAnalysisTest, NestedScopes) { EXPECT_THAT(Origin("p"), HasLoansTo({"inner"}, "after_inner_scope")); } -TEST_F(LifetimeAnalysisTest, SimpleExpiry) { - SetupTest(R"( - void target() { - MyObj* p = nullptr; - { - MyObj s; - p = &s; - POINT(before_expiry); - } // s goes out of scope here - POINT(after_expiry); - } - )"); - EXPECT_THAT(NoLoans(), AreExpiredAt("before_expiry")); - EXPECT_THAT(LoansTo({"s"}), AreExpiredAt("after_expiry")); -} - -TEST_F(LifetimeAnalysisTest, NestedExpiry) { - SetupTest(R"( - void target() { - MyObj s1; - MyObj* p = &s1; - POINT(before_inner); - { - MyObj s2; - p = &s2; - POINT(in_inner); - } // s2 expires - POINT(after_inner); - } - )"); - EXPECT_THAT(NoLoans(), AreExpiredAt("before_inner")); - EXPECT_THAT(NoLoans(), AreExpiredAt("in_inner")); - EXPECT_THAT(LoansTo({"s2"}), AreExpiredAt("after_inner")); -} - -TEST_F(LifetimeAnalysisTest, ConditionalExpiry) { - SetupTest(R"( - void target(bool cond) { - MyObj s1; - MyObj* p = &s1; - POINT(before_if); - if (cond) { - MyObj s2; - p = &s2; - POINT(then_block); - } // s2 expires here - POINT(after_if); - } - )"); - EXPECT_THAT(NoLoans(), AreExpiredAt("before_if")); - EXPECT_THAT(NoLoans(), AreExpiredAt("then_block")); - EXPECT_THAT(LoansTo({"s2"}), AreExpiredAt("after_if")); -} - -TEST_F(LifetimeAnalysisTest, LoopExpiry) { - SetupTest(R"( - void target() { - MyObj *p = nullptr; - for (int i = 0; i < 2; ++i) { - POINT(start_loop); - MyObj s; - p = &s; - POINT(end_loop); - } // s expires here on each iteration - POINT(after_loop); - } - )"); - EXPECT_THAT(LoansTo({"s"}), AreExpiredAt("start_loop")); - EXPECT_THAT(NoLoans(), AreExpiredAt("end_loop")); - EXPECT_THAT(LoansTo({"s"}), AreExpiredAt("after_loop")); -} - -TEST_F(LifetimeAnalysisTest, MultipleExpiredLoans) { - SetupTest(R"( - void target() { - MyObj *p1, *p2, *p3; - { - MyObj s1; - p1 = &s1; - POINT(p1); - } // s1 expires - POINT(p2); - { - MyObj s2; - p2 = &s2; - MyObj s3; - p3 = &s3; - POINT(p3); - } // s2, s3 expire - POINT(p4); - } - )"); - EXPECT_THAT(NoLoans(), AreExpiredAt("p1")); - EXPECT_THAT(LoansTo({"s1"}), AreExpiredAt("p2")); - EXPECT_THAT(LoansTo({"s1"}), AreExpiredAt("p3")); - EXPECT_THAT(LoansTo({"s1", "s2", "s3"}), AreExpiredAt("p4")); -} - -TEST_F(LifetimeAnalysisTest, GotoJumpsOutOfScope) { - SetupTest(R"( - void target(bool cond) { - MyObj *p = nullptr; - { - MyObj s; - p = &s; - POINT(before_goto); - if (cond) { - goto end; - } - } // `s` expires here on the path that doesn't jump - POINT(after_scope); - end: - POINT(after_goto); - } - )"); - EXPECT_THAT(NoLoans(), AreExpiredAt("before_goto")); - EXPECT_THAT(LoansTo({"s"}), AreExpiredAt("after_scope")); - EXPECT_THAT(LoansTo({"s"}), AreExpiredAt("after_goto")); -} - -TEST_F(LifetimeAnalysisTest, ContinueInLoop) { - SetupTest(R"( - void target(int count) { - MyObj *p = nullptr; - MyObj outer; - p = &outer; - POINT(before_loop); - - for (int i = 0; i < count; ++i) { - if (i % 2 == 0) { - MyObj s_even; - p = &s_even; - POINT(in_even_iter); - continue; - } - MyObj s_odd; - p = &s_odd; - POINT(in_odd_iter); - } - POINT(after_loop); - } - )"); - EXPECT_THAT(NoLoans(), AreExpiredAt("before_loop")); - EXPECT_THAT(LoansTo({"s_odd"}), AreExpiredAt("in_even_iter")); - EXPECT_THAT(LoansTo({"s_even"}), AreExpiredAt("in_odd_iter")); - EXPECT_THAT(LoansTo({"s_even", "s_odd"}), AreExpiredAt("after_loop")); -} - -TEST_F(LifetimeAnalysisTest, ReassignedPointerThenOriginalExpires) { - SetupTest(R"( - void target() { - MyObj* p = nullptr; - { - MyObj s1; - p = &s1; - POINT(p_has_s1); - { - MyObj s2; - p = &s2; - POINT(p_has_s2); - } - POINT(p_after_s2_expires); - } // s1 expires here. - POINT(p_after_s1_expires); - } - )"); - EXPECT_THAT(NoLoans(), AreExpiredAt("p_has_s1")); - EXPECT_THAT(NoLoans(), AreExpiredAt("p_has_s2")); - EXPECT_THAT(LoansTo({"s2"}), AreExpiredAt("p_after_s2_expires")); - EXPECT_THAT(LoansTo({"s1", "s2"}), AreExpiredAt("p_after_s1_expires")); -} - TEST_F(LifetimeAnalysisTest, NoDuplicateLoansForImplicitCastToConst) { SetupTest(R"( void target() { @@ -880,23 +737,6 @@ TEST_F(LifetimeAnalysisTest, GslPointerPropagation) { EXPECT_THAT(Origin("z"), HasLoansTo({"a"}, "p3")); } -TEST_F(LifetimeAnalysisTest, GslPointerLoanExpiration) { - SetupTest(R"( - void target() { - View x; - { - MyObj a; - x = a; - POINT(before_expiry); - } // `a` is destroyed here. - POINT(after_expiry); - } - )"); - - EXPECT_THAT(NoLoans(), AreExpiredAt("before_expiry")); - EXPECT_THAT(LoansTo({"a"}), AreExpiredAt("after_expiry")); -} - TEST_F(LifetimeAnalysisTest, GslPointerReassignment) { SetupTest(R"( void target() { @@ -916,7 +756,6 @@ TEST_F(LifetimeAnalysisTest, GslPointerReassignment) { EXPECT_THAT(Origin("v"), HasLoansTo({"safe"}, "p1")); EXPECT_THAT(Origin("v"), HasLoansTo({"unsafe"}, "p2")); EXPECT_THAT(Origin("v"), HasLoansTo({"unsafe"}, "p3")); - EXPECT_THAT(LoansTo({"unsafe"}), AreExpiredAt("p3")); } TEST_F(LifetimeAnalysisTest, GslPointerConversionOperator) { @@ -1174,5 +1013,187 @@ TEST_F(LifetimeAnalysisTest, LifetimeboundConversionOperator) { )"); EXPECT_THAT(Origin("v"), HasLoansTo({"owner"}, "p1")); } + +TEST_F(LifetimeAnalysisTest, LivenessDeadPointer) { + SetupTest(R"( + void target() { + POINT(p2); + MyObj s; + MyObj* p = &s; + POINT(p1); + } + )"); + EXPECT_THAT(NoOrigins(), AreLiveAt("p1")); + EXPECT_THAT(NoOrigins(), AreLiveAt("p2")); +} + +TEST_F(LifetimeAnalysisTest, LivenessSimpleReturn) { + SetupTest(R"( + MyObj* target() { + MyObj s; + MyObj* p = &s; + POINT(p1); + return p; + } + )"); + EXPECT_THAT(Origins({"p"}), AreDefinitelyLiveAt("p1")); +} + +TEST_F(LifetimeAnalysisTest, LivenessKilledByReassignment) { + SetupTest(R"( + MyObj* target() { + MyObj s1, s2; + MyObj* p = &s1; + POINT(p1); + p = &s2; + POINT(p2); + return p; + } + )"); + EXPECT_THAT(Origins({"p"}), AreDefinitelyLiveAt("p2")); + EXPECT_THAT(NoOrigins(), AreLiveAt("p1")); +} + +TEST_F(LifetimeAnalysisTest, LivenessAcrossBranches) { + SetupTest(R"( + MyObj* target(bool c) { + MyObj x, y; + MyObj* p = nullptr; + POINT(p1); + if (c) { + p = &x; + POINT(p2); + } else { + p = &y; + POINT(p3); + } + return p; + } + )"); + EXPECT_THAT(Origins({"p"}), AreDefinitelyLiveAt("p2")); + EXPECT_THAT(Origins({"p"}), AreDefinitelyLiveAt("p3")); + // Before the `if`, the value of `p` (`nullptr`) is always overwritten before. + EXPECT_THAT(NoOrigins(), AreLiveAt("p1")); +} + +TEST_F(LifetimeAnalysisTest, LivenessInLoop) { + SetupTest(R"( + MyObj* target(bool c) { + MyObj s1, s2; + MyObj* p = &s1; + MyObj* q = &s2; + POINT(p1); + while(c) { + POINT(p2); + + p = q; + POINT(p3); + } + POINT(p4); + return p; + } + )"); + + EXPECT_THAT(Origins({"p"}), AreDefinitelyLiveAt("p4")); + EXPECT_THAT(NoOrigins(), AreMaybeLiveAt("p4")); + + EXPECT_THAT(Origins({"p", "q"}), AreMaybeLiveAt("p3")); + + EXPECT_THAT(Origins({"q"}), AreDefinitelyLiveAt("p2")); + EXPECT_THAT(NoOrigins(), AreMaybeLiveAt("p2")); + + EXPECT_THAT(Origins({"p", "q"}), AreMaybeLiveAt("p1")); +} + +TEST_F(LifetimeAnalysisTest, LivenessInLoopAndIf) { + // See https://github.com/llvm/llvm-project/issues/156959. + SetupTest(R"( + void target(bool cond) { + MyObj b; + while (cond) { + POINT(p1); + + MyObj a; + View p = b; + + POINT(p2); + + if (cond) { + POINT(p3); + p = a; + } + POINT(p4); + (void)p; + POINT(p5); + } + } + )"); + EXPECT_THAT(NoOrigins(), AreLiveAt("p5")); + EXPECT_THAT(Origins({"p"}), AreDefinitelyLiveAt("p4")); + EXPECT_THAT(NoOrigins(), AreLiveAt("p3")); + EXPECT_THAT(Origins({"p"}), AreMaybeLiveAt("p2")); + EXPECT_THAT(NoOrigins(), AreLiveAt("p1")); +} + +TEST_F(LifetimeAnalysisTest, LivenessInLoopAndIf2) { + SetupTest(R"( + void target(MyObj safe, bool condition) { + MyObj* p = &safe; + MyObj* q = &safe; + POINT(p6); + + while (condition) { + POINT(p5); + MyObj x; + p = &x; + + POINT(p4); + + if (condition) { + q = p; + POINT(p3); + } + + POINT(p2); + (void)*p; + (void)*q; + POINT(p1); + } + } + )"); + EXPECT_THAT(Origins({"q"}), AreMaybeLiveAt("p1")); + EXPECT_THAT(NoOrigins(), AreDefinitelyLiveAt("p1")); + + EXPECT_THAT(Origins({"p", "q"}), AreDefinitelyLiveAt("p2")); + + EXPECT_THAT(Origins({"p", "q"}), AreDefinitelyLiveAt("p3")); + + EXPECT_THAT(Origins({"p"}), AreDefinitelyLiveAt("p4")); + EXPECT_THAT(Origins({"q"}), AreMaybeLiveAt("p4")); + + EXPECT_THAT(Origins({"q"}), AreMaybeLiveAt("p5")); + EXPECT_THAT(NoOrigins(), AreDefinitelyLiveAt("p5")); + + EXPECT_THAT(Origins({"q"}), AreMaybeLiveAt("p6")); + EXPECT_THAT(NoOrigins(), AreDefinitelyLiveAt("p6")); +} + +TEST_F(LifetimeAnalysisTest, LivenessOutsideLoop) { + SetupTest(R"( + void target(MyObj safe) { + MyObj* p = &safe; + for (int i = 0; i < 1; ++i) { + MyObj s; + p = &s; + POINT(p2); + } + POINT(p1); + (void)*p; + } + )"); + EXPECT_THAT(Origins({"p"}), AreDefinitelyLiveAt("p1")); + EXPECT_THAT(Origins({"p"}), AreMaybeLiveAt("p2")); +} + } // anonymous namespace } // namespace clang::lifetimes::internal _______________________________________________ llvm-branch-commits mailing list llvm-branch-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-branch-commits