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