| 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, sym}; | |
| 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; | |
| 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(()), | |
| } | |
| } | |
| } |