blob: 0319f3e656e1b778315dcd390c40d69b7486f35d [file] [log] [blame] [edit]
use ControlFlow::{Break, Continue};
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::res::{MaybeDef, MaybeResPath};
use clippy_utils::{fn_def_id, get_enclosing_block};
use rustc_ast::Mutability;
use rustc_ast::visit::visit_opt;
use rustc_errors::Applicability;
use rustc_hir::def_id::LocalDefId;
use rustc_hir::intravisit::{Visitor, walk_block, walk_expr};
use rustc_hir::{Expr, ExprKind, HirId, LetStmt, Node, PatKind, Stmt, StmtKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::hir::nested_filter;
use rustc_session::declare_lint_pass;
use rustc_span::{Span, sym};
use std::ops::ControlFlow;
declare_clippy_lint! {
/// ### What it does
/// Looks for code that spawns a process but never calls `wait()` on the child.
///
/// ### Why is this bad?
/// As explained in the [standard library documentation](https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning),
/// calling `wait()` is necessary on Unix platforms to properly release all OS resources associated with the process.
/// Not doing so will effectively leak process IDs and/or other limited global resources,
/// which can eventually lead to resource exhaustion, so it's recommended to call `wait()` in long-running applications.
/// Such processes are called "zombie processes".
///
/// To reduce the rate of false positives, if the spawned process is assigned to a binding, the lint actually works the other way around; it
/// conservatively checks that all uses of a variable definitely don't call `wait()` and only then emits a warning.
/// For that reason, a seemingly unrelated use can get called out as calling `wait()` in help messages.
///
/// ### Control flow
/// If a `wait()` call exists in an if/then block but not in the else block (or there is no else block),
/// then this still gets linted as not calling `wait()` in all code paths.
/// Likewise, when early-returning from the function, `wait()` calls that appear after the return expression
/// are also not accepted.
/// In other words, the `wait()` call must be unconditionally reachable after the spawn expression.
///
/// ### Example
/// ```rust
/// use std::process::Command;
///
/// let _child = Command::new("ls").spawn().expect("failed to execute child");
/// ```
/// Use instead:
/// ```rust
/// use std::process::Command;
///
/// let mut child = Command::new("ls").spawn().expect("failed to execute child");
/// child.wait().expect("failed to wait on child");
/// ```
#[clippy::version = "1.83.0"]
pub ZOMBIE_PROCESSES,
suspicious,
"not waiting on a spawned child process"
}
declare_lint_pass!(ZombieProcesses => [ZOMBIE_PROCESSES]);
impl<'tcx> LateLintPass<'tcx> for ZombieProcesses {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
if let ExprKind::Call(..) | ExprKind::MethodCall(..) = expr.kind
&& let child_ty = cx.typeck_results().expr_ty(expr)
&& child_ty.is_diag_item(cx, sym::Child)
{
match cx.tcx.parent_hir_node(expr.hir_id) {
Node::LetStmt(local)
if let PatKind::Binding(_, local_id, ..) = local.pat.kind
&& let Some(enclosing_block) = get_enclosing_block(cx, expr.hir_id) =>
{
let mut vis = WaitFinder {
cx,
local_id,
create_id: expr.hir_id,
body_id: cx.tcx.hir_enclosing_body_owner(expr.hir_id),
state: VisitorState::WalkUpToCreate,
early_return: None,
missing_wait_branch: None,
};
let res = (
walk_block(&mut vis, enclosing_block),
vis.missing_wait_branch,
vis.early_return,
);
let cause = match res {
(Break(MaybeWait(wait_span)), _, Some(return_span)) => {
Cause::EarlyReturn { wait_span, return_span }
},
(Break(MaybeWait(_)), _, None) => return,
(Continue(()), None, _) => Cause::NeverWait,
(Continue(()), Some(MissingWaitBranch::MissingElse { if_span, wait_span }), _) => {
Cause::MissingElse { wait_span, if_span }
},
(Continue(()), Some(MissingWaitBranch::MissingWaitInBranch { branch_span, wait_span }), _) => {
Cause::MissingWaitInBranch { wait_span, branch_span }
},
};
// Don't emit a suggestion since the binding is used later
check(cx, expr, cause, false);
},
Node::LetStmt(&LetStmt { pat, .. }) if let PatKind::Wild = pat.kind => {
// `let _ = child;`, also dropped immediately without `wait()`ing
check(cx, expr, Cause::NeverWait, true);
},
Node::Stmt(&Stmt {
kind: StmtKind::Semi(_),
..
}) => {
// Immediately dropped. E.g. `std::process::Command::new("echo").spawn().unwrap();`
check(cx, expr, Cause::NeverWait, true);
},
_ => {},
}
}
}
}
struct MaybeWait(Span);
/// A visitor responsible for finding a `wait()` call on a local variable.
///
/// Note that this visitor does NOT explicitly look for `wait()` calls directly, but rather does the
/// inverse -- checking if all uses of the local are either:
/// - a field access (`child.{stderr,stdin,stdout}`)
/// - calling `id` or `kill`
/// - no use at all (e.g. `let _x = child;`)
/// - taking a shared reference (`&`), `wait()` can't go through that
///
/// None of these are sufficient to prevent zombie processes.
/// Doing it like this means more FNs, but FNs are better than FPs.
struct WaitFinder<'a, 'tcx> {
cx: &'a LateContext<'tcx>,
local_id: HirId,
create_id: HirId,
body_id: LocalDefId,
state: VisitorState,
early_return: Option<Span>,
// When joining two if branches where one of them doesn't call `wait()`, stores its span for more targeted help
// messages
missing_wait_branch: Option<MissingWaitBranch>,
}
#[derive(PartialEq)]
enum VisitorState {
WalkUpToCreate,
CreateFound,
}
#[derive(Copy, Clone)]
enum MissingWaitBranch {
MissingElse { if_span: Span, wait_span: Span },
MissingWaitInBranch { branch_span: Span, wait_span: Span },
}
impl<'tcx> Visitor<'tcx> for WaitFinder<'_, 'tcx> {
type NestedFilter = nested_filter::OnlyBodies;
type Result = ControlFlow<MaybeWait>;
fn visit_expr(&mut self, ex: &'tcx Expr<'tcx>) -> Self::Result {
if ex.hir_id == self.create_id {
self.state = VisitorState::CreateFound;
return Continue(());
}
if self.state != VisitorState::CreateFound {
return walk_expr(self, ex);
}
if ex.res_local_id() == Some(self.local_id) {
match self.cx.tcx.parent_hir_node(ex.hir_id) {
Node::Stmt(Stmt {
kind: StmtKind::Semi(_),
..
}) => {},
Node::Expr(expr) if let ExprKind::Field(..) = expr.kind => {},
Node::Expr(expr) if let ExprKind::AddrOf(_, Mutability::Not, _) = expr.kind => {},
Node::Expr(expr)
if let Some(fn_did) = fn_def_id(self.cx, expr)
&& let Some(fn_name) = self.cx.tcx.get_diagnostic_name(fn_did)
&& matches!(fn_name, sym::child_id | sym::child_kill) => {},
// Conservatively assume that all other kinds of nodes call `.wait()` somehow.
_ => return Break(MaybeWait(ex.span)),
}
} else {
match ex.kind {
ExprKind::Ret(e) if self.cx.tcx.hir_enclosing_body_owner(ex.hir_id) == self.body_id => {
visit_opt!(self, visit_expr, e);
if self.early_return.is_none() {
self.early_return = Some(ex.span);
}
return Continue(());
},
ExprKind::If(cond, then, None) => {
walk_expr(self, cond)?;
if let Break(MaybeWait(wait_span)) = walk_expr(self, then)
&& self.missing_wait_branch.is_none()
{
self.missing_wait_branch = Some(MissingWaitBranch::MissingElse {
if_span: ex.span,
wait_span,
});
}
return Continue(());
},
ExprKind::If(cond, then, Some(else_)) => {
walk_expr(self, cond)?;
match (walk_expr(self, then), walk_expr(self, else_)) {
(Continue(()), Continue(())) => {},
// `wait()` in one branch but nothing in the other does not count
(Continue(()), Break(MaybeWait(wait_span))) => {
if self.missing_wait_branch.is_none() {
self.missing_wait_branch = Some(MissingWaitBranch::MissingWaitInBranch {
branch_span: ex.span.shrink_to_lo().to(then.span),
wait_span,
});
}
},
(Break(MaybeWait(wait_span)), Continue(())) => {
if self.missing_wait_branch.is_none() {
self.missing_wait_branch = Some(MissingWaitBranch::MissingWaitInBranch {
branch_span: then.span.shrink_to_hi().to(else_.span),
wait_span,
});
}
},
// `wait()` in both branches is ok
(Break(MaybeWait(wait_span)), Break(MaybeWait(_))) => {
self.missing_wait_branch = None;
return Break(MaybeWait(wait_span));
},
}
return Continue(());
},
_ => {},
}
}
walk_expr(self, ex)
}
fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt {
self.cx.tcx
}
}
#[derive(Copy, Clone)]
enum Cause {
/// No call to `wait()` at all
NeverWait,
/// `wait()` call exists, but not all code paths definitely lead to one due to
/// an early return
EarlyReturn { wait_span: Span, return_span: Span },
/// `wait()` call exists in some if branches but not this one
MissingWaitInBranch { wait_span: Span, branch_span: Span },
/// `wait()` call exists in an if/then branch but it is missing an else block
MissingElse { wait_span: Span, if_span: Span },
}
impl Cause {
fn message(self) -> &'static str {
match self {
Cause::NeverWait => "spawned process is never `wait()`ed on",
Cause::EarlyReturn { .. } | Cause::MissingWaitInBranch { .. } | Cause::MissingElse { .. } => {
"spawned process is not `wait()`ed on in all code paths"
},
}
}
fn fallback_help(self) -> &'static str {
match self {
Cause::NeverWait => "consider calling `.wait()`",
Cause::EarlyReturn { .. } | Cause::MissingWaitInBranch { .. } | Cause::MissingElse { .. } => {
"consider calling `.wait()` in all code paths"
},
}
}
}
/// This function has shared logic between the different kinds of nodes that can trigger the lint.
///
/// In particular, `let <binding> = <expr that spawns child>;` requires some custom additional logic
/// such as checking that the binding is not used in certain ways, which isn't necessary for
/// `let _ = <expr that spawns child>;`.
///
/// This checks if the program doesn't unconditionally exit after the spawn expression.
fn check<'tcx>(cx: &LateContext<'tcx>, spawn_expr: &'tcx Expr<'tcx>, cause: Cause, emit_suggestion: bool) {
let Some(block) = get_enclosing_block(cx, spawn_expr.hir_id) else {
return;
};
let mut vis = ExitPointFinder {
state: ExitPointState::WalkUpTo(spawn_expr.hir_id),
cx,
};
if let Break(ExitCallFound) = vis.visit_block(block) {
// Visitor found an unconditional `exit()` call, so don't lint.
return;
}
span_lint_and_then(cx, ZOMBIE_PROCESSES, spawn_expr.span, cause.message(), |diag| {
match cause {
Cause::EarlyReturn { wait_span, return_span } => {
diag.span_note(
return_span,
"no `wait()` call exists on the code path to this early return",
);
diag.span_note(
wait_span,
"`wait()` call exists, but it is unreachable due to the early return",
);
},
Cause::MissingWaitInBranch { wait_span, branch_span } => {
diag.span_note(branch_span, "`wait()` is not called in this if branch");
diag.span_note(wait_span, "`wait()` is called in the other branch");
},
Cause::MissingElse { if_span, wait_span } => {
diag.span_note(
if_span,
"this if expression has a `wait()` call, but it is missing an else block",
);
diag.span_note(wait_span, "`wait()` called here");
},
Cause::NeverWait => {},
}
if emit_suggestion {
diag.span_suggestion(
spawn_expr.span.shrink_to_hi(),
"try",
".wait()",
Applicability::MaybeIncorrect,
);
} else {
diag.help(cause.fallback_help());
}
diag.note("not doing so might leave behind zombie processes")
.note("see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning");
});
}
/// Checks if the given expression exits the process.
fn is_exit_expression(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
if let Some(fn_did) = fn_def_id(cx, expr)
&& let Some(fn_name) = cx.tcx.get_diagnostic_name(fn_did)
&& matches!(fn_name, sym::process_exit | sym::process_abort)
{
true
} else {
false
}
}
#[derive(Debug)]
enum ExitPointState {
/// Still walking up to the expression that initiated the visitor.
WalkUpTo(HirId),
/// We're inside of a control flow construct (e.g. `if`, `match`, `loop`)
/// Within this, we shouldn't accept any `exit()` calls in here, but we can leave all of these
/// constructs later and still continue looking for an `exit()` call afterwards. Example:
/// ```ignore
/// Command::new("").spawn().unwrap();
///
/// if true { // depth=1
/// if true { // depth=2
/// match () { // depth=3
/// () => loop { // depth=4
///
/// std::process::exit();
/// ^^^^^^^^^^^^^^^^^^^^^ conditional exit call, ignored
///
/// } // depth=3
/// } // depth=2
/// } // depth=1
/// } // depth=0
///
/// std::process::exit();
/// ^^^^^^^^^^^^^^^^^^^^^ this exit call is accepted because we're now unconditionally calling it
/// ```
/// We can only get into this state from `NoExit`.
InControlFlow { depth: u32 },
/// No exit call found yet, but looking for one.
NoExit,
}
fn expr_enters_control_flow(expr: &Expr<'_>) -> bool {
matches!(expr.kind, ExprKind::If(..) | ExprKind::Match(..) | ExprKind::Loop(..))
}
struct ExitPointFinder<'a, 'tcx> {
state: ExitPointState,
cx: &'a LateContext<'tcx>,
}
struct ExitCallFound;
impl<'tcx> Visitor<'tcx> for ExitPointFinder<'_, 'tcx> {
type Result = ControlFlow<ExitCallFound>;
fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) -> Self::Result {
match self.state {
ExitPointState::WalkUpTo(id) if expr.hir_id == id => {
self.state = ExitPointState::NoExit;
walk_expr(self, expr)
},
ExitPointState::NoExit if expr_enters_control_flow(expr) => {
self.state = ExitPointState::InControlFlow { depth: 1 };
walk_expr(self, expr)?;
if let ExitPointState::InControlFlow { .. } = self.state {
self.state = ExitPointState::NoExit;
}
Continue(())
},
ExitPointState::NoExit if is_exit_expression(self.cx, expr) => Break(ExitCallFound),
ExitPointState::InControlFlow { ref mut depth } if expr_enters_control_flow(expr) => {
*depth += 1;
walk_expr(self, expr)?;
match self.state {
ExitPointState::InControlFlow { depth: 1 } => self.state = ExitPointState::NoExit,
ExitPointState::InControlFlow { ref mut depth } => *depth -= 1,
_ => {},
}
Continue(())
},
_ => Continue(()),
}
}
}