-
Notifications
You must be signed in to change notification settings - Fork 14.3k
[clang-tidy] Add check to diagnose coroutine-hostile RAII objects #68738
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f9e2905
78e3102
6880288
ab1f823
db7d9bc
7197982
1565fae
79d0109
bd3101f
a5a88c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
//===--- CoroutineHostileRAII.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 "CoroutineHostileRAIICheck.h" | ||
#include "../utils/OptionsUtils.h" | ||
#include "clang/AST/Attr.h" | ||
#include "clang/AST/Decl.h" | ||
#include "clang/AST/ExprCXX.h" | ||
#include "clang/AST/Stmt.h" | ||
#include "clang/AST/Type.h" | ||
#include "clang/ASTMatchers/ASTMatchFinder.h" | ||
#include "clang/ASTMatchers/ASTMatchers.h" | ||
#include "clang/ASTMatchers/ASTMatchersInternal.h" | ||
#include "clang/Basic/AttrKinds.h" | ||
#include "clang/Basic/DiagnosticIDs.h" | ||
|
||
using namespace clang::ast_matchers; | ||
namespace clang::tidy::misc { | ||
namespace { | ||
using clang::ast_matchers::internal::BoundNodesTreeBuilder; | ||
|
||
AST_MATCHER_P(Stmt, forEachPrevStmt, ast_matchers::internal::Matcher<Stmt>, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. even for an locally-defined matcher, it would be helpful to have a comment on what this matcher does (the definition of "before" is non-obvious). I also don't think its contract makes much sense: the definition of
This is not just an implementation limitation but important to correctness, because we're really talking about variable scope (initialized before, but destroyed after). But if that's the case, the matcher should also be named differently, and it should yield only |
||
InnerMatcher) { | ||
DynTypedNode P; | ||
bool IsHostile = false; | ||
for (const Stmt *Child = &Node; Child; Child = P.get<Stmt>()) { | ||
auto Parents = Finder->getASTContext().getParents(*Child); | ||
if (Parents.empty()) | ||
break; | ||
P = *Parents.begin(); | ||
auto *PCS = P.get<CompoundStmt>(); | ||
if (!PCS) | ||
continue; | ||
for (const auto &Sibling : PCS->children()) { | ||
// Child contains suspension. Siblings after Child do not persist across | ||
// this suspension. | ||
if (Sibling == Child) | ||
break; | ||
// In case of a match, add the bindings as a separate match. Also don't | ||
// clear the bindings if a match is not found (unlike Matcher::matches). | ||
BoundNodesTreeBuilder SiblingBuilder; | ||
if (InnerMatcher.matches(*Sibling, Finder, &SiblingBuilder)) { | ||
Builder->addMatch(SiblingBuilder); | ||
IsHostile = true; | ||
} | ||
} | ||
} | ||
return IsHostile; | ||
} | ||
} // namespace | ||
|
||
CoroutineHostileRAIICheck::CoroutineHostileRAIICheck(StringRef Name, | ||
ClangTidyContext *Context) | ||
: ClangTidyCheck(Name, Context), | ||
RAIITypesList(utils::options::parseStringList( | ||
Options.get("RAIITypesList", "std::lock_guard;std::scoped_lock"))) {} | ||
|
||
void CoroutineHostileRAIICheck::registerMatchers(MatchFinder *Finder) { | ||
// A suspension happens with co_await or co_yield. | ||
auto ScopedLockable = varDecl(hasType(hasCanonicalType(hasDeclaration( | ||
hasAttr(attr::Kind::ScopedLockable))))) | ||
.bind("scoped-lockable"); | ||
auto OtherRAII = varDecl(hasType(hasCanonicalType(hasDeclaration( | ||
namedDecl(hasAnyName(RAIITypesList)))))) | ||
.bind("raii"); | ||
Finder->addMatcher(expr(anyOf(coawaitExpr(), coyieldExpr()), | ||
forEachPrevStmt(declStmt(forEach( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this implies a quadratic algorithm: every suspend point is going to walk every prior statement. (the matcher framework has memoization but it won't apply here). It may well be fine in practice, and I don't really have a better suggestion - leaning heavily on matchers is idiomatic in clang-tidy, and is rarely efficient :-) No action needed here. |
||
varDecl(anyOf(ScopedLockable, OtherRAII)))))) | ||
.bind("suspension"), | ||
this); | ||
} | ||
|
||
void CoroutineHostileRAIICheck::check(const MatchFinder::MatchResult &Result) { | ||
if (const auto *VD = Result.Nodes.getNodeAs<VarDecl>("scoped-lockable")) | ||
diag(VD->getLocation(), | ||
"%0 holds a lock across a suspension point of coroutine and could be " | ||
"unlocked by a different thread") | ||
<< VD; | ||
if (const auto *VD = Result.Nodes.getNodeAs<VarDecl>("raii")) | ||
diag(VD->getLocation(), | ||
"%0 persists across a suspension point of coroutine") | ||
<< VD; | ||
if (const auto *Suspension = Result.Nodes.getNodeAs<Expr>("suspension")) | ||
diag(Suspension->getBeginLoc(), "suspension point is here", | ||
DiagnosticIDs::Note); | ||
} | ||
|
||
void CoroutineHostileRAIICheck::storeOptions( | ||
ClangTidyOptions::OptionMap &Opts) { | ||
Options.store(Opts, "RAIITypesList", | ||
utils::options::serializeStringList(RAIITypesList)); | ||
} | ||
} // namespace clang::tidy::misc |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
//===--- CoroutineHostileRAIICheck.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_MISC_COROUTINESHOSTILERAIICHECK_H | ||
#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MISC_COROUTINESHOSTILERAIICHECK_H | ||
|
||
#include "../ClangTidyCheck.h" | ||
#include "clang/AST/ASTTypeTraits.h" | ||
#include "clang/ASTMatchers/ASTMatchFinder.h" | ||
#include "llvm/ADT/StringRef.h" | ||
#include <vector> | ||
|
||
namespace clang::tidy::misc { | ||
|
||
/// Detects when objects of certain hostile RAII types persists across | ||
/// suspension points in a coroutine. Such hostile types include scoped-lockable | ||
/// types and types belonging to a configurable denylist. | ||
/// | ||
/// For the user-facing documentation see: | ||
/// http://clang.llvm.org/extra/clang-tidy/checks/misc/coroutine-hostile-raii.html | ||
class CoroutineHostileRAIICheck : public ClangTidyCheck { | ||
public: | ||
CoroutineHostileRAIICheck(llvm::StringRef Name, ClangTidyContext *Context); | ||
|
||
bool isLanguageVersionSupported(const LangOptions &LangOpts) const override { | ||
return LangOpts.CPlusPlus20; | ||
} | ||
|
||
void registerMatchers(ast_matchers::MatchFinder *Finder) override; | ||
void storeOptions(ClangTidyOptions::OptionMap &Opts) override; | ||
void check(const ast_matchers::MatchFinder::MatchResult &Result) override; | ||
|
||
usx95 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
std::optional<TraversalKind> getCheckTraversalKind() const override { | ||
return TK_AsIs; | ||
} | ||
|
||
private: | ||
// List of fully qualified types which should not persist across a suspension | ||
// point in a coroutine. | ||
std::vector<StringRef> RAIITypesList; | ||
}; | ||
|
||
} // namespace clang::tidy::misc | ||
|
||
#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MISC_COROUTINESHOSTILERAIICHECK_H |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
.. title:: clang-tidy - misc-coroutine-hostile-raii | ||
|
||
misc-coroutine-hostile-raii | ||
==================== | ||
|
||
Detects when objects of certain hostile RAII types persists across suspension | ||
points in a coroutine. Such hostile types include scoped-lockable types and | ||
types belonging to a configurable denylist. | ||
|
||
Some objects require that they be destroyed on the same thread that created them. | ||
Traditionally this requirement was often phrased as "must be a local variable", | ||
under the assumption that local variables always work this way. However this is | ||
incorrect with C++20 coroutines, since an intervening ``co_await`` may cause the | ||
coroutine to suspend and later be resumed on another thread. | ||
|
||
The lifetime of an object that requires being destroyed on the same thread must | ||
not encompass a ``co_await`` or ``co_yield`` point. If you create/destroy an object, | ||
you must do so without allowing the coroutine to suspend in the meantime. | ||
|
||
Following types are considered as hostile: | ||
|
||
- Scoped-lockable types: A scoped-lockable object persisting across a suspension | ||
usx95 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
point is problematic as the lock held by this object could be unlocked by a | ||
different thread. This would be undefined behaviour. | ||
This includes all types annotated with the ``scoped_lockable`` attribute. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's kind of unfortunate that do this based on But in practice this seems like a really useful heuristic, and I don't have a better suggestion. Maybe it'll need refinement in future. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is it worth mentioning that/why |
||
|
||
- Types belonging to a configurable denylist. | ||
|
||
.. code-block:: c++ | ||
|
||
// Call some async API while holding a lock. | ||
{ | ||
const my::MutexLock l(&mu_); | ||
usx95 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// Oops! The async Bar function may finish on a different | ||
// thread from the one that created the MutexLock object and therefore called | ||
// Mutex::Lock -- now Mutex::Unlock will be called on the wrong thread. | ||
co_await Bar(); | ||
} | ||
|
||
|
||
Options | ||
------- | ||
|
||
.. option:: RAIITypesList | ||
|
||
A semicolon-separated list of qualified types which should not be allowed to | ||
persist across suspension points. | ||
Eg: ``my::lockable; a::b;::my::other::lockable;`` | ||
usx95 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
The default value of this option is `"std::lock_guard;std::scoped_lock"`. |
Uh oh!
There was an error while loading. Please reload this page.