Skip to content

Commit 170a213

Browse files
committed
warn conditional wait() calls and add more tests
1 parent e7dbc05 commit 170a213

File tree

5 files changed

+242
-64
lines changed

5 files changed

+242
-64
lines changed

Diff for: clippy_lints/src/zombie_processes.rs

+154-57
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
use clippy_utils::diagnostics::span_lint_and_then;
2-
use clippy_utils::visitors::for_each_local_use_after_expr;
3-
use clippy_utils::{fn_def_id, get_enclosing_block, match_any_def_paths, match_def_path, paths};
2+
use clippy_utils::{fn_def_id, get_enclosing_block, match_any_def_paths, match_def_path, path_to_local_id, paths};
43
use rustc_ast::Mutability;
54
use rustc_errors::Applicability;
6-
use rustc_hir::intravisit::{walk_expr, Visitor};
7-
use rustc_hir::{Expr, ExprKind, HirId, Local, Node, PatKind, Stmt, StmtKind};
5+
use rustc_hir::intravisit::{walk_block, walk_expr, walk_local, Visitor};
6+
use rustc_hir::{Expr, ExprKind, HirId, LetStmt, Node, PatKind, Stmt, StmtKind};
87
use rustc_lint::{LateContext, LateLintPass};
98
use rustc_session::declare_lint_pass;
109
use rustc_span::sym;
1110
use std::ops::ControlFlow;
11+
use ControlFlow::{Break, Continue};
1212

1313
declare_clippy_lint! {
1414
/// ### What it does
@@ -48,52 +48,21 @@ impl<'tcx> LateLintPass<'tcx> for ZombieProcesses {
4848
&& match_def_path(cx, child_adt.did(), &paths::CHILD)
4949
{
5050
match cx.tcx.parent_hir_node(expr.hir_id) {
51-
Node::Local(local) if let PatKind::Binding(_, local_id, ..) = local.pat.kind => {
52-
// If the `Child` is assigned to a variable, we want to check if the code never calls `.wait()`
53-
// on the variable, and lint if not.
54-
// This is difficult to do because expressions can be arbitrarily complex
55-
// and the variable can "escape" in various ways, e.g. you can take a `&mut` to the variable
56-
// and call `.wait()` through that, or pass it to another function...
57-
// So instead we do the inverse, checking if all uses are either:
58-
// - a field access (`child.{stderr,stdin,stdout}`)
59-
// - calling `id` or `kill`
60-
// - no use at all (e.g. `let _x = child;`)
61-
// - taking a shared reference (`&`), `wait()` can't go through that
62-
// None of these are sufficient to prevent zombie processes
63-
// Doing it like this means more FNs, but FNs are better than FPs.
64-
let has_no_wait = for_each_local_use_after_expr(cx, local_id, expr.hir_id, |expr| {
65-
match cx.tcx.parent_hir_node(expr.hir_id) {
66-
Node::Stmt(Stmt {
67-
kind: StmtKind::Semi(_),
68-
..
69-
}) => ControlFlow::Continue(()),
70-
Node::Expr(expr) if let ExprKind::Field(..) = expr.kind => ControlFlow::Continue(()),
71-
Node::Expr(expr) if let ExprKind::AddrOf(_, Mutability::Not, _) = expr.kind => {
72-
ControlFlow::Continue(())
73-
},
74-
Node::Expr(expr)
75-
if let Some(fn_did) = fn_def_id(cx, expr)
76-
&& match_any_def_paths(cx, fn_did, &[&paths::CHILD_ID, &paths::CHILD_KILL])
77-
.is_some() =>
78-
{
79-
ControlFlow::Continue(())
80-
},
81-
82-
// Conservatively assume that all other kinds of nodes call `.wait()` somehow.
83-
_ => ControlFlow::Break(()),
84-
}
85-
})
86-
.is_continue();
51+
Node::LetStmt(local)
52+
if let PatKind::Binding(_, local_id, ..) = local.pat.kind
53+
&& let Some(enclosing_block) = get_enclosing_block(cx, expr.hir_id) =>
54+
{
55+
let mut vis = WaitFinder::WalkUpTo(cx, local_id);
8756

8857
// If it does have a `wait()` call, we're done. Don't lint.
89-
if !has_no_wait {
58+
if let Break(BreakReason::WaitFound) = walk_block(&mut vis, enclosing_block) {
9059
return;
9160
}
9261

9362
// Don't emit a suggestion since the binding is used later
9463
check(cx, expr, local.hir_id, false);
9564
},
96-
Node::Local(&Local { pat, hir_id, .. }) if let PatKind::Wild = pat.kind => {
65+
Node::LetStmt(&LetStmt { pat, hir_id, .. }) if let PatKind::Wild = pat.kind => {
9766
// `let _ = child;`, also dropped immediately without `wait()`ing
9867
check(cx, expr, hir_id, true);
9968
},
@@ -111,6 +80,132 @@ impl<'tcx> LateLintPass<'tcx> for ZombieProcesses {
11180
}
11281
}
11382

83+
enum BreakReason {
84+
WaitFound,
85+
EarlyReturn,
86+
}
87+
88+
/// A visitor responsible for finding a `wait()` call on a local variable.
89+
///
90+
/// Conditional `wait()` calls are assumed to not call wait:
91+
/// ```ignore
92+
/// let mut c = Command::new("").spawn().unwrap();
93+
/// if true {
94+
/// c.wait();
95+
/// }
96+
/// ```
97+
///
98+
/// Note that this visitor does NOT explicitly look for `wait()` calls directly, but rather does the
99+
/// inverse -- checking if all uses of the local are either:
100+
/// - a field access (`child.{stderr,stdin,stdout}`)
101+
/// - calling `id` or `kill`
102+
/// - no use at all (e.g. `let _x = child;`)
103+
/// - taking a shared reference (`&`), `wait()` can't go through that
104+
/// None of these are sufficient to prevent zombie processes.
105+
/// Doing it like this means more FNs, but FNs are better than FPs.
106+
///
107+
/// `return` expressions, conditional or not, short-circuit the visitor because
108+
/// if a `wait()` call hadn't been found at that point, it might never reach one at a later point:
109+
/// ```ignore
110+
/// let mut c = Command::new("").spawn().unwrap();
111+
/// if true {
112+
/// return; // Break(BreakReason::EarlyReturn)
113+
/// }
114+
/// c.wait(); // this might not be reachable
115+
/// ```
116+
enum WaitFinder<'a, 'tcx> {
117+
WalkUpTo(&'a LateContext<'tcx>, HirId),
118+
Found(&'a LateContext<'tcx>, HirId),
119+
}
120+
121+
impl<'a, 'tcx> Visitor<'tcx> for WaitFinder<'a, 'tcx> {
122+
type Result = ControlFlow<BreakReason>;
123+
124+
fn visit_local(&mut self, l: &'tcx LetStmt<'tcx>) -> Self::Result {
125+
if let Self::WalkUpTo(cx, local_id) = *self
126+
&& let PatKind::Binding(_, pat_id, ..) = l.pat.kind
127+
&& local_id == pat_id
128+
{
129+
*self = Self::Found(cx, local_id);
130+
}
131+
132+
walk_local(self, l)
133+
}
134+
135+
fn visit_expr(&mut self, ex: &'tcx Expr<'tcx>) -> Self::Result {
136+
let Self::Found(cx, local_id) = *self else {
137+
return walk_expr(self, ex);
138+
};
139+
140+
if path_to_local_id(ex, local_id) {
141+
match cx.tcx.parent_hir_node(ex.hir_id) {
142+
Node::Stmt(Stmt {
143+
kind: StmtKind::Semi(_),
144+
..
145+
}) => {},
146+
Node::Expr(expr) if let ExprKind::Field(..) = expr.kind => {},
147+
Node::Expr(expr) if let ExprKind::AddrOf(_, Mutability::Not, _) = expr.kind => {},
148+
Node::Expr(expr)
149+
if let Some(fn_did) = fn_def_id(cx, expr)
150+
&& match_any_def_paths(cx, fn_did, &[&paths::CHILD_ID, &paths::CHILD_KILL]).is_some() => {},
151+
152+
// Conservatively assume that all other kinds of nodes call `.wait()` somehow.
153+
_ => return Break(BreakReason::WaitFound),
154+
}
155+
} else {
156+
match ex.kind {
157+
ExprKind::Ret(..) => return Break(BreakReason::EarlyReturn),
158+
ExprKind::If(cond, then, None) => {
159+
walk_expr(self, cond)?;
160+
161+
// A `wait()` call in an if expression with no else is not enough:
162+
//
163+
// let c = spawn();
164+
// if true {
165+
// c.wait();
166+
// }
167+
//
168+
// This might not call wait(). However, early returns are propagated,
169+
// because they might lead to a later wait() not being called.
170+
if let Break(BreakReason::EarlyReturn) = walk_expr(self, then) {
171+
return Break(BreakReason::EarlyReturn);
172+
}
173+
174+
return Continue(());
175+
},
176+
177+
ExprKind::If(cond, then, Some(else_)) => {
178+
walk_expr(self, cond)?;
179+
180+
#[expect(clippy::unnested_or_patterns)]
181+
match (walk_expr(self, then), walk_expr(self, else_)) {
182+
(Continue(()), Continue(()))
183+
184+
// `wait()` in one branch but nothing in the other does not count
185+
| (Continue(()), Break(BreakReason::WaitFound))
186+
| (Break(BreakReason::WaitFound), Continue(())) => {},
187+
188+
// `wait()` in both branches is ok
189+
(Break(BreakReason::WaitFound), Break(BreakReason::WaitFound)) => {
190+
return Break(BreakReason::WaitFound);
191+
},
192+
193+
// Propagate early returns in either branch
194+
(Break(BreakReason::EarlyReturn), _) | (_, Break(BreakReason::EarlyReturn)) => {
195+
return Break(BreakReason::EarlyReturn);
196+
},
197+
}
198+
199+
return Continue(());
200+
},
201+
_ => {},
202+
}
203+
}
204+
205+
walk_expr(self, ex)
206+
}
207+
}
208+
114209
/// This function has shared logic between the different kinds of nodes that can trigger the lint.
115210
///
116211
/// In particular, `let <binding> = <expr that spawns child>;` requires some custom additional logic
@@ -126,12 +221,10 @@ fn check<'tcx>(cx: &LateContext<'tcx>, spawn_expr: &'tcx Expr<'tcx>, node_id: Hi
126221

127222
let mut vis = ExitPointFinder {
128223
cx,
129-
state: ExitPointState::LookingForSpawnExpr(spawn_expr.hir_id),
224+
state: ExitPointState::WalkUpTo(spawn_expr.hir_id),
130225
};
131-
vis.visit_block(block);
132-
133-
// Visitor found an unconditional `exit()` call, so don't lint.
134-
if let ExitPointState::ExitFound = vis.state {
226+
if let Break(ExitCallFound) = vis.visit_block(block) {
227+
// Visitor found an unconditional `exit()` call, so don't lint.
135228
return;
136229
}
137230

@@ -194,7 +287,7 @@ fn is_exit_expression(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
194287
#[derive(Debug)]
195288
enum ExitPointState {
196289
/// Still walking up to the expression that initiated the visitor.
197-
LookingForSpawnExpr(HirId),
290+
WalkUpTo(HirId),
198291
/// We're inside of a control flow construct (e.g. `if`, `match`, `loop`)
199292
/// Within this, we shouldn't accept any `exit()` calls in here, but we can leave all of these
200293
/// constructs later and still continue looking for an `exit()` call afterwards. Example:
@@ -221,8 +314,6 @@ enum ExitPointState {
221314
InControlFlow { depth: u32 },
222315
/// No exit call found yet, but looking for one.
223316
NoExit,
224-
/// Found an expression that exits the process.
225-
ExitFound,
226317
}
227318

228319
fn expr_enters_control_flow(expr: &Expr<'_>) -> bool {
@@ -234,31 +325,37 @@ struct ExitPointFinder<'a, 'tcx> {
234325
cx: &'a LateContext<'tcx>,
235326
}
236327

328+
struct ExitCallFound;
329+
237330
impl<'a, 'tcx> Visitor<'tcx> for ExitPointFinder<'a, 'tcx> {
238-
fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) {
331+
type Result = ControlFlow<ExitCallFound>;
332+
333+
fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) -> Self::Result {
239334
match self.state {
240-
ExitPointState::LookingForSpawnExpr(id) if expr.hir_id == id => {
335+
ExitPointState::WalkUpTo(id) if expr.hir_id == id => {
241336
self.state = ExitPointState::NoExit;
242-
walk_expr(self, expr);
337+
walk_expr(self, expr)
243338
},
244339
ExitPointState::NoExit if expr_enters_control_flow(expr) => {
245340
self.state = ExitPointState::InControlFlow { depth: 1 };
246-
walk_expr(self, expr);
341+
walk_expr(self, expr)?;
247342
if let ExitPointState::InControlFlow { .. } = self.state {
248343
self.state = ExitPointState::NoExit;
249344
}
345+
Continue(())
250346
},
251-
ExitPointState::NoExit if is_exit_expression(self.cx, expr) => self.state = ExitPointState::ExitFound,
347+
ExitPointState::NoExit if is_exit_expression(self.cx, expr) => Break(ExitCallFound),
252348
ExitPointState::InControlFlow { ref mut depth } if expr_enters_control_flow(expr) => {
253349
*depth += 1;
254-
walk_expr(self, expr);
350+
walk_expr(self, expr)?;
255351
match self.state {
256352
ExitPointState::InControlFlow { depth: 1 } => self.state = ExitPointState::NoExit,
257353
ExitPointState::InControlFlow { ref mut depth } => *depth -= 1,
258354
_ => {},
259355
}
356+
Continue(())
260357
},
261-
_ => {},
358+
_ => Continue(()),
262359
}
263360
}
264361
}

Diff for: tests/ui/zombie_processes.rs

+61
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
#![warn(clippy::zombie_processes)]
2+
#![feature(lint_reasons)]
3+
#![allow(clippy::if_same_then_else, clippy::ifs_same_cond)]
24

35
use std::process::{Child, Command};
46

57
fn main() {
8+
{
9+
// Check that #[expect] works
10+
#[expect(clippy::zombie_processes)]
11+
let mut x = Command::new("").spawn().unwrap();
12+
}
13+
614
{
715
let mut x = Command::new("").spawn().unwrap();
816
//~^ ERROR: spawned process is never `wait()`ed on
@@ -72,6 +80,59 @@ fn main() {
7280
}
7381
}
7482

83+
{
84+
let mut x = { Command::new("").spawn().unwrap() };
85+
x.wait().unwrap();
86+
}
87+
88+
{
89+
struct S {
90+
c: Child,
91+
}
92+
93+
let mut s = S {
94+
c: Command::new("").spawn().unwrap(),
95+
};
96+
s.c.wait().unwrap();
97+
}
98+
99+
{
100+
let mut x = Command::new("").spawn().unwrap();
101+
//~^ ERROR: spawned process is never `wait()`ed on
102+
if true {
103+
return;
104+
}
105+
x.wait().unwrap();
106+
}
107+
108+
{
109+
let mut x = Command::new("").spawn().unwrap();
110+
//~^ ERROR: spawned process is never `wait()`ed on
111+
if true {
112+
x.wait().unwrap();
113+
}
114+
}
115+
116+
{
117+
let mut x = Command::new("").spawn().unwrap();
118+
if true {
119+
x.wait().unwrap();
120+
} else if true {
121+
x.wait().unwrap();
122+
} else {
123+
x.wait().unwrap();
124+
}
125+
}
126+
127+
{
128+
let mut x = Command::new("").spawn().unwrap();
129+
if true {
130+
x.wait().unwrap();
131+
return;
132+
}
133+
x.wait().unwrap();
134+
}
135+
75136
// Checking that it won't lint if spawn is the last statement of a main function.
76137
// IMPORTANT: this case must always be at the very end so it always tests the right thing.
77138
// Don't move this.

0 commit comments

Comments
 (0)