https://github.com/Harald-R created https://github.com/llvm/llvm-project/pull/210727
As proposed in [this thread](https://discourse.llvm.org/t/clang-tidy-checks-for-common-mlir-anti-patterns/91179), a clang-tidy check could be introduced which detects cases where an operation is erased, but it still has uses after. The check targets the following calls for erasing an operation: - `mlir::Operation::erase` - `mlir::Operation::destroy` - `mlir::RewriterBase::eraseOp` - `mlir::RewriterBase::replaceOp` - through derived operations of `mlir::OpState` that are wrappers around `mlir::Operation` (e.g. `mlir::func::FuncOp` and all others that inherit from `mlir::Op`) Cases where an operation's declaration variable is reinitialized are also taken into account. Additionally, cases where an operation is erased in a loop's iteration are covered as well. The implementation of this check is based on the existing [bugprone-use-after-move](https://clang.llvm.org/extra/clang-tidy/checks/bugprone/use-after-move.html). >From 4c4a2c2e0a926114d5dccdcf91375e7c3d1926d7 Mon Sep 17 00:00:00 2001 From: Harald-R <[email protected]> Date: Mon, 13 Jul 2026 14:17:39 +0300 Subject: [PATCH] Add llvm-mlir-use-after-erase check --- .../clang-tidy/llvm/CMakeLists.txt | 1 + .../clang-tidy/llvm/LLVMTidyModule.cpp | 3 + .../llvm/MlirUseAfterEraseCheck.cpp | 365 ++++++++++++++ .../clang-tidy/llvm/MlirUseAfterEraseCheck.h | 37 ++ clang-tools-extra/docs/ReleaseNotes.rst | 9 + .../docs/clang-tidy/checks/list.rst | 1 + .../checks/llvm/mlir-use-after-erase.rst | 39 ++ .../checkers/llvm/mlir-use-after-erase.cpp | 476 ++++++++++++++++++ 8 files changed, 931 insertions(+) create mode 100644 clang-tools-extra/clang-tidy/llvm/MlirUseAfterEraseCheck.cpp create mode 100644 clang-tools-extra/clang-tidy/llvm/MlirUseAfterEraseCheck.h create mode 100644 clang-tools-extra/docs/clang-tidy/checks/llvm/mlir-use-after-erase.rst create mode 100644 clang-tools-extra/test/clang-tidy/checkers/llvm/mlir-use-after-erase.cpp diff --git a/clang-tools-extra/clang-tidy/llvm/CMakeLists.txt b/clang-tools-extra/clang-tidy/llvm/CMakeLists.txt index bec3ba50c81c5..847695c853cf5 100644 --- a/clang-tools-extra/clang-tidy/llvm/CMakeLists.txt +++ b/clang-tools-extra/clang-tidy/llvm/CMakeLists.txt @@ -8,6 +8,7 @@ add_clang_library(clangTidyLLVMModule STATIC HeaderGuardCheck.cpp IncludeOrderCheck.cpp LLVMTidyModule.cpp + MlirUseAfterEraseCheck.cpp PreferIsaOrDynCastInConditionalsCheck.cpp PreferRegisterOverUnsignedCheck.cpp PreferStaticOverAnonymousNamespaceCheck.cpp diff --git a/clang-tools-extra/clang-tidy/llvm/LLVMTidyModule.cpp b/clang-tools-extra/clang-tidy/llvm/LLVMTidyModule.cpp index 918af88c979e0..1f42cce26a0b8 100644 --- a/clang-tools-extra/clang-tidy/llvm/LLVMTidyModule.cpp +++ b/clang-tools-extra/clang-tidy/llvm/LLVMTidyModule.cpp @@ -14,6 +14,7 @@ #include "FormatvStringCheck.h" #include "HeaderGuardCheck.h" #include "IncludeOrderCheck.h" +#include "MlirUseAfterEraseCheck.h" #include "PreferIsaOrDynCastInConditionalsCheck.h" #include "PreferRegisterOverUnsignedCheck.h" #include "PreferStaticOverAnonymousNamespaceCheck.h" @@ -36,6 +37,8 @@ class LLVMModule : public ClangTidyModule { CheckFactories.registerCheck<FormatvStringCheck>("llvm-formatv-string"); CheckFactories.registerCheck<LLVMHeaderGuardCheck>("llvm-header-guard"); CheckFactories.registerCheck<IncludeOrderCheck>("llvm-include-order"); + CheckFactories.registerCheck<MlirUseAfterEraseCheck>( + "llvm-mlir-use-after-erase"); CheckFactories.registerCheck<readability::NamespaceCommentCheck>( "llvm-namespace-comment"); CheckFactories.registerCheck<PreferIsaOrDynCastInConditionalsCheck>( diff --git a/clang-tools-extra/clang-tidy/llvm/MlirUseAfterEraseCheck.cpp b/clang-tools-extra/clang-tidy/llvm/MlirUseAfterEraseCheck.cpp new file mode 100644 index 0000000000000..6e5b6707cbd11 --- /dev/null +++ b/clang-tools-extra/clang-tidy/llvm/MlirUseAfterEraseCheck.cpp @@ -0,0 +1,365 @@ +//===----------------------------------------------------------------------===// +// +// 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 "MlirUseAfterEraseCheck.h" +#include "../utils/ExprSequence.h" +#include "clang/AST/Expr.h" +#include "clang/AST/ExprCXX.h" +#include "clang/ASTMatchers/ASTMatchFinder.h" +#include "clang/ASTMatchers/ASTMatchers.h" +#include "clang/Analysis/Analyses/CFGReachabilityAnalysis.h" +#include "clang/Analysis/CFG.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallPtrSet.h" +#include <optional> + +using namespace clang::ast_matchers; +using namespace clang::tidy::utils; + +namespace clang::tidy::llvm_check { + +namespace { + +/// Contains information about a use-after-erase. +struct UseAfterErase { + // The DeclRefExpr that constituted the use of the operation. + const DeclRefExpr *DeclRef; + + // Is the order in which the erase and the use are evaluated undefined? + bool EvaluationOrderUndefined = false; + + // Does the use happen in a later loop iteration than the erase? + // + // We default to false and change it to true if required in find(). + bool UseHappensInLaterLoopIteration = false; +}; + +/// Finds uses of an `mlir::Operation` variable that are reachable after the +/// operation was erased (and maintains state required by the various internal +/// helper functions). +class UseAfterEraseFinder { +public: + UseAfterEraseFinder(ASTContext *TheContext); + + // Within the given code block, finds the first use of 'ErasedVariable' that + // occurs after 'EraseCall' (the expression that erases the operation). If a + // use-after-erase is found, returns information about it. + std::optional<UseAfterErase> find(Stmt *CodeBlock, const Expr *EraseCall, + const DeclRefExpr *ErasedVariable); + +private: + std::optional<UseAfterErase> findInternal(const CFGBlock *Block, + const Expr *EraseCall, + const ValueDecl *ErasedVariable); + void getUsesAndReinits(const CFGBlock *Block, const ValueDecl *ErasedVariable, + SmallVectorImpl<const DeclRefExpr *> *Uses, + llvm::SmallPtrSetImpl<const Stmt *> *Reinits); + void getDeclRefs(const CFGBlock *Block, const Decl *ErasedVariable, + llvm::SmallPtrSetImpl<const DeclRefExpr *> *DeclRefs); + void getReinits(const CFGBlock *Block, const Decl *ErasedVariable, + llvm::SmallPtrSetImpl<const Stmt *> *Stmts, + llvm::SmallPtrSetImpl<const DeclRefExpr *> *DeclRefs); + + ASTContext *Context; + std::unique_ptr<ExprSequence> Sequence; + std::unique_ptr<StmtToBlockMap> BlockMap; + llvm::SmallPtrSet<const CFGBlock *, 8> Visited; +}; + +} // namespace + +UseAfterEraseFinder::UseAfterEraseFinder(ASTContext *TheContext) + : Context(TheContext) {} + +std::optional<UseAfterErase> +UseAfterEraseFinder::find(Stmt *CodeBlock, const Expr *EraseCall, + const DeclRefExpr *ErasedVariable) { + // 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; + const auto TheCFG = CFG::buildCFG(nullptr, CodeBlock, Context, Options); + if (!TheCFG) + return std::nullopt; + + Sequence = std::make_unique<ExprSequence>(TheCFG.get(), CodeBlock, Context); + BlockMap = std::make_unique<StmtToBlockMap>(TheCFG.get(), Context); + Visited.clear(); + + const CFGBlock *EraseBlock = BlockMap->blockContainingStmt(EraseCall); + if (!EraseBlock) + EraseBlock = &TheCFG->getEntry(); + + auto TheUseAfterErase = + findInternal(EraseBlock, EraseCall, ErasedVariable->getDecl()); + + if (TheUseAfterErase) { + if (const CFGBlock *UseBlock = + BlockMap->blockContainingStmt(TheUseAfterErase->DeclRef)) { + // Does the use happen in a later loop iteration than the erase? + // - 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 + // erase is reachable from the use. + CFGReverseBlockReachabilityAnalysis CFA(*TheCFG); + TheUseAfterErase->UseHappensInLaterLoopIteration = + UseBlock == EraseBlock ? Visited.contains(UseBlock) + : CFA.isReachable(UseBlock, EraseBlock); + } + } + return TheUseAfterErase; +} + +std::optional<UseAfterErase> +UseAfterEraseFinder::findInternal(const CFGBlock *Block, const Expr *EraseCall, + const ValueDecl *ErasedVariable) { + if (Visited.contains(Block)) + return std::nullopt; + + // Mark the block as visited (except if this is the block containing the erase + // call and it's being visited the first time). + if (!EraseCall) + Visited.insert(Block); + + // Get all uses and reinits in the block. + SmallVector<const DeclRefExpr *, 1> Uses; + llvm::SmallPtrSet<const Stmt *, 1> Reinits; + getUsesAndReinits(Block, ErasedVariable, &Uses, &Reinits); + + // Ignore all reinitializations where the erase potentially comes after the + // reinit. + SmallVector<const Stmt *, 1> ReinitsToDelete; + for (const auto *Reinit : Reinits) + if (EraseCall && Sequence->potentiallyAfter(EraseCall, Reinit)) + ReinitsToDelete.push_back(Reinit); + for (const auto *Reinit : ReinitsToDelete) + Reinits.erase(Reinit); + + // Find all uses that potentially come after the erase. + for (const auto *Use : Uses) { + if (!EraseCall || Sequence->potentiallyAfter(Use, EraseCall)) { + // 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 auto *Reinit : Reinits) + if (!Sequence->potentiallyAfter(Reinit, Use)) + HaveSavingReinit = true; + + if (!HaveSavingReinit) { + UseAfterErase TheUseAfterErase; + TheUseAfterErase.DeclRef = Use; + + // Is this a use-after-erase that depends on order of evaluation? + // This is the case if the erase potentially comes after the use (and we + // already know that the use potentially comes after the erase, which + // taken together tells us that the ordering is unclear). + TheUseAfterErase.EvaluationOrderUndefined = + EraseCall != nullptr && Sequence->potentiallyAfter(EraseCall, Use); + + return TheUseAfterErase; + } + } + } + + // If the operation 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, ErasedVariable)) + return Found; + } + + return std::nullopt; +} + +void UseAfterEraseFinder::getUsesAndReinits( + const CFGBlock *Block, const ValueDecl *ErasedVariable, + SmallVectorImpl<const DeclRefExpr *> *Uses, + llvm::SmallPtrSetImpl<const Stmt *> *Reinits) { + llvm::SmallPtrSet<const DeclRefExpr *, 1> DeclRefs; + llvm::SmallPtrSet<const DeclRefExpr *, 1> ReinitDeclRefs; + + getDeclRefs(Block, ErasedVariable, &DeclRefs); + getReinits(Block, ErasedVariable, 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(); + }); +} + +void UseAfterEraseFinder::getDeclRefs( + const CFGBlock *Block, const Decl *ErasedVariable, + llvm::SmallPtrSetImpl<const DeclRefExpr *> *DeclRefs) { + DeclRefs->clear(); + const auto DeclRefMatcher = + declRefExpr(to(equalsNode(ErasedVariable))).bind("declref"); + + for (const auto &Elem : *Block) { + const auto S = Elem.getAs<CFGStmt>(); + if (!S) + continue; + + for (const auto &Match : + match(findAll(DeclRefMatcher), *S->getStmt(), *Context)) { + const auto *DeclRef = Match.getNodeAs<DeclRefExpr>("declref"); + if (DeclRef && BlockMap->blockContainingStmt(DeclRef) == Block) + DeclRefs->insert(DeclRef); + } + } +} + +void UseAfterEraseFinder::getReinits( + const CFGBlock *Block, const Decl *ErasedVariable, + llvm::SmallPtrSetImpl<const Stmt *> *Stmts, + llvm::SmallPtrSetImpl<const DeclRefExpr *> *DeclRefs) { + const auto DeclRefMatcher = + declRefExpr(to(equalsNode(ErasedVariable))).bind("declref"); + + // A reinitialization gives the variable a new, valid operation, which makes + // subsequent uses safe again. We treat plain assignments and + // (re-)declarations as reinitializations. For "derived" ops (e.g. + // `mlir::func::FuncOp`), the assignment is an overloaded `operator=`, which + // is represented as a `CXXOperatorCallExpr` rather than a `BinaryOperator`. + const auto ReinitMatcher = + stmt(anyOf(binaryOperator(hasOperatorName("="), + hasLHS(ignoringParenImpCasts(DeclRefMatcher))), + cxxOperatorCallExpr( + hasOverloadedOperatorName("="), + hasArgument(0, ignoringParenImpCasts(DeclRefMatcher))), + declStmt(hasDescendant(equalsNode(ErasedVariable))))) + .bind("reinit"); + + Stmts->clear(); + DeclRefs->clear(); + for (const CFGElement &Elem : *Block) { + const auto S = Elem.getAs<CFGStmt>(); + if (!S) + continue; + + for (const auto &Match : + match(findAll(ReinitMatcher), *S->getStmt(), *Context)) { + const auto *TheStmt = Match.getNodeAs<Stmt>("reinit"); + const auto *DeclRef = Match.getNodeAs<DeclRefExpr>("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 'DeclRef' + // before adding it to the set. + if (DeclRef) + DeclRefs->insert(DeclRef); + } + } + } +} + +static void emitDiagnostic(const Expr *EraseCall, const DeclRefExpr *EraseArg, + const UseAfterErase &Use, ClangTidyCheck *Check) { + const auto UseLoc = Use.DeclRef->getExprLoc(); + const auto EraseLoc = EraseCall->getExprLoc(); + + Check->diag(UseLoc, "operation %0 is used after it was erased") + << EraseArg->getDecl(); + Check->diag(EraseLoc, "operation erased here", DiagnosticIDs::Note); + if (Use.EvaluationOrderUndefined) { + Check->diag(UseLoc, + "the use and erase are unsequenced, i.e. there is no guarantee " + "about the order in which they are evaluated", + DiagnosticIDs::Note); + } else if (Use.UseHappensInLaterLoopIteration) { + Check->diag(UseLoc, + "the use happens in a later loop iteration than the erase", + DiagnosticIDs::Note); + } +} + +void MlirUseAfterEraseCheck::registerMatchers(MatchFinder *Finder) { + // The reference to the operation that is being erased. + const auto Arg = declRefExpr(to(varDecl())).bind("arg"); + + // A "derived" op is a value of a subclass of `mlir::OpState` (e.g. + // `mlir::func::FuncOp`) that wraps an `mlir::Operation *`. The underlying + // operation can be obtained via the overloaded `operator->` or via + // `getOperation()`, both of which are declared in `mlir::OpState`. + const auto OfOpState = ofClass(isSameOrDerivedFrom("::mlir::OpState")); + const auto UnwrapDerivedOp = expr(anyOf( + cxxMemberCallExpr( + on(ignoringParenImpCasts(Arg)), + callee(cxxMethodDecl(hasName("getOperation"), OfOpState))), + cxxOperatorCallExpr(hasOverloadedOperatorName("->"), + hasArgument(0, ignoringParenImpCasts(Arg)), + callee(cxxMethodDecl(OfOpState))))); + + // A reference to the operation, either directly (a variable of type + // `mlir::Operation *`) or through a derived op that is unwrapped to the + // underlying `mlir::Operation *`. + const auto OpRef = expr(ignoringParenImpCasts(anyOf(Arg, UnwrapDerivedOp))); + + // Erasure through `mlir::Operation::erase` and `mlir::Operation::destroy` + // member functions. + const auto MemberErase = cxxMemberCallExpr( + on(OpRef), callee(cxxMethodDecl(hasAnyName("erase", "destroy"), + ofClass(hasName("::mlir::Operation"))))); + + // Erasure through `mlir::RewriterBase`. The operation that gets invalidated + // is always the first argument of these methods. + const auto RewriterErase = cxxMemberCallExpr( + callee( + cxxMethodDecl(hasAnyName("eraseOp", "eraseOpResults", "replaceOp", + "replaceOpWithNewOp"), + ofClass(isSameOrDerivedFrom("::mlir::RewriterBase")))), + hasArgument(0, OpRef)); + + Finder->addMatcher( + traverse(TK_AsIs, + expr(anyOf(MemberErase, RewriterErase), + anyOf(hasAncestor(compoundStmt(hasParent( + lambdaExpr().bind("containing-lambda")))), + hasAncestor(functionDecl().bind("containing-func")))) + .bind("erase-call")), + this); +} + +void MlirUseAfterEraseCheck::check(const MatchFinder::MatchResult &Result) { + const auto *EraseCall = Result.Nodes.getNodeAs<Expr>("erase-call"); + const auto *Arg = Result.Nodes.getNodeAs<DeclRefExpr>("arg"); + const auto *ContainingLambda = + Result.Nodes.getNodeAs<LambdaExpr>("containing-lambda"); + const auto *ContainingFunc = + Result.Nodes.getNodeAs<FunctionDecl>("containing-func"); + + // Only track operations that are stored in a local variable. + if (!Arg->getDecl()->getDeclContext()->isFunctionOrMethod()) + return; + + // Collect the code block that could use the operation after it was erased. + SmallVector<Stmt *> CodeBlocks; + if (ContainingLambda) + CodeBlocks.push_back(ContainingLambda->getBody()); + else if (ContainingFunc) + CodeBlocks.push_back(ContainingFunc->getBody()); + + for (Stmt *CodeBlock : CodeBlocks) { + UseAfterEraseFinder Finder(Result.Context); + if (auto Use = Finder.find(CodeBlock, EraseCall, Arg)) + emitDiagnostic(EraseCall, Arg, *Use, this); + } +} + +} // namespace clang::tidy::llvm_check diff --git a/clang-tools-extra/clang-tidy/llvm/MlirUseAfterEraseCheck.h b/clang-tools-extra/clang-tidy/llvm/MlirUseAfterEraseCheck.h new file mode 100644 index 0000000000000..52574ac76fdf9 --- /dev/null +++ b/clang-tools-extra/clang-tidy/llvm/MlirUseAfterEraseCheck.h @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_LLVM_MLIRUSEAFTERERASECHECK_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_LLVM_MLIRUSEAFTERERASECHECK_H + +#include "../ClangTidyCheck.h" + +namespace clang::tidy::llvm_check { + +/// Detects uses of an ``mlir::Operation`` after it has been erased, either +/// directly through ``Operation::erase``/``Operation::destroy``, through +/// ``mlir::RewriterBase`` helpers such as ``eraseOp`` and ``replaceOp``, +/// or through derived ops of ``mlir::OpState`` that provide are wrappers +/// around an ``mlir::Operation``. +/// +/// For the user-facing documentation see: +/// https://clang.llvm.org/extra/clang-tidy/checks/llvm/mlir-use-after-erase.html +class MlirUseAfterEraseCheck : public ClangTidyCheck { +public: + MlirUseAfterEraseCheck(StringRef Name, ClangTidyContext *Context) + : ClangTidyCheck(Name, Context) {} + void registerMatchers(ast_matchers::MatchFinder *Finder) override; + void check(const ast_matchers::MatchFinder::MatchResult &Result) override; + bool isLanguageVersionSupported(const LangOptions &LangOpts) const override { + return LangOpts.CPlusPlus; + } +}; + +} // namespace clang::tidy::llvm_check + +#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_LLVM_MLIRUSEAFTERERASECHECK_H diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst index 69c3bcf67b8db..efd5444633cd8 100644 --- a/clang-tools-extra/docs/ReleaseNotes.rst +++ b/clang-tools-extra/docs/ReleaseNotes.rst @@ -97,6 +97,15 @@ Improvements to clang-tidy New checks ^^^^^^^^^^ +- New :doc:`llvm-mlir-use-after-erase + <clang-tidy/checks/llvm/mlir-use-after-erase>` check. + + Detects uses of an ``mlir::Operation`` after it has been erased, either + directly through ``Operation::erase``/``Operation::destroy``, through + ``mlir::RewriterBase`` helpers such as ``eraseOp`` and ``replaceOp``, + or through derived ops of ``mlir::OpState`` that are wrappers around + an ``mlir::Operation``. + New check aliases ^^^^^^^^^^^^^^^^^ diff --git a/clang-tools-extra/docs/clang-tidy/checks/list.rst b/clang-tools-extra/docs/clang-tidy/checks/list.rst index 2a44dc78fbc89..7013c5f5e7510 100644 --- a/clang-tools-extra/docs/clang-tidy/checks/list.rst +++ b/clang-tools-extra/docs/clang-tidy/checks/list.rst @@ -246,6 +246,7 @@ Clang-Tidy Checks :doc:`llvm-formatv-string <llvm/formatv-string>`, :doc:`llvm-header-guard <llvm/header-guard>`, :doc:`llvm-include-order <llvm/include-order>`, "Yes" + :doc:`llvm-mlir-use-after-erase <llvm/mlir-use-after-erase>`, :doc:`llvm-namespace-comment <llvm/namespace-comment>`, :doc:`llvm-prefer-isa-or-dyn-cast-in-conditionals <llvm/prefer-isa-or-dyn-cast-in-conditionals>`, "Yes" :doc:`llvm-prefer-register-over-unsigned <llvm/prefer-register-over-unsigned>`, "Yes" diff --git a/clang-tools-extra/docs/clang-tidy/checks/llvm/mlir-use-after-erase.rst b/clang-tools-extra/docs/clang-tidy/checks/llvm/mlir-use-after-erase.rst new file mode 100644 index 0000000000000..b0a05453600ea --- /dev/null +++ b/clang-tools-extra/docs/clang-tidy/checks/llvm/mlir-use-after-erase.rst @@ -0,0 +1,39 @@ +.. title:: clang-tidy - llvm-mlir-use-after-erase + +llvm-mlir-use-after-erase +========================= + +Detects uses of an ``mlir::Operation`` after it has been erased. + +Once an operation has been erased its memory is no longer valid, so any +subsequent access to it is a use-after-free. This check flags such uses when +the operation is invalidated through: + +- ``mlir::Operation`` member functions ``erase()`` and ``destroy()``, or +- ``mlir::RewriterBase`` helpers ``eraseOp()``, ``eraseOpResults()``, + ``replaceOp()`` and ``replaceOpWithNewOp()``. +- ``mlir::OpState`` wrappers that provide ``operator->`` or ``getOperation()``. + This includes derived ops from ``mlir::Op``, which inherits ``mlir::OpState``. + +The analysis is control-flow aware: only uses that can actually be reached +after the erasing call are reported, and reassigning the variable to a new +operation clears the diagnostic. + +.. code-block:: c++ + + void example(mlir::RewriterBase &rewriter, mlir::Operation *op) { + rewriter.eraseOp(op); + op->dump(); // warning: operation 'op' is used after it was erased + } + +Uses that happen *before* the operation is erased, or after it has been reassigned, are not reported: + +.. code-block:: c++ + + void ok(mlir::Operation *op) { + op->dump(); // fine + op->erase(); + + op = ...; + op->dump(); // fine, op is reassigned to a new operation + } diff --git a/clang-tools-extra/test/clang-tidy/checkers/llvm/mlir-use-after-erase.cpp b/clang-tools-extra/test/clang-tidy/checkers/llvm/mlir-use-after-erase.cpp new file mode 100644 index 0000000000000..f4cfdb5873fe7 --- /dev/null +++ b/clang-tools-extra/test/clang-tidy/checkers/llvm/mlir-use-after-erase.cpp @@ -0,0 +1,476 @@ +// RUN: %check_clang_tidy %s llvm-mlir-use-after-erase %t + +namespace mlir { + +class BitVector {}; +class ValueRange {}; +class Location {}; + +class Operation { +public: + void destroy(); + void erase(); + + void dump(); + Location getLoc() { + return Location{}; + } +}; + +class OpState { +public: + Operation *operator->() const { return state; } + Operation *getOperation() { return state; } + +private: + Operation *state; +}; +class Op : public OpState {}; + +class Builder {}; +class OpBuilder : public Builder {}; +class RewriterBase : public OpBuilder { +public: + virtual ~RewriterBase() = default; + + virtual void eraseOp(Operation *op); + Operation *eraseOpResults(Operation *op, const BitVector &eraseIndices); + + virtual void replaceOp(Operation *op, ValueRange newValues); + virtual void replaceOp(Operation *op, Operation *newOp); + template <typename OpTy, typename... Args> + OpTy replaceOpWithNewOp(Operation *op, Args &&...args); +}; +class PatternRewriter : public RewriterBase {}; + +} // namespace mlir + +namespace test { +class MyOp : public mlir::Op { +}; + + +} // namespace test + +namespace { + +void consume([[maybe_unused]] mlir::Operation *op) { +} + +struct NoReturnDestructor { + [[noreturn]] ~NoReturnDestructor(); +}; +#define FATAL() NoReturnDestructor() + +// A type that happens to have erase() / destroy() methods but is unrelated to mlir::Operation +class NotAnOperation { +public: + void erase() {} + void destroy() {} + void dump() {} +}; + +// A type that exposes operator-> / getOperation() returning mlir::Operation* +// but is not derived from mlir::OpState +class NotAnOp { +public: + mlir::Operation* operator->() const { return op; } + mlir::Operation* getOperation() const { return op; } +private: + mlir::Operation* op; +}; + +// A type that has RewriterBase-like method names but is not derived from mlir::RewriterBase +class NotARewriter { +public: + void eraseOp(mlir::Operation *op) {} + void replaceOp(mlir::Operation *op, mlir::Operation *newOp) {} +}; + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// +// General tests + +void useAfterDestroy() { + mlir::Operation* op{}; + op->destroy(); + op->dump(); + // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase] + // CHECK-MESSAGES: :[[@LINE-3]]:9: note: operation erased here +} + +void useAfterErase() { + mlir::Operation* op{}; + op->erase(); + op->dump(); + // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase] + // CHECK-MESSAGES: :[[@LINE-3]]:9: note: operation erased here +} + +void useAfterRewriterErase() { + mlir::RewriterBase rewriter{}; + mlir::Operation* op{}; + rewriter.eraseOp(op); + op->dump(); + // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase] + // CHECK-MESSAGES: :[[@LINE-3]]:14: note: operation erased here +} + +void useAfterRewriterEraseOpResults() { + mlir::RewriterBase rewriter{}; + mlir::Operation* op{}; + rewriter.eraseOpResults(op, {}); + op->dump(); + // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase] + // CHECK-MESSAGES: :[[@LINE-3]]:14: note: operation erased here +} + +void useAfterRewriterReplaceOp() { + mlir::RewriterBase rewriter{}; + { + mlir::Operation* op{}; + rewriter.replaceOp(op, {}); + op->dump(); + // CHECK-MESSAGES: :[[@LINE-1]]:9: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase] + // CHECK-MESSAGES: :[[@LINE-3]]:18: note: operation erased here + } + { + mlir::Operation* op{}; + mlir::Operation* op2{}; + rewriter.replaceOp(op, op2); + op->dump(); + // CHECK-MESSAGES: :[[@LINE-1]]:9: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase] + // CHECK-MESSAGES: :[[@LINE-3]]:18: note: operation erased here + } +} + +void useAfterRewriterReplaceOpWithNewOp() { + mlir::RewriterBase rewriter{}; + mlir::Operation* op{}; + rewriter.replaceOpWithNewOp<mlir::Operation>(op); + op->dump(); + // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase] + // CHECK-MESSAGES: :[[@LINE-3]]:14: note: operation erased here +} + +void useAfterPatternRewriterErase() { + mlir::PatternRewriter rewriter{}; + mlir::Operation* op{}; + rewriter.eraseOp(op); + op->dump(); + // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase] + // CHECK-MESSAGES: :[[@LINE-3]]:14: note: operation erased here +} + +void useAfterDerivedArrowErase() { + test::MyOp op{}; + op->erase(); + op->dump(); + // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase] + // CHECK-MESSAGES: :[[@LINE-3]]:9: note: operation erased here +} + +void useAfterDerivedGetOperationErase() { + test::MyOp op{}; + op.getOperation()->erase(); + op->dump(); + // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase] + // CHECK-MESSAGES: :[[@LINE-3]]:24: note: operation erased here +} + +void useAfterRewriterEraseDerived() { + mlir::RewriterBase rewriter{}; + test::MyOp op{}; + rewriter.eraseOp(op.getOperation()); + op->dump(); + // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase] + // CHECK-MESSAGES: :[[@LINE-3]]:14: note: operation erased here +} + +void onlyFlagOneUseAfterErase() { + mlir::Operation* op{}; + op->erase(); + op->dump(); // A warning should only be emitted for one use-after-erase + // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase] + // CHECK-MESSAGES: :[[@LINE-3]]:9: note: operation erased here + op->dump(); +} + +void eraseAfterErase() { + mlir::Operation* op{}; + op->erase(); + op->erase(); // Erase-after-erase also counts as a use + // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase] + // CHECK-MESSAGES: :[[@LINE-3]]:9: note: operation erased here +} + +void useInCall() { + mlir::Operation* op{}; + op->erase(); + consume(op); + // CHECK-MESSAGES: :[[@LINE-1]]:13: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase] + // CHECK-MESSAGES: :[[@LINE-3]]:9: note: operation erased here +} + +void useParameterAfterErase(mlir::Operation* op) { + op->erase(); + op->dump(); // The erased operation may be a function parameter + // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase] + // CHECK-MESSAGES: :[[@LINE-3]]:9: note: operation erased here +} + +struct Container { + void useAfterEraseInMemberFunction() { + mlir::Operation* op{}; + op->erase(); + op->dump(); // The check also works in member functions + // CHECK-MESSAGES: :[[@LINE-1]]:9: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase] + // CHECK-MESSAGES: :[[@LINE-3]]:13: note: operation erased here + } +}; + +void useAfterEraseInLambda() { + [] { + mlir::Operation* op{}; + op->erase(); + op->dump(); // The check also works in lambdas + // CHECK-MESSAGES: :[[@LINE-1]]:9: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase] + // CHECK-MESSAGES: :[[@LINE-3]]:13: note: operation erased here + }(); +} + +// Using an operation before it is erased is fine +void useBeforeErase() { + { + mlir::Operation* op{}; + op->dump(); + op->destroy(); + } + { + mlir::PatternRewriter rewriter{}; + mlir::Operation* op{}; + op->dump(); + rewriter.eraseOp(op); + } +} + +void derivedUseBeforeErase() { + test::MyOp op{}; + op->dump(); // Use before erase is fine + op->erase(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests involving control flow + +void useAndEraseInLoop(int size) { + mlir::Operation* op{}; + for (int i = 0; i < size; ++i) { + op->dump(); + // CHECK-MESSAGES: :[[@LINE-1]]:9: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase] + // CHECK-MESSAGES: :[[@LINE+2]]:13: note: operation erased here + // CHECK-MESSAGES: :[[@LINE-3]]:9: note: the use happens in a later loop iteration than the erase + op->erase(); + } +} + +void derivedUseAndEraseInLoop(int size) { + test::MyOp op{}; + for (int i = 0; i < size; ++i) { + op->dump(); + // CHECK-MESSAGES: :[[@LINE-1]]:9: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase] + // CHECK-MESSAGES: :[[@LINE+2]]:13: note: operation erased here + // CHECK-MESSAGES: :[[@LINE-3]]:9: note: the use happens in a later loop iteration than the erase + op->erase(); + } +} + +void declaredInLoop() { + for (int i = 0; i < 10; ++i) { + mlir::Operation* op{}; + op->dump(); + op->erase(); // No warning if 'op' is declared inside the loop, because it is a fresh operation on every iteration + } +} + +void returnAfterErase(int i) { + mlir::Operation* op{}; + for (int j = 0; j < 10; ++j) { + op->dump(); + if (i > 0) { + op->erase(); // Don't warn if we return after the erase + return; + } + } +} + +void differentBranches(int i) { + mlir::Operation* op{}; + if (i > 0) { + op->erase(); + } else { + op->dump(); // Don't warn if the use is in a different branch from the erase + } +} + +void differentBranchesRewriter(int i) { + mlir::PatternRewriter rewriter{}; + mlir::Operation* op{}; + if (i > 0) { + rewriter.eraseOp(op); + } else { + op->dump(); // Don't warn if the use is in a different branch from the erase + } +} + +void switchFallthrough(int i) { + mlir::Operation* op{}; + switch (i) { + case 1: + op->erase(); + case 2: // A fallthrough in a switch statement causes a warning + op->dump(); + // CHECK-MESSAGES: :[[@LINE-1]]:9: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase] + // CHECK-MESSAGES: :[[@LINE-4]]:13: note: operation erased here + break; + } +} + +void noReturnDestructorAfterErase(bool cond, mlir::Operation* other) { + mlir::Operation* op{}; + op->erase(); + if (cond) { + FATAL(); // [[noreturn]] destructor + } else { + op = other; + } + op->dump(); // only reachable through the branch that reassigns 'op', so no warning should be emitted +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests for reinitializations + +void reassignAfterErase(mlir::Operation* other) { + mlir::Operation* op{}; + op->erase(); + op = other; // Reassignment to a new operation makes the later use safe + op->dump(); +} + +void derivedReassignAfterErase(test::MyOp other) { + test::MyOp op{}; + op->erase(); + op = other; // Reassignment to a new operation makes the later use safe + op->dump(); +} + +void conditionalReassign(int i, mlir::Operation* other) { + mlir::Operation* op{}; + op->erase(); + if (i > 0) { + op = other; // Reassignment is not guaranteed to happen before the use, so we still warn + } + op->dump(); + // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase] + // CHECK-MESSAGES: :[[@LINE-6]]:9: note: operation erased here +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests related to order of evaluation within expressions + +void sequencingOfEraseAndUse() { + const auto fn = [](mlir::Location, mlir::Operation) { + }; + mlir::RewriterBase rewriter{}; + mlir::Operation* op{}; + fn(op->getLoc(), rewriter.replaceOpWithNewOp<mlir::Operation>(op)); + // CHECK-MESSAGES: :[[@LINE-1]]:8: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase] + // CHECK-MESSAGES: :[[@LINE-2]]:31: note: operation erased here + // CHECK-MESSAGES: :[[@LINE-3]]:8: note: the use and erase are unsequenced, i.e. there is no guarantee about the order in which they are evaluated +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests for types and operations that are not tracked + +void eraseOnNonOperation() { + NotAnOperation* obj{}; + obj->erase(); // erase() on an unrelated type are not flagged + obj->dump(); +} + +void destroyOnNonOperation() { + NotAnOperation* obj{}; + obj->destroy(); // destroy() on an unrelated type is not flagged + obj->dump(); +} + +void eraseOpOnNonRewriter() { + NotARewriter rewriter{}; + mlir::Operation* op{}; + rewriter.eraseOp(op); // eraseOp() on a type not derived from RewriterBase is not flagged + op->dump(); +} + +void replaceOpOnNonRewriter() { + NotARewriter rewriter{}; + mlir::Operation* op{}; + mlir::Operation* newOp{}; + rewriter.replaceOp(op, newOp); // replaceOp() on a type not derived from RewriterBase is not flagged + op->dump(); +} + +void eraseOnNonOpState() { + NotAnOp op{}; + op->erase(); // operator-> on a type not derived from mlir::OpState is not flagged + op->dump(); +} + +void eraseGetOperationOnNonOpState() { + NotAnOp op{}; + op.getOperation()->erase(); // getOperation() on a type not derived from mlir::OpState is not flagged + op->dump(); +} + +mlir::Operation* globalOp{}; // Operations that are not local variables are not tracked +void eraseGlobalOperation() { + globalOp->erase(); + globalOp->dump(); +} + +struct OperationHolder { + mlir::Operation* op; // Operations stored in member fields are not tracked + void eraseAndUse() { + op->erase(); + op->dump(); + } +}; + +void differentOperations() { + mlir::Operation* op1{}; + mlir::Operation* op2{}; + op1->erase(); + op2->dump(); // Erasing one operation doesn't flag the use of a different operation +} + +void replacementOperationNotErased() { + mlir::RewriterBase rewriter{}; + mlir::Operation* op{}; + mlir::Operation* newOp{}; + rewriter.replaceOp(op, newOp); // The replacement operation passed to replaceOp is not the erased one + newOp->dump(); +} + +void decltypeIsNotUse() { + mlir::Operation* op{}; + op->erase(); + using OpType = decltype(op); // Using decltype on an erased operation is not flagged + OpType op2{}; + op2->dump(); +} + +void unevaluatedIsNotUse() { + mlir::Operation* op{}; + op->erase(); + static_assert(sizeof(op) == sizeof(void*)); // Unevaluated, so not a real use +} _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
