|  | 
|  | 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