//===- LifetimeAnnotations.cpp - -*--------------- C++------------------*-===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// #include "clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.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/Type.h" #include "clang/AST/TypeLoc.h" #include "clang/Basic/OperatorKinds.h" #include "llvm/ADT/StringSet.h" namespace clang::lifetimes { const FunctionDecl * getDeclWithMergedLifetimeBoundAttrs(const FunctionDecl *FD) { return FD != nullptr ? FD->getMostRecentDecl() : nullptr; } const CXXMethodDecl * getDeclWithMergedLifetimeBoundAttrs(const CXXMethodDecl *CMD) { const FunctionDecl *FD = CMD; return cast_if_present( getDeclWithMergedLifetimeBoundAttrs(FD)); } bool isNormalAssignmentOperator(const FunctionDecl *FD) { OverloadedOperatorKind OO = FD->getDeclName().getCXXOverloadedOperator(); bool IsAssignment = OO == OO_Equal || isCompoundAssignmentOperator(OO); if (!IsAssignment) return false; QualType RetT = FD->getReturnType(); if (!RetT->isLValueReferenceType()) return false; ASTContext &Ctx = FD->getASTContext(); QualType LHST; auto *MD = dyn_cast(FD); if (MD && MD->isCXXInstanceMember()) LHST = Ctx.getLValueReferenceType(MD->getFunctionObjectParameterType()); else LHST = FD->getParamDecl(0)->getType(); return Ctx.hasSameType(RetT, LHST); } bool isAssignmentOperatorLifetimeBound(const CXXMethodDecl *CMD) { CMD = getDeclWithMergedLifetimeBoundAttrs(CMD); return CMD && isNormalAssignmentOperator(CMD) && CMD->param_size() == 1 && CMD->getParamDecl(0)->hasAttr(); } /// Check if a function has a lifetimebound attribute on its function type /// (which represents the implicit 'this' parameter for methods). /// Returns the attribute if found, nullptr otherwise. static const LifetimeBoundAttr * getLifetimeBoundAttrFromFunctionType(const TypeSourceInfo &TSI) { // Walk through the type layers looking for a lifetimebound attribute. TypeLoc TL = TSI.getTypeLoc(); while (true) { auto ATL = TL.getAsAdjusted(); if (!ATL) break; if (auto *LBAttr = ATL.getAttrAs()) return LBAttr; TL = ATL.getModifiedLoc(); } return nullptr; } const LifetimeBoundAttr * getDirectImplicitObjectLifetimeBoundAttr(const FunctionDecl *FD) { if (const TypeSourceInfo *TSI = FD->getTypeSourceInfo()) if (const auto *Attr = getLifetimeBoundAttrFromFunctionType(*TSI)) return Attr; return nullptr; } const LifetimeBoundAttr * getImplicitObjectParamLifetimeBoundAttr(const FunctionDecl *FD) { FD = getDeclWithMergedLifetimeBoundAttrs(FD); // Attribute merging doesn't work well with attributes on function types (like // 'this' param). We need to check all redeclarations. auto CheckRedecls = [](const FunctionDecl *F) -> const LifetimeBoundAttr * { for (const FunctionDecl *Redecl : F->redecls()) if (const auto *Attr = getDirectImplicitObjectLifetimeBoundAttr(Redecl)) return Attr; return nullptr; }; if (const auto *Attr = CheckRedecls(FD)) return Attr; if (const FunctionDecl *Pattern = FD->getTemplateInstantiationPattern()) return CheckRedecls(Pattern); return nullptr; } bool implicitObjectParamIsLifetimeBound(const FunctionDecl *FD) { if (getImplicitObjectParamLifetimeBoundAttr(FD)) return true; return isNormalAssignmentOperator(FD); } bool isInStlNamespace(const Decl *D) { for (const DeclContext *DC = D->getDeclContext(); DC; DC = DC->getParent()) { if (DC->isStdNamespace()) return true; if (const auto *ND = dyn_cast(DC)) if (const IdentifierInfo *II = ND->getIdentifier()) { StringRef Name = II->getName(); if (Name.size() >= 2 && Name.front() == '_' && (Name[1] == '_' || isUppercase(Name[1]))) return true; } } return false; } bool isPointerLikeType(QualType QT) { return isGslPointerType(QT) || QT->isPointerType() || QT->isNullPtrType(); } static bool isReferenceOrPointerLikeType(QualType QT) { return QT->isReferenceType() || isPointerLikeType(QT); } bool shouldTrackImplicitObjectArg(const Expr &ImplicitObjectArgument, const CXXMethodDecl *Callee, bool RunningUnderLifetimeSafety) { if (!Callee) return false; // Check both the declaring class and the call-site object: a gsl::Owner // may inherit its accessors from a non-Owner base (e.g. libc++ optional). const bool IsGslOwnerImplicitObject = isGslOwnerType(Callee->getFunctionObjectParameterType()) || (RunningUnderLifetimeSafety && isGslOwnerType(ImplicitObjectArgument.getBestDynamicClassType())); if (auto *Conv = dyn_cast(Callee)) if (isGslPointerType(Conv->getConversionType()) && IsGslOwnerImplicitObject) return true; if (!isGslPointerType(Callee->getFunctionObjectParameterType()) && !IsGslOwnerImplicitObject) return false; // Begin and end iterators. static const llvm::StringSet<> IteratorMembers = { "begin", "end", "rbegin", "rend", "cbegin", "cend", "crbegin", "crend"}; static const llvm::StringSet<> InnerPointerGetters = { // Inner pointer getters. "c_str", "data", "get"}; static const llvm::StringSet<> ContainerFindFns = { // Map and set types. "find", "equal_range", "lower_bound", "upper_bound"}; // Track dereference operator and transparent functions like begin(), get(), // etc. for all GSL pointers. Only do so for lifetime safety analysis and not // for Sema's statement-local analysis as it starts to have false-positives. if (RunningUnderLifetimeSafety && isGslPointerType(Callee->getFunctionObjectParameterType()) && isReferenceOrPointerLikeType(Callee->getReturnType())) { // Propagate origins through GSL pointer arithmetic and dereference // operators. switch (Callee->getOverloadedOperator()) { case OO_Arrow: case OO_Star: case OO_Plus: case OO_Minus: case OO_PlusPlus: case OO_MinusMinus: return true; default: break; } if (Callee->getIdentifier() && (IteratorMembers.contains(Callee->getName()) || InnerPointerGetters.contains(Callee->getName()))) return true; } if (!isInStlNamespace(Callee->getParent())) return false; if (isPointerLikeType(Callee->getReturnType())) { if (!Callee->getIdentifier()) // e.g., std::optional::operator->() returns T*. return RunningUnderLifetimeSafety ? IsGslOwnerImplicitObject && Callee->getOverloadedOperator() == OverloadedOperatorKind::OO_Arrow : false; return IteratorMembers.contains(Callee->getName()) || InnerPointerGetters.contains(Callee->getName()) || ContainerFindFns.contains(Callee->getName()); } if (Callee->getReturnType()->isReferenceType()) { if (!Callee->getIdentifier()) { auto OO = Callee->getOverloadedOperator(); if (!IsGslOwnerImplicitObject) return false; return OO == OverloadedOperatorKind::OO_Subscript || OO == OverloadedOperatorKind::OO_Star; } return llvm::StringSwitch(Callee->getName()) .Cases({"front", "back", "at", "top", "value"}, true) .Default(false); } return false; } bool shouldTrackFirstArgument(const FunctionDecl *FD) { if (!FD->getIdentifier() || FD->getNumParams() < 1) return false; if (!FD->isInStdNamespace()) return false; // Track std:: algorithm functions that return an iterator whose lifetime is // bound to the first argument. if (FD->getNumParams() >= 2 && FD->isInStdNamespace() && isGslPointerType(FD->getReturnType())) { if (llvm::StringSwitch(FD->getName()) .Cases( { "find", "find_if", "find_if_not", "find_first_of", "adjacent_find", "search", "find_end", "lower_bound", "upper_bound", "partition_point", }, true) .Default(false)) return true; } const auto *RD = FD->getParamDecl(0)->getType()->getPointeeCXXRecordDecl(); if (!RD || !RD->isInStdNamespace()) return false; if (!RD->hasAttr() && !RD->hasAttr()) return false; if (FD->getNumParams() != 1) return false; if (FD->getReturnType()->isPointerType() || isGslPointerType(FD->getReturnType())) { return llvm::StringSwitch(FD->getName()) .Cases({"begin", "rbegin", "cbegin", "crbegin"}, true) .Cases({"end", "rend", "cend", "crend"}, true) .Case("data", true) .Default(false); } if (FD->getReturnType()->isReferenceType()) { return llvm::StringSwitch(FD->getName()) .Cases({"get", "any_cast"}, true) .Default(false); } return false; } bool shouldTrackSecondArgument(const FunctionDecl *FD) { if (FD->getNumParams() < 2) return false; const auto *RD = FD->getParamDecl(1)->getType()->getAsCXXRecordDecl(); if (!RD) return false; // For free-standing `+`/`-` operators annotated with `gsl::Pointer`, track // the second parameter when its type matches the return type. return RD->hasAttr() && (FD->getOverloadedOperator() == OO_Plus || FD->getOverloadedOperator() == OO_Minus) && ASTContext::hasSameUnqualifiedType(FD->getParamDecl(1)->getType(), FD->getReturnType()) && !isa(FD); } template static bool isRecordWithAttr(const CXXRecordDecl *RD) { if (!RD) return false; // Generally, if a primary template class declaration is annotated with an // attribute, all its specializations generated from template instantiations // should inherit the attribute. // // However, since lifetime analysis occurs during parsing, we may encounter // cases where a full definition of the specialization is not required. In // such cases, the specialization declaration remains incomplete and lacks the // attribute. Therefore, we fall back to checking the primary template class. // // Note: it is possible for a specialization declaration to have an attribute // even if the primary template does not. // // FIXME: What if the primary template and explicit specialization // declarations have conflicting attributes? We should consider diagnosing // this scenario. bool Result = RD->hasAttr(); if (auto *CTSD = dyn_cast(RD)) Result |= CTSD->getSpecializedTemplate()->getTemplatedDecl()->hasAttr(); return Result; } template static bool isRecordWithAttr(QualType Type) { return isRecordWithAttr(Type->getAsCXXRecordDecl()); } bool isGslPointerType(QualType QT) { return isRecordWithAttr(QT); } bool isGslOwnerType(QualType QT) { return isRecordWithAttr(QT); } bool isGslOwnerType(const CXXRecordDecl *RD) { return isRecordWithAttr(RD); } static StringRef getName(const CXXRecordDecl &RD) { if (const auto *CTSD = dyn_cast(&RD)) return CTSD->getSpecializedTemplate()->getName(); if (RD.getIdentifier()) return RD.getName(); return ""; } static StringRef getName(const FunctionDecl &FD) { if (FD.getIdentifier()) return FD.getName(); return ""; } static bool isStdUniquePtr(const CXXRecordDecl &RD) { return RD.isInStdNamespace() && getName(RD) == "unique_ptr"; } bool isUniquePtrRelease(const CXXMethodDecl &MD) { return MD.getIdentifier() && MD.getName() == "release" && MD.getNumParams() == 0 && isStdUniquePtr(*MD.getParent()); } bool isInvalidationMethod(const CXXMethodDecl &MD) { const CXXRecordDecl *RD = MD.getParent(); if (!isInStlNamespace(RD)) return false; // `pop_back` is excluded: it only invalidates references to the removed // element, not to other elements. static const llvm::StringSet<> Vector = {// Insertion "insert", "emplace", "emplace_back", "push_back", "insert_range", "append_range", // Removal "erase", "clear", // Memory management "reserve", "resize", "shrink_to_fit", // Assignment "assign", "assign_range"}; // `pop_*` methods are excluded: they only invalidate references to the // removed element, not to other elements. static const llvm::StringSet<> Deque = {// Insertion "insert", "emplace", "insert_range", // Removal "erase", "clear", // Memory management "resize", "shrink_to_fit", // Assignment "assign", "assign_range"}; static const llvm::StringSet<> String = { // Insertion "insert", "push_back", "append", "replace", "replace_with_range", "insert_range", "append_range", // Removal "pop_back", "erase", "clear", // Memory management "reserve", "resize", "resize_and_overwrite", "shrink_to_fit", // Assignment "swap", "assign", "assign_range"}; // FIXME: Add queue and stack and check for underlying container // (e.g. no invalidation for std::list). static const llvm::StringSet<> PriorityQueue = {// Insertion "push", "emplace", "push_range", // Removal "pop"}; // `erase` and `extract` are excluded: they only affect the removed element, // not to other elements. static const llvm::StringSet<> NodeBased = {// Removal "clear"}; // For `flat_*` container adaptors, `try_emplace` and `insert_or_assign` // only exist on `flat_map`. Listing them here is harmless since the methods // won't be found on other types. static const llvm::StringSet<> Flat = {// Insertion "insert", "emplace", "emplace_hint", "try_emplace", "insert_or_assign", "insert_range", "merge", // Removal "extract", "erase", "clear", // Assignment "replace"}; static const llvm::StringSet<> UniquePtr = {// Reallocation "reset"}; const StringRef RecordName = getName(*RD); // TODO: Consider caching this lookup by CXXMethodDecl pointer if this // StringSwitch becomes a performance bottleneck. const llvm::StringSet<> *InvalidatingMethods = llvm::StringSwitch *>(RecordName) .Case("vector", &Vector) .Case("basic_string", &String) .Case("deque", &Deque) .Case("priority_queue", &PriorityQueue) .Cases({"set", "multiset", "map", "multimap", "unordered_set", "unordered_multiset", "unordered_map", "unordered_multimap"}, &NodeBased) .Cases({"flat_map", "flat_set", "flat_multimap", "flat_multiset"}, &Flat) .Case("unique_ptr", &UniquePtr) .Default(nullptr); if (!InvalidatingMethods) return false; // Handle Operators via OverloadedOperatorKind OverloadedOperatorKind OO = MD.getOverloadedOperator(); if (OO != OO_None) { switch (OO) { case OO_Equal: // operator= : Always invalidates (Assignment) case OO_PlusEqual: // operator+= : Append (String/Vector) return true; case OO_Subscript: // operator[] : Invalidation only for // `flat_map` (Insert-or-access). // `map` and `unordered_map` are excluded. return RecordName == "flat_map"; default: return false; } } if (!MD.getIdentifier()) return false; return InvalidatingMethods->contains(MD.getName()); } bool destructsFirstArg(const FunctionDecl &FD) { if (isa(FD)) return true; return isInStlNamespace(&FD) && getName(FD) == "destroy_at"; } bool isStdCallableWrapperType(const CXXRecordDecl *RD) { if (!RD || !isInStlNamespace(RD)) return false; StringRef Name = getName(*RD); return Name == "function" || Name == "move_only_function"; } bool isStdReferenceCast(const FunctionDecl *FD) { if (!FD) return false; switch (FD->getBuiltinID()) { case Builtin::BImove: case Builtin::BImove_if_noexcept: case Builtin::BIforward: case Builtin::BIforward_like: case Builtin::BIas_const: return true; default: return false; } } } // namespace clang::lifetimes