about summary refs log tree commit diff
diff options
context:
space:
mode:
authory21 <30553356+y21@users.noreply.github.com>2023-09-10 16:14:20 +0200
committery21 <30553356+y21@users.noreply.github.com>2024-08-27 21:51:02 +0200
commite8ac4ea4187498052849531b86114a1eec5314a1 (patch)
treeafe755b7b066707901e93775f69ea71c86f087fc
parent603d5a19c9e768f7fcb4775dbb57200b563350ee (diff)
downloadrust-e8ac4ea4187498052849531b86114a1eec5314a1.tar.gz
rust-e8ac4ea4187498052849531b86114a1eec5314a1.zip
new lint: `zombie_processes`
-rw-r--r--CHANGELOG.md1
-rw-r--r--clippy_dev/src/serve.rs3
-rw-r--r--clippy_lints/src/declared_lints.rs1
-rw-r--r--clippy_lints/src/lib.rs2
-rw-r--r--clippy_lints/src/zombie_processes.rs334
-rw-r--r--clippy_utils/src/paths.rs4
-rw-r--r--tests/ui/suspicious_command_arg_space.fixed1
-rw-r--r--tests/ui/suspicious_command_arg_space.rs1
-rw-r--r--tests/ui/suspicious_command_arg_space.stderr4
-rw-r--r--tests/ui/zombie_processes.rs138
-rw-r--r--tests/ui/zombie_processes.stderr64
-rw-r--r--tests/ui/zombie_processes_fixable.fixed26
-rw-r--r--tests/ui/zombie_processes_fixable.rs26
-rw-r--r--tests/ui/zombie_processes_fixable.stderr40
14 files changed, 642 insertions, 3 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c5d7991e073..21252a3f43a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6048,6 +6048,7 @@ Released 2018-09-13
 [`zero_repeat_side_effects`]: https://rust-lang.github.io/rust-clippy/master/index.html#zero_repeat_side_effects
 [`zero_sized_map_values`]: https://rust-lang.github.io/rust-clippy/master/index.html#zero_sized_map_values
 [`zero_width_space`]: https://rust-lang.github.io/rust-clippy/master/index.html#zero_width_space
+[`zombie_processes`]: https://rust-lang.github.io/rust-clippy/master/index.html#zombie_processes
 [`zst_offset`]: https://rust-lang.github.io/rust-clippy/master/index.html#zst_offset
 <!-- end autogenerated links to lint list -->
 <!-- begin autogenerated links to configuration documentation -->
diff --git a/clippy_dev/src/serve.rs b/clippy_dev/src/serve.rs
index 19560b31fd3..cc14cd8dae6 100644
--- a/clippy_dev/src/serve.rs
+++ b/clippy_dev/src/serve.rs
@@ -29,7 +29,7 @@ pub fn run(port: u16, lint: Option<String>) -> ! {
         }
         if let Some(url) = url.take() {
             thread::spawn(move || {
-                Command::new(PYTHON)
+                let mut child = Command::new(PYTHON)
                     .arg("-m")
                     .arg("http.server")
                     .arg(port.to_string())
@@ -40,6 +40,7 @@ pub fn run(port: u16, lint: Option<String>) -> ! {
                 thread::sleep(Duration::from_millis(500));
                 // Launch browser after first export.py has completed and http.server is up
                 let _result = opener::open(url);
+                child.wait().unwrap();
             });
         }
         thread::sleep(Duration::from_millis(1000));
diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs
index 94da5752beb..10e041fbc09 100644
--- a/clippy_lints/src/declared_lints.rs
+++ b/clippy_lints/src/declared_lints.rs
@@ -768,4 +768,5 @@ pub static LINTS: &[&crate::LintInfo] = &[
     crate::zero_div_zero::ZERO_DIVIDED_BY_ZERO_INFO,
     crate::zero_repeat_side_effects::ZERO_REPEAT_SIDE_EFFECTS_INFO,
     crate::zero_sized_map_values::ZERO_SIZED_MAP_VALUES_INFO,
+    crate::zombie_processes::ZOMBIE_PROCESSES_INFO,
 ];
diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs
index 2ac06b360be..f24933683b1 100644
--- a/clippy_lints/src/lib.rs
+++ b/clippy_lints/src/lib.rs
@@ -386,6 +386,7 @@ mod write;
 mod zero_div_zero;
 mod zero_repeat_side_effects;
 mod zero_sized_map_values;
+mod zombie_processes;
 // end lints modules, do not remove this comment, it’s used in `update_lints`
 
 use clippy_config::{get_configuration_metadata, Conf};
@@ -933,5 +934,6 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
     store.register_late_pass(|_| Box::new(set_contains_or_insert::SetContainsOrInsert));
     store.register_early_pass(|| Box::new(byte_char_slices::ByteCharSlice));
     store.register_early_pass(|| Box::new(cfg_not_test::CfgNotTest));
+    store.register_late_pass(|_| Box::new(zombie_processes::ZombieProcesses));
     // add lints here, do not remove this comment, it's used in `new_lint`
 }
diff --git a/clippy_lints/src/zombie_processes.rs b/clippy_lints/src/zombie_processes.rs
new file mode 100644
index 00000000000..eda3d7820c1
--- /dev/null
+++ b/clippy_lints/src/zombie_processes.rs
@@ -0,0 +1,334 @@
+use clippy_utils::diagnostics::span_lint_and_then;
+use clippy_utils::{fn_def_id, get_enclosing_block, match_any_def_paths, match_def_path, path_to_local_id, paths};
+use rustc_ast::Mutability;
+use rustc_errors::Applicability;
+use rustc_hir::intravisit::{walk_block, walk_expr, walk_local, Visitor};
+use rustc_hir::{Expr, ExprKind, HirId, LetStmt, Node, PatKind, Stmt, StmtKind};
+use rustc_lint::{LateContext, LateLintPass};
+use rustc_session::declare_lint_pass;
+use rustc_span::sym;
+use std::ops::ControlFlow;
+use ControlFlow::{Break, Continue};
+
+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".
+    ///
+    /// ### 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.74.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 Some(child_adt) = cx.typeck_results().expr_ty(expr).ty_adt_def()
+            && match_def_path(cx, child_adt.did(), &paths::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::WalkUpTo(cx, local_id);
+
+                    // If it does have a `wait()` call, we're done. Don't lint.
+                    if let Break(BreakReason::WaitFound) = walk_block(&mut vis, enclosing_block) {
+                        return;
+                    }
+
+                    // Don't emit a suggestion since the binding is used later
+                    check(cx, expr, false);
+                },
+                Node::LetStmt(&LetStmt { pat, .. }) if let PatKind::Wild = pat.kind => {
+                    // `let _ = child;`, also dropped immediately without `wait()`ing
+                    check(cx, expr, true);
+                },
+                Node::Stmt(&Stmt {
+                    kind: StmtKind::Semi(_),
+                    ..
+                }) => {
+                    // Immediately dropped. E.g. `std::process::Command::new("echo").spawn().unwrap();`
+                    check(cx, expr, true);
+                },
+                _ => {},
+            }
+        }
+    }
+}
+
+enum BreakReason {
+    WaitFound,
+    EarlyReturn,
+}
+
+/// A visitor responsible for finding a `wait()` call on a local variable.
+///
+/// Conditional `wait()` calls are assumed to not call wait:
+/// ```ignore
+/// let mut c = Command::new("").spawn().unwrap();
+/// if true {
+///     c.wait();
+/// }
+/// ```
+///
+/// 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.
+///
+/// `return` expressions, conditional or not, short-circuit the visitor because
+/// if a `wait()` call hadn't been found at that point, it might never reach one at a later point:
+/// ```ignore
+/// let mut c = Command::new("").spawn().unwrap();
+/// if true {
+///     return; // Break(BreakReason::EarlyReturn)
+/// }
+/// c.wait(); // this might not be reachable
+/// ```
+enum WaitFinder<'a, 'tcx> {
+    WalkUpTo(&'a LateContext<'tcx>, HirId),
+    Found(&'a LateContext<'tcx>, HirId),
+}
+
+impl<'a, 'tcx> Visitor<'tcx> for WaitFinder<'a, 'tcx> {
+    type Result = ControlFlow<BreakReason>;
+
+    fn visit_local(&mut self, l: &'tcx LetStmt<'tcx>) -> Self::Result {
+        if let Self::WalkUpTo(cx, local_id) = *self
+            && let PatKind::Binding(_, pat_id, ..) = l.pat.kind
+            && local_id == pat_id
+        {
+            *self = Self::Found(cx, local_id);
+        }
+
+        walk_local(self, l)
+    }
+
+    fn visit_expr(&mut self, ex: &'tcx Expr<'tcx>) -> Self::Result {
+        let Self::Found(cx, local_id) = *self else {
+            return walk_expr(self, ex);
+        };
+
+        if path_to_local_id(ex, local_id) {
+            match 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(cx, expr)
+                        && match_any_def_paths(cx, fn_did, &[&paths::CHILD_ID, &paths::CHILD_KILL]).is_some() => {},
+
+                // Conservatively assume that all other kinds of nodes call `.wait()` somehow.
+                _ => return Break(BreakReason::WaitFound),
+            }
+        } else {
+            match ex.kind {
+                ExprKind::Ret(..) => return Break(BreakReason::EarlyReturn),
+                ExprKind::If(cond, then, None) => {
+                    walk_expr(self, cond)?;
+
+                    // A `wait()` call in an if expression with no else is not enough:
+                    //
+                    // let c = spawn();
+                    // if true {
+                    //   c.wait();
+                    // }
+                    //
+                    // This might not call wait(). However, early returns are propagated,
+                    // because they might lead to a later wait() not being called.
+                    if let Break(BreakReason::EarlyReturn) = walk_expr(self, then) {
+                        return Break(BreakReason::EarlyReturn);
+                    }
+
+                    return Continue(());
+                },
+
+                ExprKind::If(cond, then, Some(else_)) => {
+                    walk_expr(self, cond)?;
+
+                    #[expect(clippy::unnested_or_patterns)]
+                    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(BreakReason::WaitFound))
+                        | (Break(BreakReason::WaitFound), Continue(())) => {},
+
+                        // `wait()` in both branches is ok
+                        (Break(BreakReason::WaitFound), Break(BreakReason::WaitFound)) => {
+                            return Break(BreakReason::WaitFound);
+                        },
+
+                        // Propagate early returns in either branch
+                        (Break(BreakReason::EarlyReturn), _) | (_, Break(BreakReason::EarlyReturn)) => {
+                            return Break(BreakReason::EarlyReturn);
+                        },
+                    }
+
+                    return Continue(());
+                },
+                _ => {},
+            }
+        }
+
+        walk_expr(self, ex)
+    }
+}
+
+/// 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>, emit_suggestion: bool) {
+    let Some(block) = get_enclosing_block(cx, spawn_expr.hir_id) else {
+        return;
+    };
+
+    let mut vis = ExitPointFinder {
+        cx,
+        state: ExitPointState::WalkUpTo(spawn_expr.hir_id),
+    };
+    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,
+        "spawned process is never `wait()`ed on",
+        |diag| {
+            if emit_suggestion {
+                diag.span_suggestion(
+                    spawn_expr.span.shrink_to_hi(),
+                    "try",
+                    ".wait()",
+                    Applicability::MaybeIncorrect,
+                );
+            } else {
+                diag.note("consider calling `.wait()`");
+            }
+
+            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 {
+    fn_def_id(cx, expr).is_some_and(|fn_did| {
+        cx.tcx.is_diagnostic_item(sym::process_exit, fn_did) || match_def_path(cx, fn_did, &paths::ABORT)
+    })
+}
+
+#[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<'a, 'tcx> Visitor<'tcx> for ExitPointFinder<'a, '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(()),
+        }
+    }
+}
diff --git a/clippy_utils/src/paths.rs b/clippy_utils/src/paths.rs
index a767798a9c3..930fb9fb6f2 100644
--- a/clippy_utils/src/paths.rs
+++ b/clippy_utils/src/paths.rs
@@ -4,6 +4,7 @@
 //! Whenever possible, please consider diagnostic items over hardcoded paths.
 //! See <https://github.com/rust-lang/rust-clippy/issues/5393> for more information.
 
+pub const ABORT: [&str; 3] = ["std", "process", "abort"];
 pub const APPLICABILITY: [&str; 2] = ["rustc_lint_defs", "Applicability"];
 pub const APPLICABILITY_VALUES: [[&str; 3]; 4] = [
     ["rustc_lint_defs", "Applicability", "Unspecified"],
@@ -23,6 +24,9 @@ pub const CORE_RESULT_OK_METHOD: [&str; 4] = ["core", "result", "Result", "ok"];
 pub const CSTRING_AS_C_STR: [&str; 5] = ["alloc", "ffi", "c_str", "CString", "as_c_str"];
 pub const EARLY_CONTEXT: [&str; 2] = ["rustc_lint", "EarlyContext"];
 pub const EARLY_LINT_PASS: [&str; 3] = ["rustc_lint", "passes", "EarlyLintPass"];
+pub const CHILD: [&str; 3] = ["std", "process", "Child"];
+pub const CHILD_ID: [&str; 4] = ["std", "process", "Child", "id"];
+pub const CHILD_KILL: [&str; 4] = ["std", "process", "Child", "kill"];
 pub const F32_EPSILON: [&str; 4] = ["core", "f32", "<impl f32>", "EPSILON"];
 pub const F64_EPSILON: [&str; 4] = ["core", "f64", "<impl f64>", "EPSILON"];
 pub const FILE_OPTIONS: [&str; 4] = ["std", "fs", "File", "options"];
diff --git a/tests/ui/suspicious_command_arg_space.fixed b/tests/ui/suspicious_command_arg_space.fixed
index 5d7b1e0c17f..704d6ea1bb8 100644
--- a/tests/ui/suspicious_command_arg_space.fixed
+++ b/tests/ui/suspicious_command_arg_space.fixed
@@ -1,3 +1,4 @@
+#![allow(clippy::zombie_processes)]
 fn main() {
     // Things it should warn about:
     std::process::Command::new("echo").args(["-n", "hello"]).spawn().unwrap();
diff --git a/tests/ui/suspicious_command_arg_space.rs b/tests/ui/suspicious_command_arg_space.rs
index 8abd9803a0c..2a2a7557381 100644
--- a/tests/ui/suspicious_command_arg_space.rs
+++ b/tests/ui/suspicious_command_arg_space.rs
@@ -1,3 +1,4 @@
+#![allow(clippy::zombie_processes)]
 fn main() {
     // Things it should warn about:
     std::process::Command::new("echo").arg("-n hello").spawn().unwrap();
diff --git a/tests/ui/suspicious_command_arg_space.stderr b/tests/ui/suspicious_command_arg_space.stderr
index d2517b66b56..6fd07d07d7b 100644
--- a/tests/ui/suspicious_command_arg_space.stderr
+++ b/tests/ui/suspicious_command_arg_space.stderr
@@ -1,5 +1,5 @@
 error: single argument that looks like it should be multiple arguments
-  --> tests/ui/suspicious_command_arg_space.rs:3:44
+  --> tests/ui/suspicious_command_arg_space.rs:4:44
    |
 LL |     std::process::Command::new("echo").arg("-n hello").spawn().unwrap();
    |                                            ^^^^^^^^^^
@@ -12,7 +12,7 @@ LL |     std::process::Command::new("echo").args(["-n", "hello"]).spawn().unwrap
    |                                        ~~~~ ~~~~~~~~~~~~~~~
 
 error: single argument that looks like it should be multiple arguments
-  --> tests/ui/suspicious_command_arg_space.rs:6:43
+  --> tests/ui/suspicious_command_arg_space.rs:7:43
    |
 LL |     std::process::Command::new("cat").arg("--number file").spawn().unwrap();
    |                                           ^^^^^^^^^^^^^^^
diff --git a/tests/ui/zombie_processes.rs b/tests/ui/zombie_processes.rs
new file mode 100644
index 00000000000..a2abc7fc3a1
--- /dev/null
+++ b/tests/ui/zombie_processes.rs
@@ -0,0 +1,138 @@
+#![warn(clippy::zombie_processes)]
+#![allow(clippy::if_same_then_else, clippy::ifs_same_cond)]
+
+use std::process::{Child, Command};
+
+fn main() {
+    {
+        // Check that #[expect] works
+        #[expect(clippy::zombie_processes)]
+        let mut x = Command::new("").spawn().unwrap();
+    }
+
+    {
+        let mut x = Command::new("").spawn().unwrap();
+        //~^ ERROR: spawned process is never `wait()`ed on
+        x.kill();
+        x.id();
+    }
+    {
+        let mut x = Command::new("").spawn().unwrap();
+        x.wait().unwrap(); // OK
+    }
+    {
+        let x = Command::new("").spawn().unwrap();
+        x.wait_with_output().unwrap(); // OK
+    }
+    {
+        let mut x = Command::new("").spawn().unwrap();
+        x.try_wait().unwrap(); // OK
+    }
+    {
+        let mut x = Command::new("").spawn().unwrap();
+        let mut r = &mut x;
+        r.wait().unwrap(); // OK, not calling `.wait()` directly on `x` but through `r` -> `x`
+    }
+    {
+        let mut x = Command::new("").spawn().unwrap();
+        process_child(x); // OK, other function might call `.wait()` so assume it does
+    }
+    {
+        let mut x = Command::new("").spawn().unwrap();
+        //~^ ERROR: spawned process is never `wait()`ed on
+        let v = &x;
+        // (allow shared refs is fine because one cannot call `.wait()` through that)
+    }
+
+    // https://github.com/rust-lang/rust-clippy/pull/11476#issuecomment-1718456033
+    // Unconditionally exiting the process in various ways (should not lint)
+    {
+        let mut x = Command::new("").spawn().unwrap();
+        std::process::exit(0);
+    }
+    {
+        let mut x = Command::new("").spawn().unwrap();
+        std::process::abort(); // same as above, but abort instead of exit
+    }
+    {
+        let mut x = Command::new("").spawn().unwrap();
+        if true { /* nothing */ }
+        std::process::abort(); // still unconditionally exits
+    }
+
+    // Conditionally exiting
+    // It should assume that it might not exit and still lint
+    {
+        let mut x = Command::new("").spawn().unwrap();
+        //~^ ERROR: spawned process is never `wait()`ed on
+        if true {
+            std::process::exit(0);
+        }
+    }
+    {
+        let mut x = Command::new("").spawn().unwrap();
+        //~^ ERROR: spawned process is never `wait()`ed on
+        if true {
+            while false {}
+            // Calling `exit()` after leaving a while loop should still be linted.
+            std::process::exit(0);
+        }
+    }
+
+    {
+        let mut x = { Command::new("").spawn().unwrap() };
+        x.wait().unwrap();
+    }
+
+    {
+        struct S {
+            c: Child,
+        }
+
+        let mut s = S {
+            c: Command::new("").spawn().unwrap(),
+        };
+        s.c.wait().unwrap();
+    }
+
+    {
+        let mut x = Command::new("").spawn().unwrap();
+        //~^ ERROR: spawned process is never `wait()`ed on
+        if true {
+            return;
+        }
+        x.wait().unwrap();
+    }
+
+    {
+        let mut x = Command::new("").spawn().unwrap();
+        //~^ ERROR: spawned process is never `wait()`ed on
+        if true {
+            x.wait().unwrap();
+        }
+    }
+
+    {
+        let mut x = Command::new("").spawn().unwrap();
+        if true {
+            x.wait().unwrap();
+        } else if true {
+            x.wait().unwrap();
+        } else {
+            x.wait().unwrap();
+        }
+    }
+
+    {
+        let mut x = Command::new("").spawn().unwrap();
+        if true {
+            x.wait().unwrap();
+            return;
+        }
+        x.wait().unwrap();
+    }
+}
+
+fn process_child(c: Child) {
+    todo!()
+}
diff --git a/tests/ui/zombie_processes.stderr b/tests/ui/zombie_processes.stderr
new file mode 100644
index 00000000000..eec821a4c8f
--- /dev/null
+++ b/tests/ui/zombie_processes.stderr
@@ -0,0 +1,64 @@
+error: spawned process is never `wait()`ed on
+  --> tests/ui/zombie_processes.rs:14:21
+   |
+LL |         let mut x = Command::new("").spawn().unwrap();
+   |                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = note: consider calling `.wait()`
+   = note: not doing so might leave behind zombie processes
+   = note: see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning
+   = note: `-D clippy::zombie-processes` implied by `-D warnings`
+   = help: to override `-D warnings` add `#[allow(clippy::zombie_processes)]`
+
+error: spawned process is never `wait()`ed on
+  --> tests/ui/zombie_processes.rs:41:21
+   |
+LL |         let mut x = Command::new("").spawn().unwrap();
+   |                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = note: consider calling `.wait()`
+   = note: not doing so might leave behind zombie processes
+   = note: see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning
+
+error: spawned process is never `wait()`ed on
+  --> tests/ui/zombie_processes.rs:66:21
+   |
+LL |         let mut x = Command::new("").spawn().unwrap();
+   |                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = note: consider calling `.wait()`
+   = note: not doing so might leave behind zombie processes
+   = note: see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning
+
+error: spawned process is never `wait()`ed on
+  --> tests/ui/zombie_processes.rs:73:21
+   |
+LL |         let mut x = Command::new("").spawn().unwrap();
+   |                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = note: consider calling `.wait()`
+   = note: not doing so might leave behind zombie processes
+   = note: see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning
+
+error: spawned process is never `wait()`ed on
+  --> tests/ui/zombie_processes.rs:99:21
+   |
+LL |         let mut x = Command::new("").spawn().unwrap();
+   |                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = note: consider calling `.wait()`
+   = note: not doing so might leave behind zombie processes
+   = note: see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning
+
+error: spawned process is never `wait()`ed on
+  --> tests/ui/zombie_processes.rs:108:21
+   |
+LL |         let mut x = Command::new("").spawn().unwrap();
+   |                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = note: consider calling `.wait()`
+   = note: not doing so might leave behind zombie processes
+   = note: see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning
+
+error: aborting due to 6 previous errors
+
diff --git a/tests/ui/zombie_processes_fixable.fixed b/tests/ui/zombie_processes_fixable.fixed
new file mode 100644
index 00000000000..6045262f519
--- /dev/null
+++ b/tests/ui/zombie_processes_fixable.fixed
@@ -0,0 +1,26 @@
+#![warn(clippy::zombie_processes)]
+#![allow(clippy::needless_return)]
+
+use std::process::{Child, Command};
+
+fn main() {
+    let _ = Command::new("").spawn().unwrap().wait();
+    //~^ ERROR: spawned process is never `wait()`ed on
+    Command::new("").spawn().unwrap().wait();
+    //~^ ERROR: spawned process is never `wait()`ed on
+    spawn_proc().wait();
+    //~^ ERROR: spawned process is never `wait()`ed on
+    spawn_proc().wait().unwrap(); // OK
+}
+
+fn not_main() {
+    Command::new("").spawn().unwrap().wait();
+}
+
+fn spawn_proc() -> Child {
+    Command::new("").spawn().unwrap()
+}
+
+fn spawn_proc_2() -> Child {
+    return Command::new("").spawn().unwrap();
+}
diff --git a/tests/ui/zombie_processes_fixable.rs b/tests/ui/zombie_processes_fixable.rs
new file mode 100644
index 00000000000..e1ecb771641
--- /dev/null
+++ b/tests/ui/zombie_processes_fixable.rs
@@ -0,0 +1,26 @@
+#![warn(clippy::zombie_processes)]
+#![allow(clippy::needless_return)]
+
+use std::process::{Child, Command};
+
+fn main() {
+    let _ = Command::new("").spawn().unwrap();
+    //~^ ERROR: spawned process is never `wait()`ed on
+    Command::new("").spawn().unwrap();
+    //~^ ERROR: spawned process is never `wait()`ed on
+    spawn_proc();
+    //~^ ERROR: spawned process is never `wait()`ed on
+    spawn_proc().wait().unwrap(); // OK
+}
+
+fn not_main() {
+    Command::new("").spawn().unwrap();
+}
+
+fn spawn_proc() -> Child {
+    Command::new("").spawn().unwrap()
+}
+
+fn spawn_proc_2() -> Child {
+    return Command::new("").spawn().unwrap();
+}
diff --git a/tests/ui/zombie_processes_fixable.stderr b/tests/ui/zombie_processes_fixable.stderr
new file mode 100644
index 00000000000..e1c40472c32
--- /dev/null
+++ b/tests/ui/zombie_processes_fixable.stderr
@@ -0,0 +1,40 @@
+error: spawned process is never `wait()`ed on
+  --> tests/ui/zombie_processes_fixable.rs:7:13
+   |
+LL |     let _ = Command::new("").spawn().unwrap();
+   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^- help: try: `.wait()`
+   |
+   = note: not doing so might leave behind zombie processes
+   = note: see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning
+   = note: `-D clippy::zombie-processes` implied by `-D warnings`
+   = help: to override `-D warnings` add `#[allow(clippy::zombie_processes)]`
+
+error: spawned process is never `wait()`ed on
+  --> tests/ui/zombie_processes_fixable.rs:9:5
+   |
+LL |     Command::new("").spawn().unwrap();
+   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^- help: try: `.wait()`
+   |
+   = note: not doing so might leave behind zombie processes
+   = note: see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning
+
+error: spawned process is never `wait()`ed on
+  --> tests/ui/zombie_processes_fixable.rs:11:5
+   |
+LL |     spawn_proc();
+   |     ^^^^^^^^^^^^- help: try: `.wait()`
+   |
+   = note: not doing so might leave behind zombie processes
+   = note: see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning
+
+error: spawned process is never `wait()`ed on
+  --> tests/ui/zombie_processes_fixable.rs:17:5
+   |
+LL |     Command::new("").spawn().unwrap();
+   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^- help: try: `.wait()`
+   |
+   = note: not doing so might leave behind zombie processes
+   = note: see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning
+
+error: aborting due to 4 previous errors
+