Skip to main content

starnix_core/task/
zombie.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::signals::syscalls::WaitingOptions;
6use crate::signals::{SignalDetail, SignalInfo};
7use crate::task::{
8    ExitStatus, Pid, PidTableGuard, ProcessSelector, Task, ThreadGroup, ThreadGroupStateRef,
9};
10use starnix_logging::log_warn;
11use starnix_types::ownership::{OwnedRef, Releasable};
12use starnix_types::stats::TaskTimeStats;
13use starnix_uapi::signals::{SIGCHLD, Signal};
14use starnix_uapi::{pid_t, uid_t};
15use std::sync::{Arc, Weak};
16
17#[derive(Debug)]
18pub struct ZombieProcess {
19    pub task: Arc<Task>,
20    pub pgid: Pid,
21    pub exit_signal: Option<Signal>,
22    pub state: ZombieState,
23
24    /// Whether dropping this ZombieProcess should imply removing the pid from
25    /// the PidTable
26    pub is_canonical: bool,
27}
28
29impl PartialEq for ZombieProcess {
30    fn eq(&self, other: &Self) -> bool {
31        // We assume only one set of ZombieProcess data per process, so this should cover it.
32        self.task.pid == other.task.pid && self.is_canonical == other.is_canonical
33    }
34}
35
36impl Eq for ZombieProcess {}
37
38impl PartialOrd for ZombieProcess {
39    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
40        Some(self.cmp(other))
41    }
42}
43
44impl Ord for ZombieProcess {
45    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
46        (&self.task.pid, self.is_canonical).cmp(&(&other.task.pid, other.is_canonical))
47    }
48}
49
50impl ZombieProcess {
51    pub fn new(
52        task: Arc<Task>,
53        thread_group: ThreadGroupStateRef<'_>,
54        exit_status: ExitStatus,
55        exit_signal: Option<Signal>,
56    ) -> OwnedRef<Self> {
57        let time_stats = thread_group.base.time_stats() + thread_group.children_time_stats;
58        OwnedRef::new(ZombieProcess {
59            task,
60            pgid: thread_group.process_group.leader.clone(),
61            state: ZombieState { exit_status, time_stats },
62            exit_signal,
63            is_canonical: true,
64        })
65    }
66
67    pub fn pgid(&self) -> pid_t {
68        self.pgid.id
69    }
70
71    pub fn to_wait_result(&self) -> WaitResult {
72        WaitResult {
73            pid: self.task.pid.clone(),
74            uid: self.task.real_creds().uid,
75            zombie_state: self.state.clone(),
76            exit_signal: self.exit_signal,
77        }
78    }
79
80    pub fn as_artificial(&self) -> Self {
81        ZombieProcess {
82            task: self.task.clone(),
83            pgid: self.pgid.clone(),
84            state: self.state.clone(),
85            exit_signal: self.exit_signal,
86            is_canonical: false,
87        }
88    }
89
90    pub fn matches_selector(&self, selector: &ProcessSelector) -> bool {
91        match selector {
92            ProcessSelector::Any => true,
93            ProcessSelector::Pid(pid) => &self.task.pid == pid,
94            ProcessSelector::Pgid(pgid) => &self.pgid == pgid,
95        }
96    }
97
98    pub fn matches_selector_and_waiting_option(
99        &self,
100        selector: &ProcessSelector,
101        options: &WaitingOptions,
102    ) -> bool {
103        if !self.matches_selector(selector) {
104            return false;
105        }
106
107        if options.wait_for_all {
108            true
109        } else {
110            // A "clone" zombie is one which has delivered no signal, or a
111            // signal other than SIGCHLD to its parent upon termination.
112            options.wait_for_clone == (self.exit_signal != Some(SIGCHLD))
113        }
114    }
115}
116
117/// Trait for releasing a zombie process from the PID table.
118///
119/// This trait erases the lifetime parameter of [`PidTableGuard`] so that [`ZombieProcess`] can
120/// implement [`Releasable`] without tying the mutable reference lifetime to the guard's lifetime
121/// parameter, preserving variance and allowing reborrowing in loops and across sequential calls.
122pub trait ZombieReleaser {
123    fn remove_zombie(&mut self, pid: &Pid);
124}
125
126impl<'a> ZombieReleaser for PidTableGuard<'a> {
127    fn remove_zombie(&mut self, pid: &Pid) {
128        self.remove_zombie(pid);
129    }
130}
131
132impl Releasable for ZombieProcess {
133    type Context<'a> = &'a mut dyn ZombieReleaser;
134
135    fn release<'a>(self, pids: &'a mut dyn ZombieReleaser) {
136        if self.is_canonical {
137            pids.remove_zombie(&self.task.pid);
138        }
139    }
140}
141
142/// A zombie process that is pending notification.
143///
144/// # Thread Safety
145///
146/// Notifications are generally produced in contexts in which a [`ThreadGroup`] state lock is held.
147/// Any such lock must be released before notifications are delivered. The notification's
148/// recipient thread group may be:
149/// - The originating thread group, in which case delivery while locked would self-deadlock.
150/// - One of this thread group's ancestors, in which case delivery while locked would invert the
151///   parent-child ordering of [`ThreadGroup`] locks.
152///
153/// The [`PidTable`] lock must be held continuously between [`ZombieNotification`] production and
154/// delivery to protect against concurrent exit races. Delivery requires releasing [`ThreadGroup`]
155/// state locks. If the recipient thread group exits before the notification is delivered, subreaper
156/// identification becomes impossible and the zombie must be reaped without notifying observers.
157/// Holding the [`PidTable`] lock throughout notification ensures the recipient cannot concurrently
158/// exit.
159#[must_use = "Notifications must be explicitly delivered or discarded"]
160pub struct ZombieNotification {
161    /// The recipient [`ThreadGroup`], which is generally the zombie's parent.
162    pub recipient: Weak<ThreadGroup>,
163
164    /// The zombie process to notify the parent of.
165    pub zombie: OwnedRef<ZombieProcess>,
166}
167
168impl ZombieNotification {
169    pub fn new(recipient: Weak<ThreadGroup>, zombie: OwnedRef<ZombieProcess>) -> Self {
170        Self { recipient, zombie }
171    }
172
173    /// Delivers the zombie notification to the parent.
174    ///
175    /// # Thread Safety
176    ///
177    /// Acquires [`ThreadGroup`] state locks.
178    pub fn deliver(self, pids: &mut PidTableGuard<'_>) {
179        if let Some(parent) = self.recipient.upgrade() {
180            parent.do_zombie_notifications(self.zombie, pids);
181        } else {
182            log_warn!("Zombie {} reaped silently", self.zombie.task.get_pid());
183            self.zombie.release(pids);
184        }
185    }
186
187    /// Discards the zombie notification without delivering it.
188    ///
189    /// If the [`ZombieProcess`] has no other owners, it will be reaped.
190    pub fn discard(self, pids: &mut PidTableGuard<'_>) {
191        self.zombie.release(pids);
192    }
193}
194
195#[derive(Clone, Debug, PartialEq, Eq)]
196pub struct WaitResult {
197    pub pid: Pid,
198    pub uid: uid_t,
199    pub zombie_state: ZombieState,
200    pub exit_signal: Option<Signal>,
201}
202
203impl WaitResult {
204    // According to wait(2) man page, SignalInfo.signal needs to always be set to SIGCHLD
205    pub fn as_signal_info(&self) -> SignalInfo {
206        SignalInfo::with_detail(
207            SIGCHLD,
208            self.zombie_state.exit_status.signal_info_code(),
209            SignalDetail::SIGCHLD {
210                pid: self.pid.clone(),
211                uid: self.uid,
212                status: self.zombie_state.exit_status.signal_info_status(),
213            },
214        )
215    }
216}
217
218/// State of a task or thread group which has exited.
219#[derive(Clone, Debug, PartialEq, Eq)]
220pub struct ZombieState {
221    pub exit_status: ExitStatus,
222    pub time_stats: TaskTimeStats,
223}