Skip to content

Commit 41b7f88

Browse files
committed
Lint for holding locks across await points
Fixes rust-lang#4226 This introduces the lint await_holding_lock. For async functions, we iterate over all types in generator_interior_types and look for types named MutexGuard, RwLockReadGuard, or RwLockWriteGuard. If we find one then we emit a lint.
1 parent 82be9dc commit 41b7f88

File tree

6 files changed

+189
-0
lines changed

6 files changed

+189
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -1188,6 +1188,7 @@ Released 2018-09-13
11881188
[`assertions_on_constants`]: https://rust-lang.github.io/rust-clippy/master/index.html#assertions_on_constants
11891189
[`assign_op_pattern`]: https://rust-lang.github.io/rust-clippy/master/index.html#assign_op_pattern
11901190
[`assign_ops`]: https://rust-lang.github.io/rust-clippy/master/index.html#assign_ops
1191+
[`await_holding_lock`]: https://rust-lang.github.io/rust-clippy/master/index.html#await_holding_lock
11911192
[`bad_bit_mask`]: https://rust-lang.github.io/rust-clippy/master/index.html#bad_bit_mask
11921193
[`blacklisted_name`]: https://rust-lang.github.io/rust-clippy/master/index.html#blacklisted_name
11931194
[`block_in_if_condition_expr`]: https://rust-lang.github.io/rust-clippy/master/index.html#block_in_if_condition_expr
+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
use crate::utils::span_lint_and_note;
2+
use if_chain::if_chain;
3+
use rustc_hir::intravisit::FnKind;
4+
use rustc_hir::{Body, FnDecl, HirId, IsAsync};
5+
use rustc_lint::{LateContext, LateLintPass};
6+
use rustc_session::{declare_lint_pass, declare_tool_lint};
7+
use rustc_span::{Span, Symbol};
8+
9+
declare_clippy_lint! {
10+
/// **What it does:** Checks for calls to await while holding a MutexGuard.
11+
///
12+
/// **Why is this bad?** This is almost certainly an error which can result
13+
/// in a deadlock because the reactor will invoke code not visible to the
14+
/// currently visible scope.
15+
///
16+
/// **Known problems:** Detects only specifically named guard types:
17+
/// MutexGuard, RwLockReadGuard, and RwLockWriteGuard.
18+
///
19+
/// **Example:**
20+
///
21+
/// ```rust
22+
/// use std::sync::Mutex;
23+
///
24+
/// async fn foo(x: &Mutex<u32>) {
25+
/// let guard = x.lock().unwrap();
26+
/// *guard += 1;
27+
/// bar.await;
28+
/// }
29+
/// ```
30+
/// Use instead:
31+
/// ```rust
32+
/// use std::sync::Mutex;
33+
///
34+
/// async fn foo(x: &Mutex<u32>) {
35+
/// {
36+
/// let guard = x.lock().unwrap();
37+
/// *guard += 1;
38+
/// }
39+
/// bar.await;
40+
/// }
41+
/// ```
42+
pub AWAIT_HOLDING_LOCK,
43+
pedantic,
44+
"Inside an async function, holding a MutexGuard while calling await"
45+
}
46+
47+
const MUTEX_GUARD_TYPES: [&str; 3] = ["MutexGuard", "RwLockReadGuard", "RwLockWriteGuard"];
48+
49+
declare_lint_pass!(AwaitHoldingLock => [AWAIT_HOLDING_LOCK]);
50+
51+
impl LateLintPass<'_, '_> for AwaitHoldingLock {
52+
fn check_fn(
53+
&mut self,
54+
cx: &LateContext<'_, '_>,
55+
fn_kind: FnKind<'_>,
56+
_: &FnDecl<'_>,
57+
_: &Body<'_>,
58+
span: Span,
59+
_: HirId,
60+
) {
61+
if !is_async_fn(fn_kind) {
62+
return;
63+
}
64+
65+
for ty_clause in &cx.tables.generator_interior_types {
66+
if_chain! {
67+
if let rustc_middle::ty::Adt(adt, _) = ty_clause.ty.kind;
68+
if let Some(&sym) = cx.get_def_path(adt.did).iter().last();
69+
if is_symbol_mutex_guard(sym);
70+
then {
71+
span_lint_and_note(
72+
cx,
73+
AWAIT_HOLDING_LOCK,
74+
ty_clause.span,
75+
"this MutexGuard is held across an 'await' point",
76+
ty_clause.scope_span.unwrap_or(span),
77+
"these are all the await points this lock is held through"
78+
);
79+
}
80+
}
81+
}
82+
}
83+
}
84+
85+
fn is_async_fn(fn_kind: FnKind<'_>) -> bool {
86+
fn_kind.header().map_or(false, |h| match h.asyncness {
87+
IsAsync::Async => true,
88+
IsAsync::NotAsync => false,
89+
})
90+
}
91+
92+
fn is_symbol_mutex_guard(sym: Symbol) -> bool {
93+
let sym_str = sym.as_str();
94+
for ty in &MUTEX_GUARD_TYPES {
95+
if sym_str == *ty {
96+
return true;
97+
}
98+
}
99+
false
100+
}

clippy_lints/src/lib.rs

+4
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ mod assertions_on_constants;
177177
mod assign_ops;
178178
mod atomic_ordering;
179179
mod attrs;
180+
mod await_holding_lock;
180181
mod bit_mask;
181182
mod blacklisted_name;
182183
mod block_in_if_condition;
@@ -494,6 +495,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
494495
&attrs::INLINE_ALWAYS,
495496
&attrs::UNKNOWN_CLIPPY_LINTS,
496497
&attrs::USELESS_ATTRIBUTE,
498+
&await_holding_lock::AWAIT_HOLDING_LOCK,
497499
&bit_mask::BAD_BIT_MASK,
498500
&bit_mask::INEFFECTIVE_BIT_MASK,
499501
&bit_mask::VERBOSE_BIT_MASK,
@@ -856,6 +858,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
856858
]);
857859
// end register lints, do not remove this comment, it’s used in `update_lints`
858860

861+
store.register_late_pass(|| box await_holding_lock::AwaitHoldingLock);
859862
store.register_late_pass(|| box serde_api::SerdeAPI);
860863
store.register_late_pass(|| box utils::internal_lints::CompilerLintFunctions::new());
861864
store.register_late_pass(|| box utils::internal_lints::LintWithoutLintPass::default());
@@ -1090,6 +1093,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
10901093

10911094
store.register_group(true, "clippy::pedantic", Some("clippy_pedantic"), vec![
10921095
LintId::of(&attrs::INLINE_ALWAYS),
1096+
LintId::of(&await_holding_lock::AWAIT_HOLDING_LOCK),
10931097
LintId::of(&checked_conversions::CHECKED_CONVERSIONS),
10941098
LintId::of(&copies::MATCH_SAME_ARMS),
10951099
LintId::of(&copies::SAME_FUNCTIONS_IN_IF_CONDITION),

src/lintlist/mod.rs

+7
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ pub static ref ALL_LINTS: Vec<Lint> = vec![
5252
deprecation: None,
5353
module: "assign_ops",
5454
},
55+
Lint {
56+
name: "await_holding_lock",
57+
group: "pedantic",
58+
desc: "Inside an async function, holding a MutexGuard while calling await",
59+
deprecation: None,
60+
module: "await_holding_lock",
61+
},
5562
Lint {
5663
name: "bad_bit_mask",
5764
group: "correctness",

tests/ui/await_holding_lock.rs

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// edition:2018
2+
#![warn(clippy::await_holding_lock)]
3+
4+
use std::sync::Mutex;
5+
6+
async fn bad(x: &Mutex<u32>) -> u32 {
7+
let guard = x.lock().unwrap();
8+
baz().await
9+
}
10+
11+
async fn good(x: &Mutex<u32>) -> u32 {
12+
{
13+
let guard = x.lock().unwrap();
14+
let y = *guard + 1;
15+
}
16+
baz().await;
17+
let guard = x.lock().unwrap();
18+
47
19+
}
20+
21+
async fn baz() -> u32 {
22+
42
23+
}
24+
25+
async fn also_bad(x: &Mutex<u32>) -> u32 {
26+
let first = baz().await;
27+
28+
let guard = x.lock().unwrap();
29+
30+
let second = baz().await;
31+
32+
let third = baz().await;
33+
34+
first + second + third
35+
}
36+
37+
fn main() {
38+
let m = Mutex::new(100);
39+
good(&m);
40+
bad(&m);
41+
also_bad(&m);
42+
}

tests/ui/await_holding_lock.stderr

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
error: this MutexGuard is held across an 'await' point
2+
--> $DIR/await_holding_lock.rs:7:9
3+
|
4+
LL | let guard = x.lock().unwrap();
5+
| ^^^^^
6+
|
7+
= note: `-D clippy::await-holding-lock` implied by `-D warnings`
8+
note: these are all the await points this lock is held through
9+
--> $DIR/await_holding_lock.rs:7:5
10+
|
11+
LL | / let guard = x.lock().unwrap();
12+
LL | | baz().await
13+
LL | | }
14+
| |_^
15+
16+
error: this MutexGuard is held across an 'await' point
17+
--> $DIR/await_holding_lock.rs:28:9
18+
|
19+
LL | let guard = x.lock().unwrap();
20+
| ^^^^^
21+
|
22+
note: these are all the await points this lock is held through
23+
--> $DIR/await_holding_lock.rs:28:5
24+
|
25+
LL | / let guard = x.lock().unwrap();
26+
LL | |
27+
LL | | let second = baz().await;
28+
LL | |
29+
... |
30+
LL | | first + second + third
31+
LL | | }
32+
| |_^
33+
34+
error: aborting due to 2 previous errors
35+

0 commit comments

Comments
 (0)