|
| 1 | +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, match_any_def_paths, match_def_path, paths}; |
| 4 | +use rustc_ast::Mutability; |
| 5 | +use rustc_hir::{Expr, ExprKind, Node, PatKind, Stmt, StmtKind}; |
| 6 | +use rustc_lint::{LateContext, LateLintPass}; |
| 7 | +use rustc_session::{declare_lint_pass, declare_tool_lint}; |
| 8 | +use rustc_span::Span; |
| 9 | +use std::ops::ControlFlow; |
| 10 | + |
| 11 | +declare_clippy_lint! { |
| 12 | + /// ### What it does |
| 13 | + /// Looks for code that spawns a process but never calls `wait()` on the child. |
| 14 | + /// |
| 15 | + /// ### Why is this bad? |
| 16 | + /// As explained in the [standard library documentation](https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning), |
| 17 | + /// calling `wait()` is necessary on Unix platforms to properly release all OS resources associated with the process. |
| 18 | + /// Not doing so will effectively leak process IDs and/or other limited global resources, |
| 19 | + /// which can eventually lead to resource exhaustion, so it's recommended to call `wait()` in long-running applications. |
| 20 | + /// Such processes are called "zombie processes". |
| 21 | + /// |
| 22 | + /// ### Example |
| 23 | + /// ```rust |
| 24 | + /// use std::process::Command; |
| 25 | + /// |
| 26 | + /// let _child = Command::new("ls").spawn().expect("failed to execute child"); |
| 27 | + /// ``` |
| 28 | + /// Use instead: |
| 29 | + /// ```rust |
| 30 | + /// use std::process::Command; |
| 31 | + /// |
| 32 | + /// let mut child = Command::new("ls").spawn().expect("failed to execute child"); |
| 33 | + /// child.wait().expect("failed to wait on child"); |
| 34 | + /// ``` |
| 35 | + #[clippy::version = "1.74.0"] |
| 36 | + pub ZOMBIE_PROCESSES, |
| 37 | + suspicious, |
| 38 | + "not waiting on a spawned child process" |
| 39 | +} |
| 40 | +declare_lint_pass!(ZombieProcesses => [ZOMBIE_PROCESSES]); |
| 41 | + |
| 42 | +fn emit_lint(cx: &LateContext<'_>, span: Span) { |
| 43 | + span_lint_and_then( |
| 44 | + cx, |
| 45 | + ZOMBIE_PROCESSES, |
| 46 | + span, |
| 47 | + "spawned process is never `wait()`-ed on and leaves behind a zombie process", |
| 48 | + |diag| { |
| 49 | + diag.help("consider calling `.wait()`") |
| 50 | + .note("also see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning"); |
| 51 | + }, |
| 52 | + ); |
| 53 | +} |
| 54 | + |
| 55 | +impl<'tcx> LateLintPass<'tcx> for ZombieProcesses { |
| 56 | + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { |
| 57 | + if let ExprKind::Call(..) | ExprKind::MethodCall(..) = expr.kind |
| 58 | + && let Some(child_adt) = cx.typeck_results().expr_ty(expr).ty_adt_def() |
| 59 | + && match_def_path(cx, child_adt.did(), &paths::CHILD) |
| 60 | + { |
| 61 | + match cx.tcx.hir().get_parent(expr.hir_id) { |
| 62 | + Node::Local(local) if let PatKind::Binding(_, local_id, ..) = local.pat.kind => { |
| 63 | + |
| 64 | + // If the `Child` is assigned to a variable, we want to check if the code never calls `.wait()` |
| 65 | + // on the variable, and lint if not. |
| 66 | + // This is difficult to do because expressions can be arbitrarily complex |
| 67 | + // and the variable can "escape" in various ways, e.g. you can take a `&mut` to the variable |
| 68 | + // and call `.wait()` through that, or pass it to another function... |
| 69 | + // So instead we do the inverse, checking if all uses are either: |
| 70 | + // - a field access (`child.{stderr,stdin,stdout}`) |
| 71 | + // - calling `id` or `kill` |
| 72 | + // - no use at all (e.g. `let _x = child;`) |
| 73 | + // - taking a shared reference (`&`), `wait()` can't go through that |
| 74 | + // Neither of these is sufficient to prevent zombie processes |
| 75 | + // Doing it like this means more FNs, but FNs are better than FPs. |
| 76 | + let has_no_wait = for_each_local_use_after_expr(cx, local_id, expr.hir_id, |expr| { |
| 77 | + match cx.tcx.hir().get_parent(expr.hir_id) { |
| 78 | + Node::Stmt(Stmt { kind: StmtKind::Semi(_), .. }) => ControlFlow::Continue(()), |
| 79 | + Node::Expr(expr) if let ExprKind::Field(..) = expr.kind => ControlFlow::Continue(()), |
| 80 | + Node::Expr(expr) if let ExprKind::AddrOf(_, Mutability::Not, _) = expr.kind => { |
| 81 | + ControlFlow::Continue(()) |
| 82 | + } |
| 83 | + Node::Expr(expr) |
| 84 | + if let Some(fn_did) = fn_def_id(cx, expr) |
| 85 | + && match_any_def_paths(cx, fn_did, &[ |
| 86 | + &paths::CHILD_ID, |
| 87 | + &paths::CHILD_KILL, |
| 88 | + ]).is_some() => |
| 89 | + { |
| 90 | + ControlFlow::Continue(()) |
| 91 | + } |
| 92 | + |
| 93 | + // Conservatively assume that all other kinds of nodes call `.wait()` somehow. |
| 94 | + _ => ControlFlow::Break(()), |
| 95 | + } |
| 96 | + }).is_continue(); |
| 97 | + |
| 98 | + if has_no_wait { |
| 99 | + emit_lint(cx, expr.span); |
| 100 | + } |
| 101 | + }, |
| 102 | + Node::Local(local) if let PatKind::Wild = local.pat.kind => { |
| 103 | + // `let _ = child;`, also dropped immediately without `wait()`ing |
| 104 | + emit_lint(cx, expr.span); |
| 105 | + } |
| 106 | + Node::Stmt(Stmt { kind: StmtKind::Semi(_), .. }) => { |
| 107 | + // Immediately dropped. E.g. `std::process::Command::new("echo").spawn().unwrap();` |
| 108 | + emit_lint(cx, expr.span); |
| 109 | + } |
| 110 | + _ => {} |
| 111 | + } |
| 112 | + } |
| 113 | + } |
| 114 | +} |
0 commit comments