//===----------------------------------------------------------------------===// // // 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 "UseAfterMoveCheck.h" #include "clang/AST/Attr.h" #include "clang/AST/Expr.h" #include "clang/AST/ExprCXX.h" #include "clang/ASTMatchers/ASTMatchers.h" #include "clang/Analysis/Analyses/CFGReachabilityAnalysis.h" #include "clang/Analysis/CFG.h" #include "clang/Lex/Lexer.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/SmallPtrSet.h" #include "../utils/ExprSequence.h" #include "../utils/Matchers.h" #include "../utils/OptionsUtils.h" #include using namespace clang::ast_matchers; using namespace clang::tidy::utils; namespace clang::tidy::bugprone { using matchers::hasUnevaluatedContext; namespace { AST_MATCHER_P(Expr, hasParentIgnoringParenImpCasts, ast_matchers::internal::Matcher, InnerMatcher) { const Expr *E = &Node; do { const DynTypedNodeList Parents = Finder->getASTContext().getParents(*E); if (Parents.size() != 1) return false; E = Parents[0].get(); if (!E) return false; } while (isa(E)); return InnerMatcher.matches(*E, Finder, Builder); } /// Contains information about a use-after-move. struct UseAfterMove { // The DeclRefExpr that constituted the use of the object. const DeclRefExpr *DeclRef; // Is the order in which the move and the use are evaluated undefined? bool EvaluationOrderUndefined = false; // Does the use happen in a later loop iteration than the move? // // We default to false and change it to true if required in find(). bool UseHappensInLaterLoopIteration = false; }; /// Finds uses of a variable after a move (and maintains state required by the /// various internal helper functions). class UseAfterMoveFinder { public: UseAfterMoveFinder(ASTContext *TheContext, llvm::ArrayRef InvalidationFunctions, llvm::ArrayRef ReinitializationFunctions, const CXXRecordDecl *MovedAs); // Within the given code block, finds the first use of 'MovedVariable' that // occurs after 'MovingCall' (the expression that performs the move). If a // use-after-move is found, writes information about it to 'TheUseAfterMove'. // Returns whether a use-after-move was found. std::optional find(Stmt *CodeBlock, const Expr *MovingCall, const DeclRefExpr *MovedVariable); private: std::optional findInternal(const CFGBlock *Block, const Expr *MovingCall, const ValueDecl *MovedVariable); void getUsesAndReinits(const CFGBlock *Block, const ValueDecl *MovedVariable, SmallVectorImpl *Uses, llvm::SmallPtrSetImpl *Reinits); void getDeclRefs(const CFGBlock *Block, const Decl *MovedVariable, llvm::SmallPtrSetImpl *DeclRefs); void getReinits(const CFGBlock *Block, const ValueDecl *MovedVariable, llvm::SmallPtrSetImpl *Stmts, llvm::SmallPtrSetImpl *DeclRefs); ASTContext *Context; llvm::ArrayRef InvalidationFunctions; llvm::ArrayRef ReinitializationFunctions; const CXXRecordDecl *MovedAs; std::unique_ptr Sequence; std::unique_ptr BlockMap; llvm::SmallPtrSet Visited; }; } // namespace static auto getNameMatcher(llvm::ArrayRef InvalidationFunctions) { return anyOf(hasAnyName("::std::move", "::std::forward"), matchers::matchesAnyListedRegexName(InvalidationFunctions)); } static StatementMatcher makeReinitMatcher(const ValueDecl *MovedVariable, llvm::ArrayRef InvalidationFunctions, llvm::ArrayRef ReinitializationFunctions) { const auto DeclRefMatcher = declRefExpr(hasDeclaration(equalsNode(MovedVariable))).bind("declref"); const auto StandardContainerTypeMatcher = hasType(hasUnqualifiedDesugaredType( recordType(hasDeclaration(cxxRecordDecl(hasAnyName( "::std::basic_string", "::std::vector", "::std::deque", "::std::forward_list", "::std::list", "::std::set", "::std::map", "::std::multiset", "::std::multimap", "::std::unordered_set", "::std::unordered_map", "::std::unordered_multiset", "::std::unordered_multimap")))))); const auto StandardResettableOwnerTypeMatcher = hasType( hasUnqualifiedDesugaredType(recordType(hasDeclaration(cxxRecordDecl( hasAnyName("::std::unique_ptr", "::std::shared_ptr", "::std::weak_ptr", "::std::optional", "::std::any")))))); // Matches different types of reinitialization. return stmt( anyOf( // Assignment. In addition to the overloaded assignment // operator, test for built-in assignment as well, since // template functions may be instantiated to use std::move() on // built-in types. binaryOperation(hasOperatorName("="), hasLHS(ignoringParenImpCasts(DeclRefMatcher))), // std::tie() assignment: std::tie(a, b) = expr reinitializes // all variables passed to std::tie because the tuple // assignment writes back through the stored references. binaryOperation( hasOperatorName("="), hasLHS(ignoringImplicit(ignoringParenImpCasts( callExpr(callee(functionDecl(hasName("::std::tie"))), hasAnyArgument(ignoringParenImpCasts( DeclRefMatcher))))))), // Declaration. We treat this as a type of reinitialization // too, so we don't need to treat it separately. declStmt(hasDescendant(equalsNode(MovedVariable))), // clear() and assign() on standard containers. cxxMemberCallExpr( on(expr(DeclRefMatcher, StandardContainerTypeMatcher)), // To keep the matcher simple, we check for assign() calls // on all standard containers, even though only vector, // deque, forward_list and list have assign(). If assign() // is called on any of the other containers, this will be // flagged by a compile error anyway. callee(cxxMethodDecl(hasAnyName("clear", "assign")))), // reset() on standard smart pointers. cxxMemberCallExpr(on(expr(DeclRefMatcher, StandardResettableOwnerTypeMatcher)), callee(cxxMethodDecl(hasName("reset")))), // Methods that have the [[clang::reinitializes]] attribute. cxxMemberCallExpr( on(DeclRefMatcher), callee(cxxMethodDecl(hasAttr(attr::Reinitializes)))), // Functions that are specified in ReinitializationFunctions // option. callExpr( callee(functionDecl(matchers::matchesAnyListedRegexName( ReinitializationFunctions))), anyOf(cxxMemberCallExpr(on(DeclRefMatcher)), callExpr(unless(cxxMemberCallExpr()), hasArgument(0, DeclRefMatcher)))), // Passing variable to a function as a non-const pointer. callExpr(forEachArgumentWithParam( unaryOperator(hasOperatorName("&"), hasUnaryOperand(DeclRefMatcher)), unless( parmVarDecl(hasType(pointsTo(isConstQualified())))))), // Passing variable to a function as a non-const lvalue // reference (unless that function is std::move()). callExpr(forEachArgumentWithParam( traverse(TK_AsIs, DeclRefMatcher), unless(parmVarDecl(hasType( references(qualType(isConstQualified())))))), unless(callee(functionDecl( getNameMatcher(InvalidationFunctions))))))) .bind("reinit"); } // Matches nodes that are // - Part of a decltype argument or class template argument (we check this by // seeing if they are children of a TypeLoc), or // - Part of a function template argument (we check this by seeing if they are // children of a DeclRefExpr that references a function template). // DeclRefExprs that fulfill these conditions should not be counted as a use or // move. static StatementMatcher inDecltypeOrTemplateArg() { return anyOf(hasAncestor(typeLoc()), hasAncestor(declRefExpr( to(functionDecl(ast_matchers::isTemplateInstantiation())))), hasAncestor(expr(hasUnevaluatedContext()))); } UseAfterMoveFinder::UseAfterMoveFinder( ASTContext *TheContext, llvm::ArrayRef InvalidationFunctions, llvm::ArrayRef ReinitializationFunctions, const CXXRecordDecl *MovedAs) : Context(TheContext), InvalidationFunctions(InvalidationFunctions), ReinitializationFunctions(ReinitializationFunctions), MovedAs(MovedAs) {} std::optional UseAfterMoveFinder::find(Stmt *CodeBlock, const Expr *MovingCall, const DeclRefExpr *MovedVariable) { // Generate the CFG manually instead of through an AnalysisDeclContext because // it seems the latter can't be used to generate a CFG for the body of a // lambda. // // We include implicit and temporary destructors in the CFG so that // destructors marked [[noreturn]] are handled correctly in the control flow // analysis. (These are used in some styles of assertion macros.) CFG::BuildOptions Options; Options.AddImplicitDtors = true; Options.AddTemporaryDtors = true; std::unique_ptr TheCFG = CFG::buildCFG(nullptr, CodeBlock, Context, Options); if (!TheCFG) return std::nullopt; Sequence = std::make_unique(TheCFG.get(), CodeBlock, Context); BlockMap = std::make_unique(TheCFG.get(), Context); Visited.clear(); const CFGBlock *MoveBlock = BlockMap->blockContainingStmt(MovingCall); if (!MoveBlock) { // This can happen if MovingCall is in a constructor initializer, which is // not included in the CFG because the CFG is built only from the function // body. MoveBlock = &TheCFG->getEntry(); } auto TheUseAfterMove = findInternal(MoveBlock, MovingCall, MovedVariable->getDecl()); if (TheUseAfterMove) { if (const CFGBlock *UseBlock = BlockMap->blockContainingStmt(TheUseAfterMove->DeclRef)) { // Does the use happen in a later loop iteration than the move? // - If they are in the same CFG block, we know the use happened in a // later iteration if we visited that block a second time. // - Otherwise, we know the use happened in a later iteration if the // move is reachable from the use. CFGReverseBlockReachabilityAnalysis CFA(*TheCFG); TheUseAfterMove->UseHappensInLaterLoopIteration = UseBlock == MoveBlock ? Visited.contains(UseBlock) : CFA.isReachable(UseBlock, MoveBlock); } } return TheUseAfterMove; } std::optional UseAfterMoveFinder::findInternal(const CFGBlock *Block, const Expr *MovingCall, const ValueDecl *MovedVariable) { if (Visited.contains(Block)) return std::nullopt; // Mark the block as visited (except if this is the block containing the // std::move() and it's being visited the first time). if (!MovingCall) Visited.insert(Block); // Get all uses and reinits in the block. SmallVector Uses; llvm::SmallPtrSet Reinits; getUsesAndReinits(Block, MovedVariable, &Uses, &Reinits); // Ignore all reinitializations where the move potentially comes after the // reinit. // If `Reinit` is identical to `MovingCall`, we're looking at a move-to-self // (e.g. `a = std::move(a)`). Count these as reinitializations. SmallVector ReinitsToDelete; for (const Stmt *Reinit : Reinits) if (MovingCall && Reinit != MovingCall && Sequence->potentiallyAfter(MovingCall, Reinit)) ReinitsToDelete.push_back(Reinit); for (const Stmt *Reinit : ReinitsToDelete) Reinits.erase(Reinit); // Find all uses that potentially come after the move. for (const DeclRefExpr *Use : Uses) { if (!MovingCall || Sequence->potentiallyAfter(Use, MovingCall)) { // Does the use have a saving reinit? A reinit is saving if it definitely // comes before the use, i.e. if there's no potential that the reinit is // after the use. bool HaveSavingReinit = false; for (const Stmt *Reinit : Reinits) if (!Sequence->potentiallyAfter(Reinit, Use)) HaveSavingReinit = true; if (!HaveSavingReinit) { UseAfterMove TheUseAfterMove; TheUseAfterMove.DeclRef = Use; // Is this a use-after-move that depends on order of evaluation? // This is the case if the move potentially comes after the use (and we // already know that use potentially comes after the move, which taken // together tells us that the ordering is unclear). TheUseAfterMove.EvaluationOrderUndefined = MovingCall != nullptr && Sequence->potentiallyAfter(MovingCall, Use); return TheUseAfterMove; } } } // If the object wasn't reinitialized, call ourselves recursively on all // successors. if (Reinits.empty()) { for (const auto &Succ : Block->succs()) { if (Succ) { if (auto Found = findInternal(Succ, nullptr, MovedVariable)) return Found; } } } return std::nullopt; } void UseAfterMoveFinder::getUsesAndReinits( const CFGBlock *Block, const ValueDecl *MovedVariable, SmallVectorImpl *Uses, llvm::SmallPtrSetImpl *Reinits) { llvm::SmallPtrSet DeclRefs; llvm::SmallPtrSet ReinitDeclRefs; getDeclRefs(Block, MovedVariable, &DeclRefs); getReinits(Block, MovedVariable, Reinits, &ReinitDeclRefs); // All references to the variable that aren't reinitializations are uses. Uses->clear(); for (const DeclRefExpr *DeclRef : DeclRefs) if (!ReinitDeclRefs.contains(DeclRef)) Uses->push_back(DeclRef); // Sort the uses by their occurrence in the source code. llvm::sort(*Uses, [](const DeclRefExpr *D1, const DeclRefExpr *D2) { return D1->getExprLoc() < D2->getExprLoc(); }); } static std::optional getStringLiteral(const Expr *E) { assert(E); if (const auto *SL = dyn_cast(E->IgnoreParenImpCasts())) return SL->getString(); return std::nullopt; } // User defined types can use [[clang::annotate]] to mark smart-pointer-like // types with a specified move from state that matches the standard smart // pointer's moved-from state (nullptr). static bool isNullAfterMoveAnnotate(const AnnotateAttr *Attr) { if (Attr->getAnnotation() != "clang-tidy") return false; if (Attr->args_size() != 2) return false; std::optional Plugin = getStringLiteral(Attr->args_begin()[0]); std::optional Annotation = getStringLiteral(Attr->args_begin()[1]); return Plugin && Annotation && *Plugin == "bugprone-use-after-move" && *Annotation == "null_after_move"; } static bool isSpecifiedAfterMove(const ValueDecl *VD) { const Type *TheType = VD->getType().getNonReferenceType().getTypePtrOrNull(); if (!TheType) return false; const CXXRecordDecl *RecordDecl = TheType->getAsCXXRecordDecl(); if (!RecordDecl) return false; // Use the definition for the declaration, as it is the expected place to add // the annotations. if (const CXXRecordDecl *DefinitionDecl = RecordDecl->getDefinition()) { for (const auto *Attr : DefinitionDecl->specific_attrs()) if (isNullAfterMoveAnnotate(Attr)) return true; } // Standard smart pointers have a well-specified moved-from state (nullptr). const IdentifierInfo *ID = RecordDecl->getIdentifier(); if (!ID) return false; const StringRef Name = ID->getName(); if (Name != "unique_ptr" && Name != "shared_ptr" && Name != "weak_ptr") return false; return RecordDecl->getDeclContext()->isStdNamespace(); } void UseAfterMoveFinder::getDeclRefs( const CFGBlock *Block, const Decl *MovedVariable, llvm::SmallPtrSetImpl *DeclRefs) { DeclRefs->clear(); for (const auto &Elem : *Block) { std::optional S = Elem.getAs(); if (!S) continue; auto AddDeclRefs = [this, Block, DeclRefs](const ArrayRef Matches) { for (const auto &Match : Matches) { const auto *DeclRef = Match.getNodeAs("declref"); const auto *Member = Match.getNodeAs("member-expr"); const auto *Operator = Match.getNodeAs("operator"); // Non-moved member as the move only implies a base class. if (Member && MovedAs && !isa(Member->getMemberDecl()) && !MovedAs->hasMemberName(Member->getMemberDecl()->getIdentifier())) { continue; } if (DeclRef && BlockMap->blockContainingStmt(DeclRef) == Block) { // Ignore uses of a standard smart pointer or classes annotated as // "null_after_move" (smart-pointer-like behavior) that don't // dereference the pointer. if (Operator || !isSpecifiedAfterMove(DeclRef->getDecl())) DeclRefs->insert(DeclRef); } } }; auto DeclRefMatcher = declRefExpr(hasDeclaration(equalsNode(MovedVariable)), unless(inDecltypeOrTemplateArg()), unless(hasParentIgnoringParenImpCasts( memberExpr(hasDeclaration(cxxDestructorDecl())))), optionally(hasParentIgnoringParenImpCasts( memberExpr().bind("member-expr")))) .bind("declref"); AddDeclRefs(match(traverse(TK_AsIs, findAll(DeclRefMatcher)), *S->getStmt(), *Context)); AddDeclRefs(match(findAll(cxxOperatorCallExpr( hasAnyOverloadedOperatorName("*", "->", "[]"), hasArgument(0, DeclRefMatcher)) .bind("operator")), *S->getStmt(), *Context)); } } void UseAfterMoveFinder::getReinits( const CFGBlock *Block, const ValueDecl *MovedVariable, llvm::SmallPtrSetImpl *Stmts, llvm::SmallPtrSetImpl *DeclRefs) { const auto ReinitMatcher = makeReinitMatcher( MovedVariable, InvalidationFunctions, ReinitializationFunctions); Stmts->clear(); DeclRefs->clear(); for (const auto &Elem : *Block) { std::optional S = Elem.getAs(); if (!S) continue; const SmallVector Matches = match(findAll(ReinitMatcher), *S->getStmt(), *Context); for (const auto &Match : Matches) { const auto *TheStmt = Match.getNodeAs("reinit"); const auto *TheDeclRef = Match.getNodeAs("declref"); if (TheStmt && BlockMap->blockContainingStmt(TheStmt) == Block) { Stmts->insert(TheStmt); // We count DeclStmts as reinitializations, but they don't have a // DeclRefExpr associated with them -- so we need to check 'TheDeclRef' // before adding it to the set. if (TheDeclRef) DeclRefs->insert(TheDeclRef); } } } } namespace { enum MoveType { Forward = 0, // std::forward Move = 1, // std::move Invalidation = 2, // other }; } // namespace static MoveType determineMoveType(const FunctionDecl *FuncDecl) { if (FuncDecl->isInStdNamespace()) { if (FuncDecl->getName() == "move") return MoveType::Move; if (FuncDecl->getName() == "forward") return MoveType::Forward; } return MoveType::Invalidation; } static void emitDiagnostic(const Expr *MovingCall, const DeclRefExpr *MoveArg, const UseAfterMove &Use, ClangTidyCheck *Check, ASTContext *Context, MoveType Type, const FunctionDecl *MoveDecl) { const SourceLocation UseLoc = Use.DeclRef->getExprLoc(); const SourceLocation MoveLoc = MovingCall->getExprLoc(); Check->diag( UseLoc, "'%0' used after it was %select{forwarded|moved|invalidated by %2}1") << MoveArg->getDecl()->getName() << Type << MoveDecl; Check->diag(MoveLoc, "%select{forward|move|invalidation}0 occurred here", DiagnosticIDs::Note) << Type; if (Use.EvaluationOrderUndefined) { Check->diag( UseLoc, "the use and %select{forward|move|invalidation}0 are unsequenced, i.e. " "there is no guarantee about the order in which they are evaluated", DiagnosticIDs::Note) << Type; } else if (Use.UseHappensInLaterLoopIteration) { Check->diag(UseLoc, "the use happens in a later loop iteration than the " "%select{forward|move|invalidation}0", DiagnosticIDs::Note) << Type; } } UseAfterMoveCheck::UseAfterMoveCheck(StringRef Name, ClangTidyContext *Context) : ClangTidyCheck(Name, Context), InvalidationFunctions(utils::options::parseStringList( Options.get("InvalidationFunctions", ""))), ReinitializationFunctions(utils::options::parseStringList( Options.get("ReinitializationFunctions", ""))) {} void UseAfterMoveCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) { Options.store(Opts, "InvalidationFunctions", utils::options::serializeStringList(InvalidationFunctions)); Options.store(Opts, "ReinitializationFunctions", utils::options::serializeStringList(ReinitializationFunctions)); } void UseAfterMoveCheck::registerMatchers(MatchFinder *Finder) { // try_emplace is a common maybe-moving function that returns a // bool to tell callers whether it moved. Ignore std::move inside // try_emplace to avoid false positives as we don't track uses of // the bool. auto TryEmplaceMatcher = cxxMemberCallExpr(callee(cxxMethodDecl(hasName("try_emplace")))); auto Arg = declRefExpr().bind("arg"); auto IsMemberCallee = callee(functionDecl(unless(isStaticStorageClass()))); auto CallMoveMatcher = callExpr( callee(functionDecl(getNameMatcher(InvalidationFunctions)) .bind("move-decl")), anyOf(cxxMemberCallExpr(IsMemberCallee, on(Arg)), callExpr(unless(cxxMemberCallExpr(IsMemberCallee)), hasArgument(0, Arg))), unless(inDecltypeOrTemplateArg()), unless(hasParent(TryEmplaceMatcher)), expr().bind("call-move"), optionally(hasParent(implicitCastExpr(hasCastKind(CK_DerivedToBase)) .bind("optional-cast"))), anyOf(hasAncestor(compoundStmt( hasParent(lambdaExpr().bind("containing-lambda")))), hasAncestor(functionDecl( anyOf(cxxConstructorDecl( hasAnyConstructorInitializer(withInitializer( expr(anyOf(equalsBoundNode("call-move"), hasDescendant(expr( equalsBoundNode("call-move"))))) .bind("containing-ctor-init")))) .bind("containing-ctor"), functionDecl().bind("containing-func")))))); Finder->addMatcher( traverse( TK_AsIs, // To find the Stmt that we assume performs the actual move, we look // for the direct ancestor of the std::move() that isn't one of the // node types ignored by ignoringParenImpCasts(). stmt( forEach(expr(ignoringParenImpCasts(CallMoveMatcher))), // Don't allow an InitListExpr to be the moving call. An // InitListExpr has both a syntactic and a semantic form, and the // parent-child relationships are different between the two. This // could cause an InitListExpr to be analyzed as the moving call // in addition to the Expr that we actually want, resulting in two // diagnostics with different code locations for the same move. unless(initListExpr()), unless(expr(ignoringParenImpCasts(equalsBoundNode("call-move"))))) .bind("moving-call")), this); } void UseAfterMoveCheck::check(const MatchFinder::MatchResult &Result) { const auto *ContainingCtor = Result.Nodes.getNodeAs("containing-ctor"); const auto *ContainingCtorInit = Result.Nodes.getNodeAs("containing-ctor-init"); const auto *ContainingLambda = Result.Nodes.getNodeAs("containing-lambda"); const auto *ContainingFunc = Result.Nodes.getNodeAs("containing-func"); const auto *CallMove = Result.Nodes.getNodeAs("call-move"); const auto *MovingCall = Result.Nodes.getNodeAs("moving-call"); const auto *Arg = Result.Nodes.getNodeAs("arg"); const auto *MoveDecl = Result.Nodes.getNodeAs("move-decl"); const auto *ParentCast = Result.Nodes.getNodeAs("optional-cast"); if (!MovingCall || !MovingCall->getExprLoc().isValid()) MovingCall = CallMove; // Ignore the std::move if the variable that was passed to it isn't a local // variable. if (!Arg->getDecl()->getDeclContext()->isFunctionOrMethod()) return; // Collect all code blocks that could use the arg after move. SmallVector CodeBlocks{}; if (ContainingCtor) { CodeBlocks.push_back(ContainingCtor->getBody()); if (ContainingCtorInit) { // Collect the constructor initializer expressions. bool BeforeMove{true}; for (const CXXCtorInitializer *Init : ContainingCtor->inits()) { if (BeforeMove && Init->getInit()->IgnoreImplicit() == ContainingCtorInit->IgnoreImplicit()) BeforeMove = false; if (!BeforeMove) CodeBlocks.push_back(Init->getInit()); } } } else if (ContainingLambda) { CodeBlocks.push_back(ContainingLambda->getBody()); } else if (ContainingFunc) { CodeBlocks.push_back(ContainingFunc->getBody()); } const CXXRecordDecl *MovedAs = ParentCast ? ParentCast->getType()->getAsCXXRecordDecl() : nullptr; for (Stmt *CodeBlock : CodeBlocks) { UseAfterMoveFinder Finder(Result.Context, InvalidationFunctions, ReinitializationFunctions, MovedAs); if (auto Use = Finder.find(CodeBlock, MovingCall, Arg)) emitDiagnostic(MovingCall, Arg, *Use, this, Result.Context, determineMoveType(MoveDecl), MoveDecl); } } } // namespace clang::tidy::bugprone