diff --git a/clang-tools-extra/clang-tidy/cppcoreguidelines/CMakeLists.txt b/clang-tools-extra/clang-tidy/cppcoreguidelines/CMakeLists.txt --- a/clang-tools-extra/clang-tidy/cppcoreguidelines/CMakeLists.txt +++ b/clang-tools-extra/clang-tidy/cppcoreguidelines/CMakeLists.txt @@ -18,6 +18,7 @@ MissingStdForwardCheck.cpp NarrowingConversionsCheck.cpp NoMallocCheck.cpp + NoSuspendWithLockCheck.cpp OwningMemoryCheck.cpp PreferMemberInitializerCheck.cpp ProBoundsArrayToPointerDecayCheck.cpp @@ -50,6 +51,7 @@ clang_target_link_libraries(clangTidyCppCoreGuidelinesModule PRIVATE + clangAnalysis clangAST clangASTMatchers clangBasic diff --git a/clang-tools-extra/clang-tidy/cppcoreguidelines/CppCoreGuidelinesTidyModule.cpp b/clang-tools-extra/clang-tidy/cppcoreguidelines/CppCoreGuidelinesTidyModule.cpp --- a/clang-tools-extra/clang-tidy/cppcoreguidelines/CppCoreGuidelinesTidyModule.cpp +++ b/clang-tools-extra/clang-tidy/cppcoreguidelines/CppCoreGuidelinesTidyModule.cpp @@ -32,6 +32,7 @@ #include "MissingStdForwardCheck.h" #include "NarrowingConversionsCheck.h" #include "NoMallocCheck.h" +#include "NoSuspendWithLockCheck.h" #include "OwningMemoryCheck.h" #include "PreferMemberInitializerCheck.h" #include "ProBoundsArrayToPointerDecayCheck.h" @@ -89,6 +90,8 @@ CheckFactories.registerCheck( "cppcoreguidelines-narrowing-conversions"); CheckFactories.registerCheck("cppcoreguidelines-no-malloc"); + CheckFactories.registerCheck( + "cppcoreguidelines-no-suspend-with-lock"); CheckFactories.registerCheck( "cppcoreguidelines-noexcept-destructor"); CheckFactories.registerCheck( diff --git a/clang-tools-extra/clang-tidy/cppcoreguidelines/NoSuspendWithLockCheck.h b/clang-tools-extra/clang-tidy/cppcoreguidelines/NoSuspendWithLockCheck.h new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clang-tidy/cppcoreguidelines/NoSuspendWithLockCheck.h @@ -0,0 +1,44 @@ +//===--- NoSuspendWithLockCheck.h - clang-tidy ------------------*- 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 +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_CPPCOREGUIDELINES_NOSUSPENDWITHLOCKCHECK_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_CPPCOREGUIDELINES_NOSUSPENDWITHLOCKCHECK_H + +#include "../ClangTidyCheck.h" + +namespace clang::tidy::cppcoreguidelines { + +/// Flag coroutines that suspend while any lock guard is alive. +/// This check implements CppCoreGuideline CP.52. +/// +/// For the user-facing documentation see: +/// http://clang.llvm.org/extra/clang-tidy/checks/cppcoreguidelines/no-suspend-with-lock.html +class NoSuspendWithLockCheck : public ClangTidyCheck { +public: + NoSuspendWithLockCheck(StringRef Name, ClangTidyContext *Context) + : ClangTidyCheck(Name, Context), + LockGuards(Options.get("LockGuards", + "::std::unique_lock;::std::scoped_lock;::" + "std::shared_lock;::std::lock_guard")) {} + void storeOptions(ClangTidyOptions::OptionMap &Opts) override; + 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.CPlusPlus20; + } + +private: + /// Semicolon-separated list of fully qualified names of lock guard template + /// types. Defaults to + /// `::std::unique_lock;::std::scoped_lock;::std::shared_lock;::std::lock_guard`. + const StringRef LockGuards; +}; + +} // namespace clang::tidy::cppcoreguidelines + +#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_CPPCOREGUIDELINES_NOSUSPENDWITHLOCKCHECK_H diff --git a/clang-tools-extra/clang-tidy/cppcoreguidelines/NoSuspendWithLockCheck.cpp b/clang-tools-extra/clang-tidy/cppcoreguidelines/NoSuspendWithLockCheck.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clang-tidy/cppcoreguidelines/NoSuspendWithLockCheck.cpp @@ -0,0 +1,73 @@ +//===--- NoSuspendWithLockCheck.cpp - clang-tidy --------------------------===// +// +// 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 "NoSuspendWithLockCheck.h" +#include "../utils/ExprSequence.h" +#include "../utils/Matchers.h" +#include "../utils/OptionsUtils.h" +#include "clang/AST/ASTContext.h" +#include "clang/ASTMatchers/ASTMatchFinder.h" +#include "clang/Analysis/CFG.h" + +using namespace clang::ast_matchers; + +namespace clang::tidy::cppcoreguidelines { + +void NoSuspendWithLockCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) { + Options.store(Opts, "LockGuards", LockGuards); +} + +void NoSuspendWithLockCheck::registerMatchers(MatchFinder *Finder) { + auto LockType = elaboratedType(namesType(templateSpecializationType( + hasDeclaration(namedDecl(matchers::matchesAnyListedName( + utils::options::parseStringList(LockGuards))))))); + + StatementMatcher Lock = + declStmt(has(varDecl(hasType(LockType)).bind("lock-decl"))) + .bind("lock-decl-stmt"); + Finder->addMatcher( + expr(anyOf(coawaitExpr(), coyieldExpr(), dependentCoawaitExpr()), + forCallable(functionDecl().bind("function")), + unless(isInTemplateInstantiation()), + hasAncestor( + compoundStmt(has(Lock), forCallable(equalsBoundNode("function"))) + .bind("block"))) + .bind("suspend"), + this); +} + +void NoSuspendWithLockCheck::check(const MatchFinder::MatchResult &Result) { + const auto *Block = Result.Nodes.getNodeAs("block"); + const auto *Suspend = Result.Nodes.getNodeAs("suspend"); + const auto *LockDecl = Result.Nodes.getNodeAs("lock-decl"); + const auto *LockStmt = Result.Nodes.getNodeAs("lock-decl-stmt"); + + if (!Block || !Suspend || !LockDecl || !LockStmt) + return; + + ASTContext &Context = *Result.Context; + CFG::BuildOptions Options; + Options.AddImplicitDtors = true; + Options.AddTemporaryDtors = true; + + std::unique_ptr TheCFG = CFG::buildCFG( + nullptr, const_cast(Block), &Context, Options); + if (!TheCFG) + return; + + utils::ExprSequence Sequence(TheCFG.get(), Block, &Context); + const Stmt *LastBlockStmt = Block->body_back(); + if (Sequence.inSequence(LockStmt, Suspend) && + (Suspend == LastBlockStmt || + Sequence.inSequence(Suspend, LastBlockStmt))) { + diag(Suspend->getBeginLoc(), "coroutine suspended with lock %0 held") + << LockDecl; + } +} + +} // namespace clang::tidy::cppcoreguidelines diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst --- a/clang-tools-extra/docs/ReleaseNotes.rst +++ b/clang-tools-extra/docs/ReleaseNotes.rst @@ -136,6 +136,12 @@ extracted from an optional-like type and then used to create a new instance of the same optional-like type. +- New :doc:`cppcoreguidelines-no-suspend-with-lock + ` check. + + Flags coroutines that suspend while a lock guard is in scope at the + suspension point. + - New :doc:`modernize-use-constraints ` check. diff --git a/clang-tools-extra/docs/clang-tidy/checks/cppcoreguidelines/no-suspend-with-lock.rst b/clang-tools-extra/docs/clang-tidy/checks/cppcoreguidelines/no-suspend-with-lock.rst new file mode 100644 --- /dev/null +++ b/clang-tools-extra/docs/clang-tidy/checks/cppcoreguidelines/no-suspend-with-lock.rst @@ -0,0 +1,40 @@ +.. title:: clang-tidy - cppcoreguidelines-no-suspend-with-lock + +cppcoreguidelines-no-suspend-with-lock +====================================== + +Flags coroutines that suspend while a lock guard is in scope at the +suspension point. + +When a coroutine suspends, any mutexes held by the coroutine will remain +locked until the coroutine resumes and eventually destructs the lock guard. +This can lead to long periods with a mutex held and runs the risk of deadlock. + +Instead, locks should be released before suspending a coroutine. + +This check only checks suspending coroutines while a lock_guard is in scope; +it does not consider manual locking or unlocking of mutexes, e.g., through +calls to ``std::mutex::lock()``. + +Examples: + +.. code-block:: c++ + + future bad_coro() { + std::lock_guard lock{mtx}; + ++some_counter; + co_await something(); // Suspending while holding a mutex + } + + future good_coro() { + { + std::lock_guard lock{mtx}; + ++some_counter; + } + // Destroy the lock_guard to release the mutex before suspending the coroutine + co_await something(); // Suspending while holding a mutex + } + +This check implements `CP.52 +`_ +from the C++ Core Guidelines. diff --git a/clang-tools-extra/docs/clang-tidy/checks/list.rst b/clang-tools-extra/docs/clang-tidy/checks/list.rst --- a/clang-tools-extra/docs/clang-tidy/checks/list.rst +++ b/clang-tools-extra/docs/clang-tidy/checks/list.rst @@ -200,6 +200,7 @@ `cppcoreguidelines-missing-std-forward `_, `cppcoreguidelines-narrowing-conversions `_, `cppcoreguidelines-no-malloc `_, + `cppcoreguidelines-no-suspend-with-lock `_, `cppcoreguidelines-owning-memory `_, `cppcoreguidelines-prefer-member-initializer `_, "Yes" `cppcoreguidelines-pro-bounds-array-to-pointer-decay `_, diff --git a/clang-tools-extra/test/clang-tidy/checkers/cppcoreguidelines/no-suspend-with-lock.cpp b/clang-tools-extra/test/clang-tidy/checkers/cppcoreguidelines/no-suspend-with-lock.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/test/clang-tidy/checkers/cppcoreguidelines/no-suspend-with-lock.cpp @@ -0,0 +1,279 @@ +// RUN: %check_clang_tidy -std=c++20 %s cppcoreguidelines-no-suspend-with-lock %t -- -- -fno-delayed-template-parsing + +// NOLINTBEGIN +namespace std { +template +struct coroutine_traits { + using promise_type = typename T::promise_type; +}; +template +struct coroutine_handle; +template <> +struct coroutine_handle { + coroutine_handle() noexcept; + coroutine_handle(decltype(nullptr)) noexcept; + static constexpr coroutine_handle from_address(void*); +}; +template +struct coroutine_handle { + coroutine_handle() noexcept; + coroutine_handle(decltype(nullptr)) noexcept; + static constexpr coroutine_handle from_address(void*); + operator coroutine_handle<>() const noexcept; +}; + +template +class unique_lock { +public: + unique_lock() noexcept; + explicit unique_lock(Mutex &m); + unique_lock& operator=(unique_lock&&); + void unlock(); + Mutex* release() noexcept; + Mutex* mutex() const noexcept; + void swap(unique_lock& other) noexcept; +}; + +class mutex { +public: + mutex() noexcept; + ~mutex(); + mutex(const mutex &) = delete; + mutex &operator=(const mutex &) = delete; + + void lock(); + void unlock(); +}; +} // namespace std + +class my_own_mutex { +public: + void lock(); + void unlock(); +}; + +struct Awaiter { + bool await_ready() noexcept; + void await_suspend(std::coroutine_handle<>) noexcept; + void await_resume() noexcept; +}; + +struct Coro { + struct promise_type { + Awaiter initial_suspend(); + Awaiter final_suspend() noexcept; + void return_void(); + Coro get_return_object(); + void unhandled_exception(); + Awaiter yield_value(int); + }; +}; +// NOLINTEND + +std::mutex mtx; +std::mutex mtx2; + +Coro awaits_with_lock() { + std::unique_lock lock(mtx); + + co_await Awaiter{}; + // CHECK-MESSAGES: [[@LINE-1]]:3: warning: coroutine suspended with lock 'lock' held [cppcoreguidelines-no-suspend-with-lock] + + co_await Awaiter{}; + // CHECK-MESSAGES: [[@LINE-1]]:3: warning: coroutine suspended with lock 'lock' held [cppcoreguidelines-no-suspend-with-lock] + + if (true) { + co_await Awaiter{}; + // CHECK-MESSAGES: [[@LINE-1]]:5: warning: coroutine suspended with lock 'lock' held [cppcoreguidelines-no-suspend-with-lock] + } + + if (true) { + std::unique_lock lock2; + lock2.unlock(); + co_await Awaiter{}; + // CHECK-MESSAGES: [[@LINE-1]]:5: warning: coroutine suspended with lock 'lock2' held [cppcoreguidelines-no-suspend-with-lock] + } +} + +Coro awaits_with_lock_in_try() try { + std::unique_lock lock(mtx); + co_await Awaiter{}; + // CHECK-MESSAGES: [[@LINE-1]]:3: warning: coroutine suspended with lock 'lock' held [cppcoreguidelines-no-suspend-with-lock] +} catch (...) {} + +Coro lock_possibly_unlocked() { + // CppCoreGuideline CP.52's enforcement strictly requires flagging + // code that suspends while any lock guard is not destructed. + + { + std::unique_lock lock(mtx); + lock.unlock(); + co_await Awaiter{}; + // CHECK-MESSAGES: [[@LINE-1]]:5: warning: coroutine suspended with lock 'lock' held [cppcoreguidelines-no-suspend-with-lock] + } + + { + std::unique_lock lock(mtx); + lock.release(); + co_await Awaiter{}; + // CHECK-MESSAGES: [[@LINE-1]]:5: warning: coroutine suspended with lock 'lock' held [cppcoreguidelines-no-suspend-with-lock] + } + + { + std::unique_lock lock(mtx); + std::unique_lock lock2; + lock.swap(lock2); + co_await Awaiter{}; + // CHECK-MESSAGES: [[@LINE-1]]:5: warning: coroutine suspended with lock 'lock' held [cppcoreguidelines-no-suspend-with-lock] + } + + { + std::unique_lock lock(mtx); + std::unique_lock lock2{mtx2}; + lock.swap(lock2); + co_await Awaiter{}; + // CHECK-MESSAGES: [[@LINE-1]]:5: warning: coroutine suspended with lock 'lock' held [cppcoreguidelines-no-suspend-with-lock] + } + + { + std::unique_lock lock(mtx); + lock = std::unique_lock{}; + co_await Awaiter{}; + // CHECK-MESSAGES: [[@LINE-1]]:5: warning: coroutine suspended with lock 'lock' held [cppcoreguidelines-no-suspend-with-lock] + } + + { + std::unique_lock lock(mtx); + lock = std::unique_lock{mtx2}; + co_await Awaiter{}; + // CHECK-MESSAGES: [[@LINE-1]]:5: warning: coroutine suspended with lock 'lock' held [cppcoreguidelines-no-suspend-with-lock] + } +} + +Coro await_with_underlying_mutex_unlocked() { + std::unique_lock lock(mtx); + + // Even though we unlock the mutex here, 'lock' is still active unless + // there is a call to lock.unlock(). This is a bug in the program since + // it will result in locking the mutex twice. The check does not track + // unlock calls on the underlying mutex held by a lock guard object. + mtx.unlock(); + + co_await Awaiter{}; + // CHECK-MESSAGES: [[@LINE-1]]:3: warning: coroutine suspended with lock 'lock' held [cppcoreguidelines-no-suspend-with-lock] +} + +Coro await_with_empty_lock() { + std::unique_lock lock; + co_await Awaiter{}; + // CHECK-MESSAGES: [[@LINE-1]]:3: warning: coroutine suspended with lock 'lock' held [cppcoreguidelines-no-suspend-with-lock] +} + +Coro await_before_lock() { + co_await Awaiter{}; + std::unique_lock lock(mtx); +} + +Coro await_with_lock_different_scope() { + { + std::unique_lock lock(mtx); + } + co_await Awaiter{}; +} + +Coro await_with_goto() { +first: + co_await Awaiter{}; + std::unique_lock lock(mtx); + goto first; +} + +void await_in_lambda() { + auto f1 = []() -> Coro { + std::unique_lock lock(mtx); + co_await Awaiter{}; + // CHECK-MESSAGES: [[@LINE-1]]:5: warning: coroutine suspended with lock 'lock' held [cppcoreguidelines-no-suspend-with-lock] + }; + + auto f2 = [](auto& m) -> Coro { + std::unique_lock lock(m); + co_await Awaiter{}; + // CHECK-MESSAGES: [[@LINE-1]]:5: warning: coroutine suspended with lock 'lock' held [cppcoreguidelines-no-suspend-with-lock] + }; +} + +void await_in_lambda_without_immediate_mutex() { + std::unique_lock lock(mtx); + + auto f1 = []() -> Coro { + co_await Awaiter{}; + }; + + // The check only finds suspension points where there is a lock held in the + // immediate callable. + f1(); +} + +Coro yields_with_lock() { + std::unique_lock lock(mtx); + co_yield 0; + // CHECK-MESSAGES: [[@LINE-1]]:3: warning: coroutine suspended with lock 'lock' held [cppcoreguidelines-no-suspend-with-lock] +} + +template +Coro awaits_templated_type(Mutex& m) { + std::unique_lock lock(m); + co_await Awaiter{}; + // CHECK-MESSAGES: [[@LINE-1]]:3: warning: coroutine suspended with lock 'lock' held [cppcoreguidelines-no-suspend-with-lock] +} + +template +Coro awaits_in_template_function(T) { + std::unique_lock lock(mtx); + co_await Awaiter{}; + // CHECK-MESSAGES: [[@LINE-1]]:3: warning: coroutine suspended with lock 'lock' held [cppcoreguidelines-no-suspend-with-lock] +} + +template +Coro awaits_in_never_instantiated_template_of_mutex(Mutex& m) { + // Nothing should instantiate this function + std::unique_lock lock(m); + co_await Awaiter{}; + // CHECK-MESSAGES: [[@LINE-1]]:3: warning: coroutine suspended with lock 'lock' held [cppcoreguidelines-no-suspend-with-lock] +} + +template +Coro awaits_in_never_instantiated_templated_function(T) { + // Nothing should instantiate this function + std::unique_lock lock(mtx); + co_await Awaiter{}; + // CHECK-MESSAGES: [[@LINE-1]]:3: warning: coroutine suspended with lock 'lock' held [cppcoreguidelines-no-suspend-with-lock] +} + +template +struct my_container { + + Coro push_back() { + std::unique_lock lock(mtx_); + co_await Awaiter{}; + // CHECK-MESSAGES: [[@LINE-1]]:5: warning: coroutine suspended with lock 'lock' held [cppcoreguidelines-no-suspend-with-lock] + } + + template + Coro emplace_back(Args&&...) { + std::unique_lock lock(mtx_); + co_await Awaiter{}; + // CHECK-MESSAGES: [[@LINE-1]]:5: warning: coroutine suspended with lock 'lock' held [cppcoreguidelines-no-suspend-with-lock] + } + + std::mutex mtx_; +}; + +void calls_templated_functions() { + my_own_mutex m2; + awaits_templated_type(mtx); + awaits_templated_type(m2); + + awaits_in_template_function(1); + awaits_in_template_function(1.0); +}