Skip to main content

subc_os/
lib.rs

1//! Operating-system primitives the subc daemon needs and cannot reach without
2//! unsafe code, each behind a small safe API.
3//!
4//! The daemon crates forbid unsafe code. This crate is the one deliberate
5//! exception (like `subc-uptime` and `subc-cgroup`): every `unsafe` block here
6//! is a single foreign call with its preconditions stated beside it, and nothing
7//! unsafe is exported.
8//!
9//! Today it answers one question: is the process now holding pid N the same
10//! process the daemon spawned earlier? A pid alone cannot say, because the
11//! kernel reuses pids once a process has been reaped. [`Process`] reads the two
12//! facts that tell processes apart, the kernel's start time for the pid and the
13//! file identity (device and inode) of the executable image it runs, and sends
14//! signals to it.
15//!
16//! Sources, per platform:
17//!
18//! - Linux: the start time is field 22 of `/proc/<pid>/stat` (clock ticks since
19//!   boot), and the executable is `stat` through `/proc/<pid>/exe`, which
20//!   resolves to the running image even if its file has since been replaced or
21//!   deleted. A pidfd is opened before either is read and signals go through it
22//!   (`pidfd_send_signal`), so the process that was checked is the process that
23//!   is signalled. No unsafe code is needed: rustix wraps both calls.
24//! - macOS: the start time is `kp_proc.p_starttime` from `sysctl`
25//!   `KERN_PROC_PID` (microseconds since the epoch), and the executable is the
26//!   path `proc_pidpath` reports, then `stat` on that path. These two calls are
27//!   unsafe. macOS has no pidfd, so a signal is a plain
28//!   `kill` sent right after the checks; see [`Process::signal`].
29//! - Windows: creation time and forced stops use one retained process handle.
30//!   `ExecutableCapture` pins the resolved executable until a suspended spawn
31//!   binds its volume and 128-bit file ID to that handle and creation time.
32//! - Anywhere else: [`Process::open`] reports [`std::io::ErrorKind::Unsupported`].
33//!
34//! For persisted PID owners, [`process_identity`] reads versioned kernel start
35//! identities and distinguishes alive, dead and unknown without spawning a
36//! process. Its foreign calls are signal-zero `kill` on Unix and `proc_pidinfo`
37//! on macOS. Only dead owners may be reclaimed; unknown owners stay protected.
38//!
39//! It also reads how much memory and CPU time one process is using, for
40//! reporting only; see [`resource_usage`]. On Linux that is procfs again; on
41//! macOS it is `proc_pid_rusage`, plus `mach_timebase_info` to convert its CPU
42//! times to nanoseconds, the other two unsafe calls in the crate.
43//!
44//! And it carries the launch nonce from the daemon to each module it spawns
45//! over an inherited Unix pipe or a PID-authenticated Windows named pipe:
46//! [`launch_nonce`] is the one cached reader every module uses. Windows keeps
47//! an environment copy for old Windows readers until live source reports show
48//! every module consuming the named pipe.
49
50#![deny(unsafe_code)]
51
52#[cfg(all(unix, feature = "test-support"))]
53pub mod fork_exec_test;
54pub mod launch_nonce;
55pub mod privacy_identity;
56pub mod process_identity;
57#[cfg(windows)]
58pub mod windows_acl;
59#[cfg(unix)]
60pub use launch_nonce::LaunchNonceHandoff;
61pub use launch_nonce::{
62    launch_nonce, LaunchNonce, LaunchNonceError, LaunchNonceSource, LAUNCH_NONCE_ENV,
63    LAUNCH_NONCE_FD, LAUNCH_NONCE_FD_ENV, LAUNCH_NONCE_PIPE_ENV, LAUNCH_NONCE_PIPE_FALLBACK_ENV,
64};
65
66#[cfg(windows)]
67pub use launch_nonce::{LaunchNoncePipeDelivery, LaunchNoncePipeHandoff};
68
69#[cfg(target_os = "linux")]
70mod linux;
71#[cfg(target_os = "macos")]
72mod macos;
73#[cfg(windows)]
74mod windows;
75#[cfg(all(test, windows))]
76mod windows_tests;
77#[cfg(windows)]
78pub use windows::{
79    ExecutableCapture, ImageAgreement, ImageUnavailable, SpawnedImage, WindowsFileIdentity,
80};
81
82#[cfg(target_os = "linux")]
83use linux as platform;
84#[cfg(target_os = "macos")]
85use macos as platform;
86
87use std::{io, path::Path};
88
89/// True where [`Process`] can identify and stop a process by pid.
90pub const PROCESS_IDENTITY_SUPPORTED: bool =
91    cfg!(any(target_os = "linux", target_os = "macos", windows));
92
93/// Device and inode of a file: which file, independent of the name used to
94/// reach it.
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
96pub struct FileIdentity {
97    pub device: u64,
98    pub inode: u64,
99}
100
101/// The device and inode of the file at `path`, following symlinks. `None` if it
102/// cannot be read or the platform has no inode numbers.
103pub fn file_identity(path: &Path) -> Option<FileIdentity> {
104    #[cfg(unix)]
105    {
106        use std::os::unix::fs::MetadataExt;
107
108        std::fs::metadata(path).ok().map(|metadata| FileIdentity {
109            device: metadata.dev(),
110            inode: metadata.ino(),
111        })
112    }
113    #[cfg(not(unix))]
114    {
115        let _ = path;
116        None
117    }
118}
119
120/// What a live process looks like right now.
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub struct Observation {
123    /// The kernel's start time for the process. Opaque: compare it only with a
124    /// value read on the same host by this crate. Linux counts clock ticks since
125    /// boot; macOS counts microseconds since the epoch; Windows counts 100 ns
126    /// intervals since the Windows epoch.
127    pub start_time: u64,
128    /// The file the process is executing, or `None` if it could not be read
129    /// (for example, a process owned by another user).
130    /// Windows uses the full file ID in `SpawnedImage` instead of a Unix inode;
131    /// this field is always `None` there.
132    pub executable: Option<FileIdentity>,
133}
134
135/// Whether the running executable's identity could be read. Permission denial
136/// is distinct on Linux because a non-dumpable process hides its executable
137/// while leaving its start time readable.
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum ExecutableAccess {
140    Readable,
141    PermissionDenied,
142    Unavailable,
143}
144
145/// A signal [`Process::signal`] can send.
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub enum Signal {
148    /// SIGTERM: a request to exit, which the process may handle or ignore.
149    Terminate,
150    /// SIGKILL: ends the process; it cannot be handled or ignored.
151    Kill,
152}
153
154/// The kernel start time of the process holding `pid`, or `None` if there is
155/// none, it has already exited (a zombie waiting to be reaped counts as exited),
156/// or the platform has no source.
157pub fn start_time(pid: u32) -> Option<u64> {
158    #[cfg(any(target_os = "linux", target_os = "macos"))]
159    {
160        platform::start_time(pid)
161    }
162    #[cfg(windows)]
163    {
164        Process::open(pid)
165            .ok()??
166            .observe()
167            .map(|observation| observation.start_time)
168    }
169    #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
170    {
171        let _ = pid;
172        None
173    }
174}
175
176/// True where [`resource_usage`] can read a live process. Elsewhere it always
177/// answers `None`, and a caller can use this to say "not supported here"
178/// rather than "could not read".
179pub const RESOURCE_USAGE_SUPPORTED: bool =
180    cfg!(any(target_os = "linux", target_os = "macos", windows));
181
182/// What [`ResourceUsage::memory_bytes`] measures. The platforms offer
183/// different figures, and they are not interchangeable.
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185pub enum MemoryKind {
186    /// macOS `phys_footprint`: the memory the kernel charges to the process
187    /// (dirty and compressed pages, among others), which is also what jetsam
188    /// acts on. Pages an allocator has released with `MADV_FREE` do not count.
189    PhysFootprint,
190    /// Linux `VmRSS`: pages of the process resident in RAM, including shared
191    /// file-backed pages. Swapped-out pages are not included; see
192    /// [`ResourceUsage::swap_bytes`].
193    ResidentSet,
194    /// Windows `WorkingSetSize`: pageable memory currently resident in RAM,
195    /// including shared pages. This is not Unix RSS or private committed memory.
196    WindowsWorkingSet,
197}
198
199/// One reading of a process's memory and cumulative CPU time.
200///
201/// It covers the process named by the pid alone: its threads are included,
202/// processes it has started are not.
203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
204pub struct ResourceUsage {
205    /// Memory in bytes, measured as [`Self::memory_kind`] says.
206    pub memory_bytes: u64,
207    pub memory_kind: MemoryKind,
208    /// Bytes swapped out (Linux `VmSwap`). `None` where the platform does not
209    /// report it for a single process, which is not the same as zero.
210    pub swap_bytes: Option<u64>,
211    /// CPU time spent in user mode since the process started.
212    pub cpu_user: std::time::Duration,
213    /// CPU time spent in the kernel on the process's behalf since it started.
214    pub cpu_system: std::time::Duration,
215}
216
217/// Memory and cumulative CPU time of the process holding `pid`, read now.
218///
219/// `None` when there is no such process, it has exited (a zombie awaiting its
220/// reap counts as exited), it cannot be read (for example, another user's
221/// process on macOS), or the platform has no source
222/// (see [`RESOURCE_USAGE_SUPPORTED`]). Never a reading of zeros in place of
223/// one of those.
224///
225/// Like any pid-based read, this describes whatever process holds `pid` now;
226/// a caller that needs it to be a particular process should confirm that
227/// process's [`start_time`] around the call.
228pub fn resource_usage(pid: u32) -> Option<ResourceUsage> {
229    #[cfg(any(target_os = "linux", target_os = "macos"))]
230    {
231        platform::resource_usage(pid)
232    }
233    #[cfg(windows)]
234    {
235        Process::open(pid).ok()??.resource_usage()
236    }
237    #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
238    {
239        let _ = pid;
240        None
241    }
242}
243
244/// A handle on the process holding one pid at the moment it was opened.
245#[derive(Debug)]
246pub struct Process {
247    pid: u32,
248    #[cfg(target_os = "linux")]
249    pidfd: Option<std::os::fd::OwnedFd>,
250    #[cfg(windows)]
251    handle: std::os::windows::io::OwnedHandle,
252}
253
254impl Process {
255    /// Open a handle on the process now holding `pid`.
256    ///
257    /// `Ok(None)` means no process holds that pid (on macOS, also a zombie
258    /// awaiting its reap; on Linux a zombie opens, and [`Self::observe`] then
259    /// reports it as exited). On Linux this opens a pidfd,
260    /// which from then on refers to this exact process even if it exits and the
261    /// pid is reused; when the kernel cannot open one (older than 5.3, or a
262    /// seccomp policy refusing the call) the handle falls back to the pid, as
263    /// on macOS.
264    pub fn open(pid: u32) -> io::Result<Option<Self>> {
265        #[cfg(target_os = "linux")]
266        {
267            linux::open(pid).map(|opened| opened.map(|pidfd| Self { pid, pidfd }))
268        }
269        #[cfg(target_os = "macos")]
270        {
271            Ok(platform::exists(pid).then_some(Self { pid }))
272        }
273        #[cfg(windows)]
274        {
275            windows::open(pid).map(|opened| opened.map(|handle| Self { pid, handle }))
276        }
277        #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
278        {
279            let _ = pid;
280            Err(io::Error::new(
281                io::ErrorKind::Unsupported,
282                "process identity is not available on this platform",
283            ))
284        }
285    }
286
287    pub fn pid(&self) -> u32 {
288        self.pid
289    }
290
291    /// True when signals go through a pidfd, so they cannot reach a different
292    /// process that has since reused this pid.
293    pub fn signals_through_pidfd(&self) -> bool {
294        #[cfg(target_os = "linux")]
295        {
296            self.pidfd.is_some()
297        }
298        #[cfg(not(target_os = "linux"))]
299        {
300            false
301        }
302    }
303
304    /// The process's start time and executable, or `None` when it has exited
305    /// (including as a zombie not yet reaped) or cannot be observed. On Linux,
306    /// [`Self::is_alive_but_unobservable`] distinguishes a hidden procfs entry.
307    pub fn observe(&self) -> Option<Observation> {
308        self.observe_with_executable_access()
309            .map(|(observed, _)| observed)
310    }
311
312    /// Whether Linux hides the entire procfs entry while the process still
313    /// exists. This proves existence, not identity, and never authorizes a signal.
314    /// Other platforms return false without changing their observation behavior.
315    pub fn is_alive_but_unobservable(&self) -> bool {
316        #[cfg(target_os = "linux")]
317        {
318            linux::alive_but_unobservable(self.pid, self.pidfd.as_ref())
319        }
320        #[cfg(not(target_os = "linux"))]
321        {
322            false
323        }
324    }
325
326    /// Observe the process and distinguish Linux executable permission denial
327    /// from other failures, without changing the legacy observation's fields.
328    /// Other platforms retain their existing readable/unavailable distinction.
329    pub fn observe_with_executable_access(&self) -> Option<(Observation, ExecutableAccess)> {
330        #[cfg(any(target_os = "linux", target_os = "macos"))]
331        {
332            #[cfg(target_os = "linux")]
333            if !linux::pidfd_alive(self.pidfd.as_ref()) {
334                return None;
335            }
336            let start_time = platform::start_time(self.pid)?;
337            #[cfg(target_os = "linux")]
338            let (executable, access) = match linux::executable_identity(self.pid) {
339                Ok(identity) => (Some(identity), ExecutableAccess::Readable),
340                Err(error) if error.kind() == io::ErrorKind::PermissionDenied => {
341                    (None, ExecutableAccess::PermissionDenied)
342                }
343                Err(_) => (None, ExecutableAccess::Unavailable),
344            };
345            #[cfg(target_os = "macos")]
346            let executable = macos::executable_identity(self.pid);
347            #[cfg(target_os = "macos")]
348            let access = if executable.is_some() {
349                ExecutableAccess::Readable
350            } else {
351                ExecutableAccess::Unavailable
352            };
353            Some((
354                Observation {
355                    start_time,
356                    executable,
357                },
358                access,
359            ))
360        }
361        #[cfg(windows)]
362        {
363            windows::observe(self).map(|observed| (observed, ExecutableAccess::Unavailable))
364        }
365        #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
366        {
367            None
368        }
369    }
370
371    /// Wait on the retained Windows handle. `Ok(false)` means the bound elapsed,
372    /// not that a reused PID was observed.
373    #[cfg(windows)]
374    pub fn wait_for_exit(&self, timeout: std::time::Duration) -> io::Result<bool> {
375        windows::wait(self, timeout)
376    }
377
378    /// Force the confirmed Windows process to stop, then wait on the same handle.
379    /// This is not a graceful termination signal. A different creation time
380    /// refuses before any action; `Ok(false)` means the wait bound elapsed.
381    #[cfg(windows)]
382    pub fn force_stop(
383        &self,
384        expected_start_time: u64,
385        timeout: std::time::Duration,
386    ) -> io::Result<bool> {
387        windows::force_stop(self, expected_start_time, timeout)
388    }
389
390    /// Windows resources read through the retained handle, not by reopening its PID.
391    #[cfg(windows)]
392    pub fn resource_usage(&self) -> Option<ResourceUsage> {
393        windows::resource_usage(self)
394    }
395
396    /// Send `signal` to the process.
397    ///
398    /// With a pidfd the signal can only reach the process this handle was
399    /// opened on: if that process has exited, the call fails with `ESRCH` even
400    /// if the pid has been reused. Without one (macOS, or a Linux kernel with no
401    /// pidfd) the signal goes to whatever holds the pid now, so callers should
402    /// [`Self::observe`] immediately before signalling. What remains is the
403    /// time between that check and this call; for a different process to be
404    /// hit, the checked one must exit, be reaped, and have its pid handed to a
405    /// new process inside that window, and both kernels hand out pids in
406    /// increasing order, so a reuse needs the whole pid space to wrap first.
407    ///
408    /// `Ok(false)` means the process had already exited (`ESRCH`).
409    pub fn signal(&self, signal: Signal) -> io::Result<bool> {
410        #[cfg(any(target_os = "linux", target_os = "macos"))]
411        {
412            #[cfg(target_os = "linux")]
413            let result = linux::signal(self.pid, self.pidfd.as_ref(), signal);
414            #[cfg(target_os = "macos")]
415            let result = macos::signal(self.pid, signal);
416            match result {
417                Ok(()) => Ok(true),
418                Err(rustix::io::Errno::SRCH) => Ok(false),
419                Err(error) => Err(error.into()),
420            }
421        }
422        #[cfg(not(any(target_os = "linux", target_os = "macos")))]
423        {
424            let _ = signal;
425            Err(io::Error::new(
426                io::ErrorKind::Unsupported,
427                "process signalling is not available on this platform",
428            ))
429        }
430    }
431}
432
433#[cfg(all(test, any(target_os = "linux", target_os = "macos")))]
434mod tests {
435    use std::{
436        process::{Child, Command},
437        time::{Duration, Instant},
438    };
439
440    use super::*;
441
442    fn spawn_sleep() -> Child {
443        Command::new("sleep")
444            .arg("60")
445            .spawn()
446            .expect("spawn sleep")
447    }
448
449    /// The executable a spawned `sleep` runs, resolved the way `Command` found it.
450    fn sleep_identity() -> FileIdentity {
451        let path = ["/bin/sleep", "/usr/bin/sleep"]
452            .into_iter()
453            .find(|path| Path::new(path).exists())
454            .expect("sleep is installed");
455        file_identity(Path::new(path)).expect("stat sleep")
456    }
457
458    /// Right after `spawn` returns the child may not have finished exec yet,
459    /// and until then it still runs the test binary's image.
460    fn wait_for_executable(process: &Process, expected: FileIdentity) -> Observation {
461        let deadline = Instant::now() + Duration::from_secs(5);
462        loop {
463            let observation = process.observe().expect("child is alive");
464            if observation.executable == Some(expected) || Instant::now() > deadline {
465                return observation;
466            }
467            std::thread::sleep(Duration::from_millis(10));
468        }
469    }
470
471    #[test]
472    fn own_process_is_observable_with_its_own_image() {
473        let process = Process::open(std::process::id())
474            .expect("open own process")
475            .expect("own process exists");
476        let observation = process.observe().expect("own process is alive");
477        let own_image = file_identity(&std::env::current_exe().unwrap()).unwrap();
478        assert_eq!(observation.executable, Some(own_image));
479        assert_eq!(start_time(std::process::id()), Some(observation.start_time));
480    }
481
482    #[test]
483    fn child_start_time_is_stable_and_differs_from_ours() {
484        let mut child = spawn_sleep();
485        let pid = child.id();
486        let process = Process::open(pid).unwrap().unwrap();
487        let observation = wait_for_executable(&process, sleep_identity());
488        assert_eq!(observation.executable, Some(sleep_identity()));
489        assert_eq!(start_time(pid), Some(observation.start_time));
490        child.kill().unwrap();
491        child.wait().unwrap();
492    }
493
494    #[test]
495    fn a_signalled_and_unreaped_child_reads_as_exited() {
496        let mut child = spawn_sleep();
497        let process = Process::open(child.id()).unwrap().unwrap();
498        assert!(process.signal(Signal::Terminate).unwrap());
499        let deadline = Instant::now() + Duration::from_secs(5);
500        while process.observe().is_some() {
501            assert!(Instant::now() < deadline, "child still observed as alive");
502            std::thread::sleep(Duration::from_millis(10));
503        }
504        // Not yet reaped: the pid is still a zombie here, and still reads as exited.
505        assert_eq!(start_time(child.id()), None);
506        child.wait().unwrap();
507    }
508
509    #[test]
510    fn a_reaped_child_cannot_be_opened_or_observed() {
511        let mut child = spawn_sleep();
512        let pid = child.id();
513        child.kill().unwrap();
514        child.wait().unwrap();
515        // The pid could in principle be reused by now; either way it is not the child.
516        if let Some(process) = Process::open(pid).unwrap() {
517            if let Some(observation) = process.observe() {
518                assert_ne!(observation.executable, Some(sleep_identity()));
519            }
520        }
521    }
522
523    /// The macOS fields are read at fixed offsets, so check the value is a
524    /// plausible start time and not some other field: our own process started
525    /// in the past, and not long ago.
526    #[cfg(target_os = "macos")]
527    #[test]
528    fn macos_start_time_is_microseconds_since_the_epoch() {
529        let now = std::time::SystemTime::now()
530            .duration_since(std::time::UNIX_EPOCH)
531            .unwrap()
532            .as_micros() as u64;
533        let started = start_time(std::process::id()).unwrap();
534        assert!(started <= now, "start time {started} is after now {now}");
535        assert!(
536            now - started < 3_600 * 1_000_000,
537            "start time {started} is more than an hour before now {now}"
538        );
539    }
540
541    /// Keeps one core busy for at least `wall` of wall-clock time.
542    /// This thread's CPU time, from the thread CPU clock rather than the
543    /// process-usage API under test.
544    fn thread_cpu_time() -> Duration {
545        let now = rustix::time::clock_gettime(rustix::time::ClockId::ThreadCPUTime);
546        Duration::new(now.tv_sec as u64, now.tv_nsec as u32)
547    }
548
549    /// Spend `cpu` of this thread's CPU time. Measured on CPU time, not wall
550    /// time: on a loaded machine the thread is descheduled for part of any
551    /// wall interval, so a wall-timed loop can do far less work than its
552    /// duration suggests. A generous wall cap keeps a stalled clock from
553    /// hanging the test.
554    fn burn_cpu(cpu: Duration) {
555        let start = thread_cpu_time();
556        let give_up = Instant::now() + Duration::from_secs(60);
557        let mut value = 0u64;
558        while thread_cpu_time().saturating_sub(start) < cpu {
559            assert!(
560                Instant::now() < give_up,
561                "thread CPU clock stopped advancing"
562            );
563            for step in 0..10_000u64 {
564                value = std::hint::black_box(value.wrapping_mul(31).wrapping_add(step));
565            }
566        }
567        std::hint::black_box(value);
568    }
569
570    #[test]
571    fn own_resource_usage_is_present_and_plausible() {
572        // Clean executable pages need not count toward physical footprint, and
573        // nextest runs this case in a fresh process with little private memory.
574        // Touch and retain private pages so the byte/unit check has a known
575        // lower bound instead of assuming a minimum footprint for the binary.
576        let pages = vec![0xa5u8; 8 * 1024 * 1024];
577        std::hint::black_box(&pages);
578        let usage = resource_usage(std::process::id()).expect("own process is readable");
579        assert!(
580            usage.memory_bytes >= pages.len() as u64,
581            "memory {} bytes cannot account for {} touched private bytes",
582            usage.memory_bytes,
583            pages.len()
584        );
585        std::hint::black_box(&pages);
586        assert!(
587            usage.memory_bytes < 64 * 1024 * 1024 * 1024,
588            "memory {} bytes is implausibly large",
589            usage.memory_bytes
590        );
591        #[cfg(target_os = "macos")]
592        assert_eq!(usage.memory_kind, MemoryKind::PhysFootprint);
593        #[cfg(target_os = "linux")]
594        {
595            assert_eq!(usage.memory_kind, MemoryKind::ResidentSet);
596            assert!(usage.swap_bytes.is_some(), "Linux reports VmSwap");
597        }
598    }
599
600    /// CPU time must grow with busy work, and by roughly the amount of work
601    /// done: a reading in the wrong unit (for example Mach ticks taken as
602    /// nanoseconds on Apple silicon, about 24 times too small) grows too, but
603    /// not by enough.
604    #[test]
605    fn own_cpu_time_grows_by_about_the_busy_work_done() {
606        let pid = std::process::id();
607        let total = |usage: ResourceUsage| usage.cpu_user + usage.cpu_system;
608        let before = total(resource_usage(pid).unwrap());
609        let busy = Duration::from_millis(400);
610        burn_cpu(busy);
611        let after = total(resource_usage(pid).unwrap());
612        let grown = after.saturating_sub(before);
613        // This thread alone spent `busy` of CPU time, so the process total
614        // grew by at least that much; other tests' threads only add to it.
615        // The 10% allowance covers tick rounding in the reading, and is far
616        // tighter than the ~24x a unit error would cause.
617        assert!(
618            grown >= busy * 9 / 10,
619            "cpu time grew by {grown:?} over {busy:?} of busy work"
620        );
621    }
622
623    #[test]
624    fn a_child_reads_its_own_usage_not_ours() {
625        let mut child = spawn_sleep();
626        let process = Process::open(child.id()).unwrap().unwrap();
627        wait_for_executable(&process, sleep_identity());
628        let ours = resource_usage(std::process::id()).unwrap();
629        let usage = resource_usage(child.id()).expect("live child is readable");
630        assert!(usage.memory_bytes > 0);
631        assert!(
632            usage.memory_bytes < ours.memory_bytes,
633            "a sleeping child ({} bytes) should be smaller than the test binary ({} bytes)",
634            usage.memory_bytes,
635            ours.memory_bytes
636        );
637        child.kill().unwrap();
638        child.wait().unwrap();
639    }
640
641    #[test]
642    fn an_exited_child_reads_as_unavailable_not_zero() {
643        let mut child = spawn_sleep();
644        let pid = child.id();
645        child.kill().unwrap();
646        // Killed but not reaped: a zombie, which still has a pid.
647        let deadline = Instant::now() + Duration::from_secs(5);
648        while start_time(pid).is_some() {
649            assert!(Instant::now() < deadline, "child still observed as alive");
650            std::thread::sleep(Duration::from_millis(10));
651        }
652        assert_eq!(resource_usage(pid), None, "a zombie reads as unavailable");
653        child.wait().unwrap();
654        // Reaped: the pid names nothing (barring reuse, which would be some
655        // other live process and so still not a reading of zeros).
656        if let Some(usage) = resource_usage(pid) {
657            assert!(usage.memory_bytes > 0, "a reused pid is some live process");
658        }
659    }
660
661    #[test]
662    fn a_pid_with_no_process_reads_as_unavailable() {
663        // Above both kernels' pid limits (Linux caps pid_max at 2^22, macOS at
664        // 99998), so nothing can hold it.
665        assert_eq!(resource_usage(i32::MAX as u32), None);
666        // Not a representable pid at all.
667        assert_eq!(resource_usage(u32::MAX), None);
668    }
669
670    #[cfg(target_os = "linux")]
671    #[test]
672    fn linux_signals_go_through_a_pidfd() {
673        let mut child = spawn_sleep();
674        let process = Process::open(child.id()).unwrap().unwrap();
675        assert!(process.signals_through_pidfd());
676        child.kill().unwrap();
677        child.wait().unwrap();
678        // The pidfd still names the reaped child, so a signal cannot reach anything else.
679        assert!(!process.signal(Signal::Kill).unwrap());
680    }
681}