Skip to content

Commit 3151281

Browse files
authored
[clang-tidy] Add check to diagnose coroutine-hostile RAII objects (#68738)
This check detects **hostile-RAII** objects which should not **persist across a suspension point in a coroutine**. 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. The check considers the following type as hostile: - **Scoped-lockable types**: A scoped-lockable object persisting across a suspension point is problematic as the lock held by this object could be unlocked by a different thread. This would be undefined behaviour. - Types belonging to a configurable **denylist**. ```cpp // Call some async API while holding a lock. const my::MutexLock l(&mu_); // 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(); ```
1 parent 71c97c7 commit 3151281

File tree

8 files changed

+402
-0
lines changed

8 files changed

+402
-0
lines changed

clang-tools-extra/clang-tidy/misc/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ add_custom_target(genconfusable DEPENDS Confusables.inc)
1818

1919
add_clang_library(clangTidyMiscModule
2020
ConstCorrectnessCheck.cpp
21+
CoroutineHostileRAIICheck.cpp
2122
DefinitionsInHeadersCheck.cpp
2223
ConfusableIdentifierCheck.cpp
2324
HeaderIncludeCycleCheck.cpp
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
//===--- CoroutineHostileRAII.cpp - clang-tidy ----------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#include "CoroutineHostileRAIICheck.h"
10+
#include "../utils/OptionsUtils.h"
11+
#include "clang/AST/Attr.h"
12+
#include "clang/AST/Decl.h"
13+
#include "clang/AST/ExprCXX.h"
14+
#include "clang/AST/Stmt.h"
15+
#include "clang/AST/Type.h"
16+
#include "clang/ASTMatchers/ASTMatchFinder.h"
17+
#include "clang/ASTMatchers/ASTMatchers.h"
18+
#include "clang/ASTMatchers/ASTMatchersInternal.h"
19+
#include "clang/Basic/AttrKinds.h"
20+
#include "clang/Basic/DiagnosticIDs.h"
21+
22+
using namespace clang::ast_matchers;
23+
namespace clang::tidy::misc {
24+
namespace {
25+
using clang::ast_matchers::internal::BoundNodesTreeBuilder;
26+
27+
AST_MATCHER_P(Stmt, forEachPrevStmt, ast_matchers::internal::Matcher<Stmt>,
28+
InnerMatcher) {
29+
DynTypedNode P;
30+
bool IsHostile = false;
31+
for (const Stmt *Child = &Node; Child; Child = P.get<Stmt>()) {
32+
auto Parents = Finder->getASTContext().getParents(*Child);
33+
if (Parents.empty())
34+
break;
35+
P = *Parents.begin();
36+
auto *PCS = P.get<CompoundStmt>();
37+
if (!PCS)
38+
continue;
39+
for (const auto &Sibling : PCS->children()) {
40+
// Child contains suspension. Siblings after Child do not persist across
41+
// this suspension.
42+
if (Sibling == Child)
43+
break;
44+
// In case of a match, add the bindings as a separate match. Also don't
45+
// clear the bindings if a match is not found (unlike Matcher::matches).
46+
BoundNodesTreeBuilder SiblingBuilder;
47+
if (InnerMatcher.matches(*Sibling, Finder, &SiblingBuilder)) {
48+
Builder->addMatch(SiblingBuilder);
49+
IsHostile = true;
50+
}
51+
}
52+
}
53+
return IsHostile;
54+
}
55+
} // namespace
56+
57+
CoroutineHostileRAIICheck::CoroutineHostileRAIICheck(StringRef Name,
58+
ClangTidyContext *Context)
59+
: ClangTidyCheck(Name, Context),
60+
RAIITypesList(utils::options::parseStringList(
61+
Options.get("RAIITypesList", "std::lock_guard;std::scoped_lock"))) {}
62+
63+
void CoroutineHostileRAIICheck::registerMatchers(MatchFinder *Finder) {
64+
// A suspension happens with co_await or co_yield.
65+
auto ScopedLockable = varDecl(hasType(hasCanonicalType(hasDeclaration(
66+
hasAttr(attr::Kind::ScopedLockable)))))
67+
.bind("scoped-lockable");
68+
auto OtherRAII = varDecl(hasType(hasCanonicalType(hasDeclaration(
69+
namedDecl(hasAnyName(RAIITypesList))))))
70+
.bind("raii");
71+
Finder->addMatcher(expr(anyOf(coawaitExpr(), coyieldExpr()),
72+
forEachPrevStmt(declStmt(forEach(
73+
varDecl(anyOf(ScopedLockable, OtherRAII))))))
74+
.bind("suspension"),
75+
this);
76+
}
77+
78+
void CoroutineHostileRAIICheck::check(const MatchFinder::MatchResult &Result) {
79+
if (const auto *VD = Result.Nodes.getNodeAs<VarDecl>("scoped-lockable"))
80+
diag(VD->getLocation(),
81+
"%0 holds a lock across a suspension point of coroutine and could be "
82+
"unlocked by a different thread")
83+
<< VD;
84+
if (const auto *VD = Result.Nodes.getNodeAs<VarDecl>("raii"))
85+
diag(VD->getLocation(),
86+
"%0 persists across a suspension point of coroutine")
87+
<< VD;
88+
if (const auto *Suspension = Result.Nodes.getNodeAs<Expr>("suspension"))
89+
diag(Suspension->getBeginLoc(), "suspension point is here",
90+
DiagnosticIDs::Note);
91+
}
92+
93+
void CoroutineHostileRAIICheck::storeOptions(
94+
ClangTidyOptions::OptionMap &Opts) {
95+
Options.store(Opts, "RAIITypesList",
96+
utils::options::serializeStringList(RAIITypesList));
97+
}
98+
} // namespace clang::tidy::misc
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//===--- CoroutineHostileRAIICheck.h - clang-tidy ----------------*- C++-*-===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MISC_COROUTINESHOSTILERAIICHECK_H
10+
#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MISC_COROUTINESHOSTILERAIICHECK_H
11+
12+
#include "../ClangTidyCheck.h"
13+
#include "clang/AST/ASTTypeTraits.h"
14+
#include "clang/ASTMatchers/ASTMatchFinder.h"
15+
#include "llvm/ADT/StringRef.h"
16+
#include <vector>
17+
18+
namespace clang::tidy::misc {
19+
20+
/// Detects when objects of certain hostile RAII types persists across
21+
/// suspension points in a coroutine. Such hostile types include scoped-lockable
22+
/// types and types belonging to a configurable denylist.
23+
///
24+
/// For the user-facing documentation see:
25+
/// http://clang.llvm.org/extra/clang-tidy/checks/misc/coroutine-hostile-raii.html
26+
class CoroutineHostileRAIICheck : public ClangTidyCheck {
27+
public:
28+
CoroutineHostileRAIICheck(llvm::StringRef Name, ClangTidyContext *Context);
29+
30+
bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
31+
return LangOpts.CPlusPlus20;
32+
}
33+
34+
void registerMatchers(ast_matchers::MatchFinder *Finder) override;
35+
void storeOptions(ClangTidyOptions::OptionMap &Opts) override;
36+
void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
37+
38+
std::optional<TraversalKind> getCheckTraversalKind() const override {
39+
return TK_AsIs;
40+
}
41+
42+
private:
43+
// List of fully qualified types which should not persist across a suspension
44+
// point in a coroutine.
45+
std::vector<StringRef> RAIITypesList;
46+
};
47+
48+
} // namespace clang::tidy::misc
49+
50+
#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MISC_COROUTINESHOSTILERAIICHECK_H

clang-tools-extra/clang-tidy/misc/MiscTidyModule.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include "../ClangTidyModuleRegistry.h"
1212
#include "ConfusableIdentifierCheck.h"
1313
#include "ConstCorrectnessCheck.h"
14+
#include "CoroutineHostileRAIICheck.h"
1415
#include "DefinitionsInHeadersCheck.h"
1516
#include "HeaderIncludeCycleCheck.h"
1617
#include "IncludeCleanerCheck.h"
@@ -41,6 +42,8 @@ class MiscModule : public ClangTidyModule {
4142
"misc-confusable-identifiers");
4243
CheckFactories.registerCheck<ConstCorrectnessCheck>(
4344
"misc-const-correctness");
45+
CheckFactories.registerCheck<CoroutineHostileRAIICheck>(
46+
"misc-coroutine-hostile-raii");
4447
CheckFactories.registerCheck<DefinitionsInHeadersCheck>(
4548
"misc-definitions-in-headers");
4649
CheckFactories.registerCheck<HeaderIncludeCycleCheck>(

clang-tools-extra/docs/ReleaseNotes.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,13 @@ New checks
163163
Flags coroutines that suspend while a lock guard is in scope at the
164164
suspension point.
165165

166+
- New :doc:`misc-coroutine-hostile-raii
167+
<clang-tidy/checks/misc/coroutine-hostile-raii>` check.
168+
169+
Detects when objects of certain hostile RAII types persists across suspension
170+
points in a coroutine. Such hostile types include scoped-lockable types and
171+
types belonging to a configurable denylist.
172+
166173
- New :doc:`modernize-use-constraints
167174
<clang-tidy/checks/modernize/use-constraints>` check.
168175

clang-tools-extra/docs/clang-tidy/checks/list.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ Clang-Tidy Checks
241241
:doc:`llvmlibc-restrict-system-libc-headers <llvmlibc/restrict-system-libc-headers>`, "Yes"
242242
:doc:`misc-confusable-identifiers <misc/confusable-identifiers>`,
243243
:doc:`misc-const-correctness <misc/const-correctness>`, "Yes"
244+
:doc:`misc-coroutine-hostile-raii <misc/coroutine-hostile-raii.html>`_,
244245
:doc:`misc-definitions-in-headers <misc/definitions-in-headers>`, "Yes"
245246
:doc:`misc-header-include-cycle <misc/header-include-cycle>`,
246247
:doc:`misc-include-cleaner <misc/include-cleaner>`, "Yes"
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
.. title:: clang-tidy - misc-coroutine-hostile-raii
2+
3+
misc-coroutine-hostile-raii
4+
====================
5+
6+
Detects when objects of certain hostile RAII types persists across suspension
7+
points in a coroutine. Such hostile types include scoped-lockable types and
8+
types belonging to a configurable denylist.
9+
10+
Some objects require that they be destroyed on the same thread that created them.
11+
Traditionally this requirement was often phrased as "must be a local variable",
12+
under the assumption that local variables always work this way. However this is
13+
incorrect with C++20 coroutines, since an intervening ``co_await`` may cause the
14+
coroutine to suspend and later be resumed on another thread.
15+
16+
The lifetime of an object that requires being destroyed on the same thread must
17+
not encompass a ``co_await`` or ``co_yield`` point. If you create/destroy an object,
18+
you must do so without allowing the coroutine to suspend in the meantime.
19+
20+
Following types are considered as hostile:
21+
22+
- Scoped-lockable types: A scoped-lockable object persisting across a suspension
23+
point is problematic as the lock held by this object could be unlocked by a
24+
different thread. This would be undefined behaviour.
25+
This includes all types annotated with the ``scoped_lockable`` attribute.
26+
27+
- Types belonging to a configurable denylist.
28+
29+
.. code-block:: c++
30+
31+
// Call some async API while holding a lock.
32+
{
33+
const my::MutexLock l(&mu_);
34+
35+
// Oops! The async Bar function may finish on a different
36+
// thread from the one that created the MutexLock object and therefore called
37+
// Mutex::Lock -- now Mutex::Unlock will be called on the wrong thread.
38+
co_await Bar();
39+
}
40+
41+
42+
Options
43+
-------
44+
45+
.. option:: RAIITypesList
46+
47+
A semicolon-separated list of qualified types which should not be allowed to
48+
persist across suspension points.
49+
Eg: ``my::lockable; a::b;::my::other::lockable;``
50+
The default value of this option is `"std::lock_guard;std::scoped_lock"`.

0 commit comments

Comments
 (0)