Skip to main content

hermit/
physical_exit_watch.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Observes guest processes' physical exits through pidfds, for backends whose
10//! processes leave the host some time after their scheduler-granted exit.
11//!
12//! Detcore holds only abstract lifetime state; how a process's exit is observed
13//! on the host belongs to the backend (owner directive 082). A backend hands
14//! this watcher one pidfd per guest process (from `pidfd_open`, or a socket's
15//! `SO_PEERPIDFD`) together with the process id Detcore knows it by. The
16//! watcher calls the backend's callback with that id exactly once, after the
17//! exit is complete as the guest's parent can see it; a backend wires the
18//! callback to `GlobalState::complete_physical_process_exit`.
19//!
20//! The pidfd alone is not enough. `exit_notify` runs under
21//! `write_lock_irq(&tasklist_lock)`: it sets the zombie state, then
22//! `do_notify_parent` wakes pidfd waiters and only then queues SIGCHLD to the
23//! parent. So when the pidfd becomes readable, the watcher calls
24//! `waitid(P_PIDFD, pidfd, WEXITED | WNOHANG | WNOWAIT)`. While the process is
25//! unreaped, the kernel takes `read_lock(&tasklist_lock)` for that call
26//! (`__do_wait`), which cannot be granted until `exit_notify`'s write section,
27//! and with it the SIGCHLD queueing, has finished. A non-parent gets `ECHILD`
28//! and nothing is consumed; `WNOWAIT` leaves a parent's zombie in place. If
29//! the process was already reaped, `release_task` ran after that section
30//! anyway. Either way, a parent probing its pending signals or children sees
31//! the exit in every run once it is reported.
32//!
33//! The watcher never waits on or reaps a process: the guest's own parent, or
34//! the backend's launcher for the root, stays the only reaper.
35//!
36//! [`PhysicalExitWatch::new`] watches on a thread of its own.
37//! [`PhysicalExitWatch::on_runtime`] watches on an existing tokio runtime
38//! instead, for a launcher that must not create a thread: in-guest LiteInst's
39//! coordinator shares the guest's PID namespace, where every thread takes a
40//! process id the guest would otherwise see.
41
42use std::io;
43use std::os::fd::AsRawFd;
44use std::os::fd::FromRawFd;
45use std::os::fd::OwnedFd;
46use std::sync::Arc;
47use std::sync::Condvar;
48use std::sync::Mutex;
49use std::thread::JoinHandle;
50use std::time::Duration;
51use std::time::Instant;
52
53use tokio::io::Interest;
54use tokio::io::unix::AsyncFd;
55
56/// Why [`PhysicalExitWatch::drain`] returned without every watched process gone.
57#[derive(Debug)]
58pub enum DrainError {
59    /// Some watched processes had not exited when the timeout expired; their ids.
60    TimedOut(Vec<i32>),
61    /// The watcher failed, so exits are no longer observed.
62    Watcher(io::Error),
63}
64
65struct Entry {
66    /// Shared with the entry's task in runtime mode.
67    pidfd: Arc<OwnedFd>,
68    raw_pid: i32,
69    /// The process's birth identity, if the kernel provides one; see
70    /// [`birth_identity`].
71    identity: Option<u64>,
72}
73
74/// What [`PhysicalExitWatch::watch`] did with a registration.
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76pub enum Watched {
77    /// The process is now watched.
78    New,
79    /// A process with the same birth identity is already watched; this
80    /// registration joined it, and its exit is reported once.
81    Joined,
82    /// A process with the same birth identity was already seen to exit; its
83    /// exit was (or is being) reported, and is not reported again.
84    AlreadyReported,
85}
86
87/// The filesystem magic of pidfs (`PIDFS_MAGIC`), Linux 6.9 and later.
88const PIDFS_MAGIC: i64 = 0x5049_4446;
89
90/// A process's birth identity: the inode number of a pidfd for it, when that
91/// is a pidfs inode (Linux 6.9 and later), which is one per process and never
92/// reused while the kernel runs. Before pidfs every pidfd shares one
93/// anonymous inode, which names no process, so there is none. Every pidfd for
94/// the same process has the same inode, however it was obtained
95/// (`pidfd_open`, `SO_PEERPIDFD`, `CLONE_PIDFD`).
96pub fn birth_identity(pidfd: &OwnedFd) -> Option<u64> {
97    let mut filesystem: libc::statfs = unsafe { std::mem::zeroed() };
98    if unsafe { libc::fstatfs(pidfd.as_raw_fd(), &mut filesystem) } != 0 {
99        return None;
100    }
101    // `f_type`'s width differs between targets.
102    #[allow(clippy::unnecessary_cast)]
103    if filesystem.f_type as i64 != PIDFS_MAGIC {
104        return None;
105    }
106    let mut metadata: libc::stat = unsafe { std::mem::zeroed() };
107    (unsafe { libc::fstat(pidfd.as_raw_fd(), &mut metadata) } == 0).then_some(metadata.st_ino)
108}
109
110/// Returns once `entry`'s exit has been published to its parent; see the
111/// module documentation for why this `waitid` call is the synchronization.
112fn await_publication(entry: &Entry) -> io::Result<()> {
113    let mut info: libc::siginfo_t = unsafe { std::mem::zeroed() };
114    loop {
115        let rc = unsafe {
116            libc::waitid(
117                libc::P_PIDFD,
118                entry.pidfd.as_raw_fd() as libc::id_t,
119                &mut info,
120                libc::WEXITED | libc::WNOHANG | libc::WNOWAIT,
121            )
122        };
123        if rc == 0 {
124            return Ok(());
125        }
126        let error = io::Error::last_os_error();
127        match error.raw_os_error() {
128            // Not our child, or already reaped: both after publication.
129            Some(libc::ECHILD) => return Ok(()),
130            Some(libc::EINTR) => continue,
131            _ => {
132                return Err(io::Error::new(
133                    error.kind(),
134                    format!(
135                        "waitid on the pidfd of exited process {}: {error}",
136                        entry.raw_pid
137                    ),
138                ));
139            }
140        }
141    }
142}
143
144#[derive(Default)]
145struct State {
146    /// Processes whose exit has not been observed yet.
147    live: Vec<Entry>,
148    /// Processes whose exit was observed but whose callback has not returned.
149    delivering: Vec<i32>,
150    /// Set by `Drop`; the thread exits at its next wakeup.
151    shutdown: bool,
152    /// The watcher's terminal error, if it stopped observing.
153    failure: Option<io::Error>,
154    /// Set by `fail_and_kill_all`: the run has failed, so a process watched
155    /// from now on is killed as soon as it is registered.
156    killing: bool,
157    /// Birth identities whose exit was observed, for the rest of the run, so
158    /// that a late registration for an exited process is recognized.
159    exited: std::collections::HashSet<u64>,
160}
161
162impl State {
163    /// `Some` once a drain can return: every report delivered, or a failure.
164    fn drain_outcome(&self) -> Option<Result<(), DrainError>> {
165        if let Some(failure) = &self.failure {
166            return Some(Err(DrainError::Watcher(io::Error::new(
167                failure.kind(),
168                failure.to_string(),
169            ))));
170        }
171        (self.live.is_empty() && self.delivering.is_empty()).then_some(Ok(()))
172    }
173
174    fn pending(&self) -> Vec<i32> {
175        let mut pending: Vec<i32> = self.live.iter().map(|entry| entry.raw_pid).collect();
176        pending.extend(&self.delivering);
177        pending
178    }
179
180    fn fail(&mut self, error: io::Error) {
181        if self.failure.is_none() {
182            self.failure = Some(error);
183        }
184    }
185}
186
187struct Shared {
188    state: Mutex<State>,
189    /// Notified whenever a callback returns or the watcher fails.
190    changed: Condvar,
191    /// The same notification for [`PhysicalExitWatch::drain_async`].
192    changed_async: tokio::sync::Notify,
193    /// Wakes the watcher thread's poll after `live` grows or on shutdown.
194    wake: OwnedFd,
195}
196
197impl Shared {
198    fn notify_changed(&self) {
199        self.changed.notify_all();
200        self.changed_async.notify_waiters();
201    }
202
203    fn record_failure(&self, error: io::Error) {
204        self.state.lock().unwrap().fail(error);
205        self.notify_changed();
206    }
207
208    /// Ends `raw_pid`'s delivery after its callback returned.
209    fn delivered(&self, raw_pid: i32) {
210        let mut state = self.state.lock().unwrap();
211        if let Some(index) = state.delivering.iter().position(|&pid| pid == raw_pid) {
212            state.delivering.remove(index);
213        }
214        drop(state);
215        self.notify_changed();
216    }
217
218    fn wake_thread(&self) {
219        let one: u64 = 1;
220        // An eventfd write only fails if the counter would overflow, which
221        // still leaves the descriptor readable, so the wakeup is not lost.
222        let _ = unsafe {
223            libc::write(
224                self.wake.as_raw_fd(),
225                &one as *const u64 as *const libc::c_void,
226                std::mem::size_of::<u64>(),
227            )
228        };
229    }
230}
231
232type ExitCallback = Arc<dyn Fn(i32) + Send + Sync>;
233
234/// How the watched pidfds are polled.
235enum Driver {
236    /// One thread polls every pidfd.
237    Thread(Option<JoinHandle<()>>),
238    /// One task per pidfd on an existing tokio runtime.
239    Runtime {
240        handle: tokio::runtime::Handle,
241        on_exit: ExitCallback,
242        tasks: Mutex<Vec<tokio::task::JoinHandle<()>>>,
243    },
244}
245
246/// A watcher over a set of pidfds. See the module documentation.
247pub struct PhysicalExitWatch {
248    shared: Arc<Shared>,
249    driver: Driver,
250}
251
252impl PhysicalExitWatch {
253    fn shared() -> io::Result<Arc<Shared>> {
254        let wake = unsafe { libc::eventfd(0, libc::EFD_CLOEXEC | libc::EFD_NONBLOCK) };
255        if wake < 0 {
256            return Err(io::Error::last_os_error());
257        }
258        Ok(Arc::new(Shared {
259            state: Mutex::new(State::default()),
260            changed: Condvar::new(),
261            changed_async: tokio::sync::Notify::new(),
262            wake: unsafe { OwnedFd::from_raw_fd(wake) },
263        }))
264    }
265
266    /// Watches on `handle`'s runtime and creates no thread. `on_exit` is called
267    /// on that runtime, once per watched process, with the id given to
268    /// [`PhysicalExitWatch::watch`]. Wait for the reports with
269    /// [`PhysicalExitWatch::drain_async`]: on a current-thread runtime the
270    /// blocking [`PhysicalExitWatch::drain`] would stop the very tasks it
271    /// waits for.
272    pub fn on_runtime(
273        handle: tokio::runtime::Handle,
274        on_exit: impl Fn(i32) + Send + Sync + 'static,
275    ) -> io::Result<Self> {
276        Ok(Self {
277            shared: Self::shared()?,
278            driver: Driver::Runtime {
279                handle,
280                on_exit: Arc::new(on_exit),
281                tasks: Mutex::new(Vec::new()),
282            },
283        })
284    }
285
286    /// Starts the watcher thread. `on_exit` is called on that thread, once per
287    /// watched process, with the id given to [`PhysicalExitWatch::watch`].
288    pub fn new(on_exit: impl Fn(i32) + Send + 'static) -> io::Result<Self> {
289        let shared = Self::shared()?;
290        let thread_shared = Arc::clone(&shared);
291        let thread = std::thread::Builder::new()
292            .name("physical-exit-watch".to_string())
293            .spawn(move || {
294                // A panicking callback must not lose reports silently: record
295                // it as the watcher's terminal failure, which `drain` returns
296                // and `watch` refuses on.
297                let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
298                    watch_loop(&thread_shared, &on_exit)
299                }));
300                let failure = match outcome {
301                    Ok(Ok(())) => None,
302                    Ok(Err(error)) => Some(error),
303                    Err(_) => Some(io::Error::other("the physical-exit callback panicked")),
304                };
305                if let Some(error) = failure {
306                    thread_shared.record_failure(error);
307                }
308            })?;
309        Ok(Self {
310            shared,
311            driver: Driver::Thread(Some(thread)),
312        })
313    }
314
315    /// Watches one process. `pidfd` must refer to it; `raw_pid` is the id the
316    /// callback reports. A process that has already exited is reported at once.
317    ///
318    /// Registrations are keyed by birth identity where the kernel provides
319    /// one: a second registration for the same process (for example its
320    /// admission and its parent's creation report, in either order) joins the
321    /// first and is reported once, and one for a process already seen to exit
322    /// is not reported again. So a second registration cannot add a watch
323    /// for a process already watched or already reported, even when its pid
324    /// has been reused. (The report itself still carries only `raw_pid`;
325    /// carrying the identity through to the completion is later work.)
326    pub fn watch(&self, pidfd: OwnedFd, raw_pid: i32) -> io::Result<Watched> {
327        let identity = birth_identity(&pidfd);
328        let mut state = self.shared.state.lock().unwrap();
329        if let Some(failure) = &state.failure {
330            return Err(io::Error::new(failure.kind(), failure.to_string()));
331        }
332        if let Some(identity) = identity {
333            if state.exited.contains(&identity) {
334                return Ok(Watched::AlreadyReported);
335            }
336            if state
337                .live
338                .iter()
339                .any(|entry| entry.identity == Some(identity))
340            {
341                return Ok(Watched::Joined);
342            }
343        }
344        let pidfd = Arc::new(pidfd);
345        state.live.push(Entry {
346            pidfd: Arc::clone(&pidfd),
347            raw_pid,
348            identity,
349        });
350        let killing = state.killing;
351        drop(state);
352        if killing {
353            // Still watched, so its exit is reported and drained like any other.
354            if let Err(error) = send_sigkill(&pidfd, raw_pid) {
355                self.shared.record_failure(error);
356            }
357        }
358        match &self.driver {
359            Driver::Thread(_) => self.shared.wake_thread(),
360            Driver::Runtime {
361                handle,
362                on_exit,
363                tasks,
364            } => {
365                let shared = Arc::clone(&self.shared);
366                let on_exit = Arc::clone(on_exit);
367                let task = handle.spawn(async move {
368                    if let Err(error) = watch_one(&shared, pidfd, raw_pid, &on_exit).await {
369                        shared.record_failure(error);
370                    }
371                });
372                let mut tasks = tasks.lock().unwrap();
373                tasks.retain(|task| !task.is_finished());
374                tasks.push(task);
375            }
376        }
377        Ok(Watched::New)
378    }
379
380    /// The watcher's terminal failure, if it has stopped observing exits.
381    pub fn failure(&self) -> Option<io::Error> {
382        let state = self.shared.state.lock().unwrap();
383        state
384            .failure
385            .as_ref()
386            .map(|failure| io::Error::new(failure.kind(), failure.to_string()))
387    }
388
389    /// Sends `SIGKILL` to every watched process that has not exited yet, and
390    /// to every process watched from now on. Used when a run has failed, so
391    /// that no guest outlives it, including one admitted after the failure;
392    /// the exits that follow are reported as usual. `SIGKILL` also ends a
393    /// stopped process. Every process is attempted; the first error other
394    /// than `ESRCH` (the process already exited, so its report is on the way)
395    /// is returned.
396    pub fn fail_and_kill_all(&self) -> io::Result<()> {
397        let mut state = self.shared.state.lock().unwrap();
398        state.killing = true;
399        let mut first_error = None;
400        for entry in &state.live {
401            if let Err(error) = send_sigkill(&entry.pidfd, entry.raw_pid) {
402                first_error.get_or_insert(error);
403            }
404        }
405        first_error.map_or(Ok(()), Err)
406    }
407
408    /// Waits until every watched process has exited and its callback has
409    /// returned, or until `timeout` expires. Blocks the calling thread, so a
410    /// runtime-mode watcher is drained with [`PhysicalExitWatch::drain_async`].
411    pub fn drain(&self, timeout: Duration) -> Result<(), DrainError> {
412        // An unrepresentable deadline (for example Duration::MAX) waits forever.
413        let deadline = Instant::now().checked_add(timeout);
414        let mut state = self.shared.state.lock().unwrap();
415        loop {
416            if let Some(outcome) = state.drain_outcome() {
417                return outcome;
418            }
419            let Some(deadline) = deadline else {
420                state = self.shared.changed.wait(state).unwrap();
421                continue;
422            };
423            let now = Instant::now();
424            if now >= deadline {
425                return Err(DrainError::TimedOut(state.pending()));
426            }
427            state = self
428                .shared
429                .changed
430                .wait_timeout(state, deadline - now)
431                .unwrap()
432                .0;
433        }
434    }
435
436    /// [`PhysicalExitWatch::drain`] without blocking the thread, so the
437    /// watcher's own tasks keep running when it shares a runtime with the
438    /// caller.
439    pub async fn drain_async(&self, timeout: Duration) -> Result<(), DrainError> {
440        let deadline = tokio::time::Instant::now().checked_add(timeout);
441        loop {
442            // Registered before the state is read, so a change in between
443            // still wakes this wait.
444            let changed = self.shared.changed_async.notified();
445            tokio::pin!(changed);
446            changed.as_mut().enable();
447            {
448                let state = self.shared.state.lock().unwrap();
449                if let Some(outcome) = state.drain_outcome() {
450                    return outcome;
451                }
452                if deadline.is_some_and(|deadline| tokio::time::Instant::now() >= deadline) {
453                    return Err(DrainError::TimedOut(state.pending()));
454                }
455            }
456            match deadline {
457                Some(deadline) => {
458                    let _ = tokio::time::timeout_at(deadline, changed).await;
459                }
460                None => changed.await,
461            }
462        }
463    }
464}
465
466impl Drop for PhysicalExitWatch {
467    fn drop(&mut self) {
468        self.shared.state.lock().unwrap().shutdown = true;
469        match &mut self.driver {
470            Driver::Thread(thread) => {
471                self.shared.wake_thread();
472                if let Some(thread) = thread.take() {
473                    let _ = thread.join();
474                }
475            }
476            Driver::Runtime { tasks, .. } => {
477                for task in tasks.get_mut().unwrap().drain(..) {
478                    task.abort();
479                }
480            }
481        }
482    }
483}
484
485/// Runtime mode: observes one process's exit and reports it.
486/// `SIGKILL` through `pidfd`. `ESRCH` (the process already exited, so its
487/// report is on the way) is not an error.
488fn send_sigkill(pidfd: &OwnedFd, raw_pid: i32) -> io::Result<()> {
489    let rc = unsafe {
490        libc::syscall(
491            libc::SYS_pidfd_send_signal,
492            pidfd.as_raw_fd(),
493            libc::SIGKILL,
494            std::ptr::null::<libc::siginfo_t>(),
495            0,
496        )
497    };
498    if rc < 0 {
499        let error = io::Error::last_os_error();
500        if error.raw_os_error() != Some(libc::ESRCH) {
501            return Err(io::Error::new(
502                error.kind(),
503                format!("SIGKILL to watched process {raw_pid}: {error}"),
504            ));
505        }
506    }
507    Ok(())
508}
509
510async fn watch_one(
511    shared: &Shared,
512    pidfd: Arc<OwnedFd>,
513    raw_pid: i32,
514    on_exit: &ExitCallback,
515) -> io::Result<()> {
516    // A pidfd is readable once its process has exited; POLLHUP after it is
517    // reaped is reported as readable too. The registration is dropped before
518    // the entry is.
519    {
520        let registration = AsyncFd::with_interest(Arc::clone(&pidfd), Interest::READABLE)?;
521        let _ready = registration.readable().await?;
522    }
523    let entry = {
524        let mut state = shared.state.lock().unwrap();
525        let Some(index) = state
526            .live
527            .iter()
528            .position(|entry| Arc::ptr_eq(&entry.pidfd, &pidfd))
529        else {
530            return Ok(());
531        };
532        let entry = state.live.remove(index);
533        state.exited.extend(entry.identity);
534        state.delivering.push(raw_pid);
535        entry
536    };
537    await_publication(&entry)?;
538    // As on the thread: a panicking callback becomes the watcher's failure.
539    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| on_exit(raw_pid)))
540        .map_err(|_| io::Error::other("the physical-exit callback panicked"))?;
541    shared.delivered(raw_pid);
542    Ok(())
543}
544
545fn watch_loop(shared: &Shared, on_exit: &impl Fn(i32)) -> io::Result<()> {
546    loop {
547        // Snapshot the descriptors to poll. Entries are only removed by this
548        // thread, so each index stays valid until the removal below.
549        let mut fds = vec![libc::pollfd {
550            fd: shared.wake.as_raw_fd(),
551            events: libc::POLLIN,
552            revents: 0,
553        }];
554        {
555            let state = shared.state.lock().unwrap();
556            if state.shutdown {
557                return Ok(());
558            }
559            fds.extend(state.live.iter().map(|entry| libc::pollfd {
560                fd: entry.pidfd.as_raw_fd(),
561                events: libc::POLLIN,
562                revents: 0,
563            }));
564        }
565        let ready = unsafe { libc::poll(fds.as_mut_ptr(), fds.len() as libc::nfds_t, -1) };
566        if ready < 0 {
567            let error = io::Error::last_os_error();
568            if error.kind() == io::ErrorKind::Interrupted {
569                continue;
570            }
571            return Err(error);
572        }
573        if fds[0].revents != 0 {
574            let mut counter: u64 = 0;
575            let _ = unsafe {
576                libc::read(
577                    shared.wake.as_raw_fd(),
578                    &mut counter as *mut u64 as *mut libc::c_void,
579                    std::mem::size_of::<u64>(),
580                )
581            };
582        }
583        let exited: Vec<libc::c_int> = fds[1..]
584            .iter()
585            .filter(|fd| fd.revents != 0)
586            .map(|fd| fd.fd)
587            .collect();
588        if exited.is_empty() {
589            continue;
590        }
591        // POLLIN is the exit; POLLHUP/POLLERR/POLLNVAL on a pidfd also mean
592        // the process can no longer be observed alive, so all end the entry.
593        let reported: Vec<Entry> = {
594            let mut state = shared.state.lock().unwrap();
595            let (reported, live): (Vec<Entry>, Vec<Entry>) = std::mem::take(&mut state.live)
596                .into_iter()
597                .partition(|entry| exited.contains(&entry.pidfd.as_raw_fd()));
598            state.live = live;
599            state
600                .exited
601                .extend(reported.iter().filter_map(|entry| entry.identity));
602            state
603                .delivering
604                .extend(reported.iter().map(|entry| entry.raw_pid));
605            reported
606        };
607        // Report outside the lock: the callback takes the scheduler's lock.
608        // A process stays in `delivering` until its callback has returned, so
609        // `drain` cannot succeed before the report has been delivered.
610        for entry in reported {
611            await_publication(&entry)?;
612            on_exit(entry.raw_pid);
613            shared.delivered(entry.raw_pid);
614        }
615    }
616}
617
618#[cfg(test)]
619mod tests {
620    use std::os::unix::process::ExitStatusExt;
621    use std::process::Child;
622    use std::process::Command;
623    use std::sync::mpsc;
624
625    use super::*;
626
627    fn pidfd_open(pid: u32) -> OwnedFd {
628        let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid as libc::pid_t, 0) };
629        assert!(fd >= 0, "pidfd_open: {}", io::Error::last_os_error());
630        unsafe { OwnedFd::from_raw_fd(fd as i32) }
631    }
632
633    /// Kills and reaps its child however the test ends. `Child::kill` does not
634    /// signal a child that was already reaped, so a reused pid is never hit.
635    struct Reaped(Child);
636
637    impl Reaped {
638        fn spawn(command: &mut Command) -> Self {
639            Self(command.spawn().unwrap())
640        }
641        fn pid(&self) -> u32 {
642            self.0.id()
643        }
644    }
645
646    impl Drop for Reaped {
647        fn drop(&mut self) {
648            let _ = self.0.kill();
649            let _ = self.0.wait();
650        }
651    }
652
653    /// The same guard for a raw `fork` child; `reap` disarms it.
654    struct ForkedChild(Option<libc::pid_t>);
655
656    impl ForkedChild {
657        fn reap(&mut self) -> libc::c_int {
658            let pid = self.0.take().unwrap();
659            let mut status = 0;
660            assert_eq!(unsafe { libc::waitpid(pid, &mut status, 0) }, pid);
661            status
662        }
663    }
664
665    impl Drop for ForkedChild {
666        fn drop(&mut self) {
667            if let Some(pid) = self.0 {
668                let mut status = 0;
669                unsafe {
670                    libc::kill(pid, libc::SIGKILL);
671                    libc::waitpid(pid, &mut status, 0);
672                }
673            }
674        }
675    }
676
677    #[test]
678    fn exiting_child_is_reported_once_with_its_pid() {
679        let (sender, receiver) = mpsc::channel();
680        let watch = PhysicalExitWatch::new(move |pid| sender.send(pid).unwrap()).unwrap();
681        let mut child = Reaped::spawn(Command::new("sh").args(["-c", "exit 3"]));
682        let pid = child.pid();
683        watch.watch(pidfd_open(pid), pid as i32).unwrap();
684        assert_eq!(
685            receiver.recv_timeout(Duration::from_secs(30)).unwrap(),
686            pid as i32
687        );
688        watch.drain(Duration::from_secs(30)).unwrap();
689        // The watcher never reaps: the exit status is still the parent's to collect.
690        assert_eq!(child.0.wait().unwrap().code(), Some(3));
691        drop(watch);
692        assert!(
693            receiver.try_recv().is_err(),
694            "the exit was reported more than once"
695        );
696    }
697
698    /// Whether this kernel's pidfds are pidfs inodes, decided from raw
699    /// filesystem metadata rather than by [`birth_identity`], the function
700    /// under test.
701    fn pidfs_by_metadata() -> bool {
702        let pidfd = pidfd_open(std::process::id());
703        let mut filesystem: libc::statfs = unsafe { std::mem::zeroed() };
704        assert_eq!(
705            unsafe { libc::fstatfs(pidfd.as_raw_fd(), &mut filesystem) },
706            0
707        );
708        #[allow(clippy::unnecessary_cast)]
709        let magic = filesystem.f_type as i64;
710        magic == 0x5049_4446
711    }
712
713    /// Every pidfd for one process has the same birth identity, two processes
714    /// have different ones, and a non-pidfd has none. Before pidfs (Linux
715    /// 6.9) no pidfd has one.
716    #[test]
717    fn a_birth_identity_names_one_process() {
718        let pidfs = pidfs_by_metadata();
719        let first = Reaped::spawn(Command::new("sleep").arg("60"));
720        let second = Reaped::spawn(Command::new("sleep").arg("60"));
721        let (a, b) = (pidfd_open(first.pid()), pidfd_open(first.pid()));
722        let other = pidfd_open(second.pid());
723        let anonymous = unsafe { libc::eventfd(0, libc::EFD_CLOEXEC) };
724        assert!(anonymous >= 0);
725        let anonymous = unsafe { OwnedFd::from_raw_fd(anonymous) };
726        assert_eq!(birth_identity(&anonymous), None);
727        if !pidfs {
728            assert_eq!(birth_identity(&a), None);
729            assert_eq!(birth_identity(&other), None);
730            return;
731        }
732        let mut metadata: libc::stat = unsafe { std::mem::zeroed() };
733        assert_eq!(unsafe { libc::fstat(a.as_raw_fd(), &mut metadata) }, 0);
734        let identity = metadata.st_ino;
735        assert_eq!(birth_identity(&a), Some(identity));
736        assert_eq!(birth_identity(&b), Some(identity));
737        assert_ne!(birth_identity(&other), Some(identity));
738        assert!(birth_identity(&other).is_some());
739    }
740
741    /// A second registration of the same process joins the first, and its
742    /// exit is reported once.
743    #[test]
744    fn a_second_registration_of_a_process_joins_the_first() {
745        let (sender, receiver) = mpsc::channel();
746        let watch = PhysicalExitWatch::new(move |pid| sender.send(pid).unwrap()).unwrap();
747        let mut child = Reaped::spawn(Command::new("sleep").arg("60"));
748        let pid = child.pid();
749        let keyed = pidfs_by_metadata();
750        assert_eq!(
751            watch.watch(pidfd_open(pid), pid as i32).unwrap(),
752            Watched::New
753        );
754        let second = watch.watch(pidfd_open(pid), pid as i32).unwrap();
755        assert_eq!(second, if keyed { Watched::Joined } else { Watched::New });
756        child.0.kill().unwrap();
757        watch.drain(Duration::from_secs(30)).unwrap();
758        let reports = receiver.try_iter().collect::<Vec<i32>>();
759        let expected = if keyed {
760            vec![pid as i32]
761        } else {
762            vec![pid as i32; 2]
763        };
764        assert_eq!(reports, expected);
765    }
766
767    /// A registration that arrives after the process's exit was observed is
768    /// recognized and not reported again, even after the process was reaped.
769    #[test]
770    fn a_registration_after_the_exit_was_observed_is_not_reported_again() {
771        let (sender, receiver) = mpsc::channel();
772        let watch = PhysicalExitWatch::new(move |pid| sender.send(pid).unwrap()).unwrap();
773        let mut child = Reaped::spawn(Command::new("sh").args(["-c", "exit 0"]));
774        let pid = child.pid();
775        let late = pidfd_open(pid);
776        if !pidfs_by_metadata() {
777            return;
778        }
779        assert_eq!(
780            watch.watch(pidfd_open(pid), pid as i32).unwrap(),
781            Watched::New
782        );
783        assert_eq!(
784            receiver.recv_timeout(Duration::from_secs(30)).unwrap(),
785            pid as i32
786        );
787        watch.drain(Duration::from_secs(30)).unwrap();
788        child.0.wait().unwrap();
789        assert_eq!(
790            watch.watch(late, pid as i32).unwrap(),
791            Watched::AlreadyReported
792        );
793        watch.drain(Duration::from_secs(30)).unwrap();
794        drop(watch);
795        assert!(receiver.try_recv().is_err(), "the exit was reported again");
796    }
797
798    #[test]
799    fn drain_honours_its_timeout() {
800        let watch = PhysicalExitWatch::new(|_| {}).unwrap();
801        let mut child = Reaped::spawn(Command::new("sleep").arg("60"));
802        let pid = child.pid();
803        watch.watch(pidfd_open(pid), pid as i32).unwrap();
804        let start = Instant::now();
805        match watch.drain(Duration::from_millis(200)) {
806            Err(DrainError::TimedOut(live)) => assert_eq!(live, [pid as i32]),
807            other => panic!("expected a timeout, got {other:?}"),
808        }
809        let waited = start.elapsed();
810        assert!(
811            waited >= Duration::from_millis(200),
812            "returned early: {waited:?}"
813        );
814        assert!(waited < Duration::from_secs(20), "overran: {waited:?}");
815        child.0.kill().unwrap();
816        assert_eq!(child.0.wait().unwrap().signal(), Some(libc::SIGKILL));
817    }
818
819    #[test]
820    fn drain_without_processes_accepts_an_unbounded_timeout() {
821        let watch = PhysicalExitWatch::new(|_| {}).unwrap();
822        watch.drain(Duration::MAX).unwrap();
823    }
824
825    #[test]
826    fn drain_waits_for_the_callback_to_return() {
827        // The process has exited, but the report has not been delivered until
828        // the callback returns; drain must not succeed in between.
829        let (started_sender, started) = mpsc::channel();
830        let (release, release_receiver) = mpsc::channel::<()>();
831        let watch = PhysicalExitWatch::new(move |pid| {
832            started_sender.send(pid).unwrap();
833            let _ = release_receiver.recv();
834        })
835        .unwrap();
836        // Rebound after `watch`, so it is dropped first if an assertion below
837        // fails: the paused callback then returns and `watch` can join.
838        let release = release;
839        let mut child = Reaped::spawn(&mut Command::new("true"));
840        let pid = child.pid();
841        watch.watch(pidfd_open(pid), pid as i32).unwrap();
842        assert_eq!(
843            started.recv_timeout(Duration::from_secs(30)).unwrap(),
844            pid as i32
845        );
846        match watch.drain(Duration::from_millis(200)) {
847            Err(DrainError::TimedOut(pending)) => assert_eq!(pending, [pid as i32]),
848            other => panic!("drain returned before the callback did: {other:?}"),
849        }
850        release.send(()).unwrap();
851        watch.drain(Duration::from_secs(30)).unwrap();
852        child.0.wait().unwrap();
853    }
854
855    #[test]
856    fn a_panicking_callback_fails_the_watcher() {
857        let watch = PhysicalExitWatch::new(|_| panic!("callback failure under test")).unwrap();
858        let mut child = Reaped::spawn(&mut Command::new("true"));
859        let pid = child.pid();
860        watch.watch(pidfd_open(pid), pid as i32).unwrap();
861        match watch.drain(Duration::from_secs(30)) {
862            Err(DrainError::Watcher(_)) => {}
863            other => panic!("expected the watcher's failure, got {other:?}"),
864        }
865        child.0.wait().unwrap();
866        let other = Reaped::spawn(Command::new("sleep").arg("60"));
867        assert!(
868            watch
869                .watch(pidfd_open(other.pid()), other.pid() as i32)
870                .is_err(),
871            "a failed watcher accepted a new process"
872        );
873    }
874
875    #[test]
876    fn kill_all_ends_every_watched_process() {
877        let (sender, receiver) = mpsc::channel();
878        let watch = PhysicalExitWatch::new(move |pid| sender.send(pid).unwrap()).unwrap();
879        let mut children: Vec<Reaped> = (0..3)
880            .map(|_| Reaped::spawn(Command::new("sleep").arg("60")))
881            .collect();
882        // A stopped process is ended too: stop one and wait until it is stopped.
883        let stopped = children[0].pid();
884        assert_eq!(
885            unsafe { libc::kill(stopped as libc::pid_t, libc::SIGSTOP) },
886            0
887        );
888        let mut info: libc::siginfo_t = unsafe { std::mem::zeroed() };
889        let rc = unsafe {
890            libc::waitid(
891                libc::P_PID,
892                stopped,
893                &mut info,
894                libc::WSTOPPED | libc::WNOWAIT,
895            )
896        };
897        assert_eq!(rc, 0, "waitid: {}", io::Error::last_os_error());
898        assert_eq!(info.si_code, libc::CLD_STOPPED);
899        for child in &children {
900            watch
901                .watch(pidfd_open(child.pid()), child.pid() as i32)
902                .unwrap();
903        }
904        watch.fail_and_kill_all().unwrap();
905        watch.drain(Duration::from_secs(30)).unwrap();
906        let mut reported: Vec<i32> = receiver.try_iter().collect();
907        reported.sort_unstable();
908        let mut expected: Vec<i32> = children.iter().map(|child| child.pid() as i32).collect();
909        expected.sort_unstable();
910        assert_eq!(reported, expected);
911        for child in &mut children {
912            assert_eq!(child.0.wait().unwrap().signal(), Some(libc::SIGKILL));
913        }
914    }
915
916    #[test]
917    fn a_process_watched_after_kill_all_is_killed_and_reported() {
918        let (sender, receiver) = mpsc::channel();
919        let watch = PhysicalExitWatch::new(move |pid| sender.send(pid).unwrap()).unwrap();
920        watch.fail_and_kill_all().unwrap();
921        // Admitted after the run failed: it must not outlive the run.
922        let mut late = Reaped::spawn(Command::new("sleep").arg("60"));
923        watch
924            .watch(pidfd_open(late.pid()), late.pid() as i32)
925            .unwrap();
926        watch.drain(Duration::from_secs(30)).unwrap();
927        assert_eq!(
928            receiver.try_iter().collect::<Vec<i32>>(),
929            [late.pid() as i32]
930        );
931        assert_eq!(late.0.wait().unwrap().signal(), Some(libc::SIGKILL));
932    }
933
934    #[test]
935    fn already_exited_process_is_reported_at_once() {
936        let (sender, receiver) = mpsc::channel();
937        let watch = PhysicalExitWatch::new(move |pid| sender.send(pid).unwrap()).unwrap();
938        let mut child = Reaped::spawn(&mut Command::new("true"));
939        let pid = child.pid();
940        let pidfd = pidfd_open(pid);
941        // Wait for the exit without reaping, so the pidfd is readable before it is watched.
942        let mut info: libc::siginfo_t = unsafe { std::mem::zeroed() };
943        let rc =
944            unsafe { libc::waitid(libc::P_PID, pid, &mut info, libc::WEXITED | libc::WNOWAIT) };
945        assert_eq!(rc, 0, "waitid: {}", io::Error::last_os_error());
946        watch.watch(pidfd, pid as i32).unwrap();
947        assert_eq!(
948            receiver.recv_timeout(Duration::from_secs(30)).unwrap(),
949            pid as i32
950        );
951        child.0.wait().unwrap();
952    }
953
954    fn thread_count() -> usize {
955        std::fs::read_dir("/proc/self/task").unwrap().count()
956    }
957
958    #[test]
959    fn runtime_mode_reports_each_exit_once_and_creates_no_thread() {
960        // Counting this process's threads needs a process no other test
961        // shares, so the test re-runs itself alone in a fresh one.
962        if std::env::var_os(INNER_RUN).is_none() {
963            return run_in_fresh_process(
964                "physical_exit_watch::tests::runtime_mode_reports_each_exit_once_and_creates_no_thread",
965                false,
966            );
967        }
968        let runtime = tokio::runtime::Builder::new_current_thread()
969            .enable_all()
970            .build()
971            .unwrap();
972        let threads = thread_count();
973        runtime.block_on(async {
974            let (sender, receiver) = mpsc::channel();
975            let watch =
976                PhysicalExitWatch::on_runtime(tokio::runtime::Handle::current(), move |pid| {
977                    sender.send(pid).unwrap()
978                })
979                .unwrap();
980            let mut children: Vec<Reaped> = (0..3)
981                .map(|code| Reaped::spawn(Command::new("sh").args(["-c", &format!("exit {code}")])))
982                .collect();
983            for child in &children {
984                watch
985                    .watch(pidfd_open(child.pid()), child.pid() as i32)
986                    .unwrap();
987            }
988            watch.drain_async(Duration::from_secs(30)).await.unwrap();
989            assert_eq!(
990                thread_count(),
991                threads,
992                "the runtime-mode watcher started a thread"
993            );
994            let mut reported: Vec<i32> = receiver.try_iter().collect();
995            reported.sort_unstable();
996            let mut expected: Vec<i32> = children.iter().map(|child| child.pid() as i32).collect();
997            expected.sort_unstable();
998            assert_eq!(reported, expected);
999            // The watcher never reaps: each status is still the parent's to collect.
1000            for (code, child) in children.iter_mut().enumerate() {
1001                assert_eq!(child.0.wait().unwrap().code(), Some(code as i32));
1002            }
1003            drop(watch);
1004            assert!(
1005                receiver.try_recv().is_err(),
1006                "an exit was reported more than once"
1007            );
1008        });
1009    }
1010
1011    #[tokio::test]
1012    async fn runtime_mode_drain_honours_its_timeout() {
1013        let (sender, receiver) = mpsc::channel();
1014        let watch = PhysicalExitWatch::on_runtime(tokio::runtime::Handle::current(), move |pid| {
1015            sender.send(pid).unwrap()
1016        })
1017        .unwrap();
1018        let mut sleeper = Reaped::spawn(Command::new("sleep").arg("60"));
1019        let pid = sleeper.pid();
1020        watch.watch(pidfd_open(pid), pid as i32).unwrap();
1021        let start = Instant::now();
1022        match watch.drain_async(Duration::from_millis(200)).await {
1023            Err(DrainError::TimedOut(live)) => assert_eq!(live, [pid as i32]),
1024            other => panic!("expected a timeout, got {other:?}"),
1025        }
1026        let waited = start.elapsed();
1027        assert!(
1028            waited >= Duration::from_millis(200),
1029            "returned early: {waited:?}"
1030        );
1031        assert!(waited < Duration::from_secs(20), "overran: {waited:?}");
1032        assert!(receiver.try_recv().is_err(), "reported a live process");
1033        sleeper.0.kill().unwrap();
1034        watch.drain_async(Duration::from_secs(30)).await.unwrap();
1035        assert_eq!(receiver.try_recv().unwrap(), pid as i32);
1036        assert_eq!(sleeper.0.wait().unwrap().signal(), Some(libc::SIGKILL));
1037    }
1038
1039    #[tokio::test]
1040    async fn runtime_mode_reports_an_already_exited_process() {
1041        let (sender, receiver) = mpsc::channel();
1042        let watch = PhysicalExitWatch::on_runtime(tokio::runtime::Handle::current(), move |pid| {
1043            sender.send(pid).unwrap()
1044        })
1045        .unwrap();
1046        let mut child = Reaped::spawn(&mut Command::new("true"));
1047        let pid = child.pid();
1048        let pidfd = pidfd_open(pid);
1049        // Readable before it is registered: the registration must still fire.
1050        let mut info: libc::siginfo_t = unsafe { std::mem::zeroed() };
1051        let rc =
1052            unsafe { libc::waitid(libc::P_PID, pid, &mut info, libc::WEXITED | libc::WNOWAIT) };
1053        assert_eq!(rc, 0, "waitid: {}", io::Error::last_os_error());
1054        watch.watch(pidfd, pid as i32).unwrap();
1055        watch.drain_async(Duration::from_secs(30)).await.unwrap();
1056        assert_eq!(receiver.try_recv().unwrap(), pid as i32);
1057        child.0.wait().unwrap();
1058    }
1059
1060    #[tokio::test]
1061    async fn runtime_mode_panicking_callback_fails_the_watcher() {
1062        let watch = PhysicalExitWatch::on_runtime(tokio::runtime::Handle::current(), |_| {
1063            panic!("callback failure under test")
1064        })
1065        .unwrap();
1066        let mut child = Reaped::spawn(&mut Command::new("true"));
1067        let pid = child.pid();
1068        watch.watch(pidfd_open(pid), pid as i32).unwrap();
1069        match watch.drain_async(Duration::from_secs(30)).await {
1070            Err(DrainError::Watcher(_)) => {}
1071            other => panic!("expected the watcher's failure, got {other:?}"),
1072        }
1073        child.0.wait().unwrap();
1074        let other = Reaped::spawn(Command::new("sleep").arg("60"));
1075        assert!(
1076            watch
1077                .watch(pidfd_open(other.pid()), other.pid() as i32)
1078                .is_err(),
1079            "a failed watcher accepted a new process"
1080        );
1081    }
1082
1083    /// Set in a test's own fresh process; see [`run_in_fresh_process`].
1084    const INNER_RUN: &str = "HERMIT_PHYSICAL_EXIT_WATCH_INNER_RUN";
1085
1086    /// Re-runs the test `name` alone in a fresh process of this test binary and
1087    /// requires it to pass. Used where the property needs a process no other
1088    /// test shares: under a threaded harness another test can fork a child
1089    /// that inherits this test's descriptors, or a harness thread can take a
1090    /// signal. With `block_sigchld`, the fresh process starts with SIGCHLD
1091    /// blocked in every thread (a blocked mask survives exec, and `pre_exec`
1092    /// runs after std resets the child's mask).
1093    fn run_in_fresh_process(name: &str, block_sigchld: bool) {
1094        use std::os::unix::process::CommandExt;
1095        let mut command = Command::new(std::env::current_exe().unwrap());
1096        command
1097            .args(["--exact", name, "--nocapture", "--test-threads=1"])
1098            .env(INNER_RUN, "1");
1099        if block_sigchld {
1100            unsafe {
1101                command.pre_exec(|| {
1102                    let set = sigchld_set();
1103                    if libc::sigprocmask(libc::SIG_BLOCK, &set, std::ptr::null_mut()) != 0 {
1104                        return Err(io::Error::last_os_error());
1105                    }
1106                    Ok(())
1107                });
1108            }
1109        }
1110        let output = command.output().unwrap();
1111        let stdout = String::from_utf8_lossy(&output.stdout);
1112        let stderr = String::from_utf8_lossy(&output.stderr);
1113        assert!(
1114            output.status.success(),
1115            "inner run failed:\n{stdout}\n{stderr}"
1116        );
1117        assert!(
1118            stdout.contains("1 passed"),
1119            "inner run did not execute the test:\n{stdout}\n{stderr}"
1120        );
1121    }
1122
1123    fn sigchld_set() -> libc::sigset_t {
1124        let mut set: libc::sigset_t = unsafe { std::mem::zeroed() };
1125        unsafe {
1126            libc::sigemptyset(&mut set);
1127            libc::sigaddset(&mut set, libc::SIGCHLD);
1128        }
1129        set
1130    }
1131
1132    #[test]
1133    fn sigchld_is_queued_before_the_exit_is_reported() {
1134        // The kernel wakes pidfd waiters before it queues SIGCHLD to the
1135        // parent. The callback must find SIGCHLD already pending, observed
1136        // without any call that takes tasklist_lock itself. That needs every
1137        // thread of the process to block SIGCHLD, so nothing can dequeue it,
1138        // which this test process cannot arrange for its harness threads. So
1139        // the test re-runs itself in a fresh process whose initial mask blocks
1140        // SIGCHLD (a blocked mask survives exec, and pre_exec runs after std
1141        // resets the child's mask).
1142        if std::env::var_os(INNER_RUN).is_some() {
1143            return sigchld_inner();
1144        }
1145        run_in_fresh_process(
1146            "physical_exit_watch::tests::sigchld_is_queued_before_the_exit_is_reported",
1147            true,
1148        );
1149    }
1150
1151    fn sigchld_inner() {
1152        let chld = sigchld_set();
1153        let mut mask: libc::sigset_t = unsafe { std::mem::zeroed() };
1154        unsafe { libc::pthread_sigmask(libc::SIG_BLOCK, std::ptr::null(), &mut mask) };
1155        assert_eq!(
1156            unsafe { libc::sigismember(&mask, libc::SIGCHLD) },
1157            1,
1158            "the inner process must start with SIGCHLD blocked"
1159        );
1160        let (sender, receiver) = mpsc::channel();
1161        let watch = PhysicalExitWatch::new(move |pid| {
1162            let mut pending: libc::sigset_t = unsafe { std::mem::zeroed() };
1163            unsafe { libc::sigpending(&mut pending) };
1164            let queued = unsafe { libc::sigismember(&pending, libc::SIGCHLD) } == 1;
1165            sender.send((pid, queued)).unwrap();
1166        })
1167        .unwrap();
1168        let zero = libc::timespec {
1169            tv_sec: 0,
1170            tv_nsec: 0,
1171        };
1172        for _ in 0..50 {
1173            let mut child = Reaped::spawn(&mut Command::new("true"));
1174            let pid = child.pid();
1175            watch.watch(pidfd_open(pid), pid as i32).unwrap();
1176            let (reported, queued) = receiver.recv_timeout(Duration::from_secs(30)).unwrap();
1177            assert_eq!(reported, pid as i32);
1178            assert!(queued, "exit of {pid} reported before SIGCHLD was queued");
1179            child.0.wait().unwrap();
1180            // Consume this child's SIGCHLD before the next child.
1181            let signal = unsafe { libc::sigtimedwait(&chld, std::ptr::null_mut(), &zero) };
1182            assert_eq!(signal, libc::SIGCHLD);
1183        }
1184    }
1185
1186    #[test]
1187    fn descriptors_are_closed_before_the_pidfd_is_readable() {
1188        // The property the scheduler relies on: once the exit is reported, a
1189        // pipe whose only writer was the exited process reads EOF. Run in a
1190        // fresh process, so no other test's fork can inherit the writer.
1191        if std::env::var_os(INNER_RUN).is_none() {
1192            return run_in_fresh_process(
1193                "physical_exit_watch::tests::descriptors_are_closed_before_the_pidfd_is_readable",
1194                false,
1195            );
1196        }
1197        let mut pipe = [0; 2];
1198        assert_eq!(
1199            unsafe { libc::pipe2(pipe.as_mut_ptr(), libc::O_CLOEXEC) },
1200            0
1201        );
1202        let read_end = unsafe { OwnedFd::from_raw_fd(pipe[0]) };
1203        let write_end = unsafe { OwnedFd::from_raw_fd(pipe[1]) };
1204        let pid = unsafe { libc::fork() };
1205        assert!(pid >= 0);
1206        if pid == 0 {
1207            // Child: only async-signal-safe calls. Keep the write end, exit.
1208            unsafe {
1209                libc::close(read_end.as_raw_fd());
1210                libc::_exit(0)
1211            };
1212        }
1213        let mut child = ForkedChild(Some(pid));
1214        drop(write_end);
1215        let (sender, receiver) = mpsc::channel();
1216        let watch = PhysicalExitWatch::new(move |pid| sender.send(pid).unwrap()).unwrap();
1217        watch.watch(pidfd_open(pid as u32), pid).unwrap();
1218        assert_eq!(receiver.recv_timeout(Duration::from_secs(30)).unwrap(), pid);
1219        let mut byte = 0u8;
1220        let flags = unsafe { libc::fcntl(read_end.as_raw_fd(), libc::F_GETFL) };
1221        unsafe {
1222            libc::fcntl(
1223                read_end.as_raw_fd(),
1224                libc::F_SETFL,
1225                flags | libc::O_NONBLOCK,
1226            )
1227        };
1228        let n = unsafe {
1229            libc::read(
1230                read_end.as_raw_fd(),
1231                &mut byte as *mut u8 as *mut libc::c_void,
1232                1,
1233            )
1234        };
1235        assert_eq!(
1236            n,
1237            0,
1238            "expected EOF, got {n} ({})",
1239            io::Error::last_os_error()
1240        );
1241        child.reap();
1242    }
1243
1244    /// A parent that auto-reaps (SIGCHLD set to SIG_IGN, or SA_NOCLDWAIT)
1245    /// never holds a zombie: the kernel reaps the child as it exits. The exit
1246    /// is still reported exactly once, through the reaped (ECHILD) path of the
1247    /// publication barrier. The disposition is process-wide, so each case runs
1248    /// alone in a fresh process.
1249    #[test]
1250    fn a_child_of_a_parent_ignoring_sigchld_is_reported_once() {
1251        if std::env::var_os(INNER_RUN).is_none() {
1252            return run_in_fresh_process(
1253                "physical_exit_watch::tests::a_child_of_a_parent_ignoring_sigchld_is_reported_once",
1254                false,
1255            );
1256        }
1257        auto_reaped_inner(libc::SIG_IGN, 0);
1258    }
1259
1260    #[test]
1261    fn a_child_of_a_parent_with_sa_nocldwait_is_reported_once() {
1262        if std::env::var_os(INNER_RUN).is_none() {
1263            return run_in_fresh_process(
1264                "physical_exit_watch::tests::a_child_of_a_parent_with_sa_nocldwait_is_reported_once",
1265                false,
1266            );
1267        }
1268        auto_reaped_inner(libc::SIG_DFL, libc::SA_NOCLDWAIT);
1269    }
1270
1271    fn auto_reaped_inner(handler: libc::sighandler_t, flags: libc::c_int) {
1272        let mut action: libc::sigaction = unsafe { std::mem::zeroed() };
1273        action.sa_sigaction = handler;
1274        action.sa_flags = flags;
1275        assert_eq!(
1276            unsafe { libc::sigaction(libc::SIGCHLD, &action, std::ptr::null_mut()) },
1277            0
1278        );
1279        let (sender, receiver) = mpsc::channel();
1280        let watch = PhysicalExitWatch::new(move |pid| sender.send(pid).unwrap()).unwrap();
1281        for _ in 0..20 {
1282            // The child waits on a pipe until it is watched, so it cannot be
1283            // reaped before its pidfd exists.
1284            let mut pipe = [0; 2];
1285            assert_eq!(
1286                unsafe { libc::pipe2(pipe.as_mut_ptr(), libc::O_CLOEXEC) },
1287                0
1288            );
1289            let pid = unsafe { libc::fork() };
1290            assert!(pid >= 0);
1291            if pid == 0 {
1292                let mut byte = 0u8;
1293                unsafe {
1294                    libc::close(pipe[1]);
1295                    libc::read(pipe[0], (&raw mut byte).cast(), 1);
1296                    libc::_exit(0)
1297                };
1298            }
1299            unsafe { libc::close(pipe[0]) };
1300            watch.watch(pidfd_open(pid as u32), pid).unwrap();
1301            unsafe { libc::close(pipe[1]) };
1302            assert_eq!(receiver.recv_timeout(Duration::from_secs(30)).unwrap(), pid);
1303            // Auto-reaped: there is no zombie left to collect.
1304            let mut status = 0;
1305            assert_eq!(unsafe { libc::waitpid(pid, &mut status, 0) }, -1);
1306            assert_eq!(
1307                io::Error::last_os_error().raw_os_error(),
1308                Some(libc::ECHILD)
1309            );
1310        }
1311        watch.drain(Duration::from_secs(30)).unwrap();
1312        drop(watch);
1313        assert!(
1314            receiver.try_recv().is_err(),
1315            "an exit was reported more than once"
1316        );
1317    }
1318}