Skip to main content

rightkit_child_process/
foreign.rs

1//! Processes this program did not spawn: a pid read from a pid file, a helper another
2//! launcher started, a descendant that outlived its parent.
3//!
4//! [`AdoptedProcess`](crate::AdoptedProcess) can only say a pid is running. A pid alone is a
5//! name the kernel reuses, so signalling a remembered number can reach an unrelated process.
6//! [`AdoptedIdentity`] remembers the pid together with the process's start time (and its
7//! executable path, informational), read while the process was known to be the right one, and
8//! every signal first checks that the pid still names that process:
9//!
10//! * a pid that now names a different process (other start time; the executable path is not
11//!   compared because a process that `exec`s keeps its pid and start time) is refused with [`SignalResult::IdentityMismatch`] and nothing is
12//!   sent;
13//! * a pid that names nothing is [`SignalResult::Gone`];
14//! * this process is never signalled, and neither is a pid that cannot be described.
15//!
16//! The parent pid is deliberately not part of the identity: an orphan is re-parented without
17//! becoming a different process.
18//!
19//! # Limits
20//!
21//! Unix: the check and the `kill` are two system calls. The pid can change hands in the
22//! microseconds between them only if the original process exits and the number wraps, the
23//! gap [`identity`](crate::identity) documents; it cannot be closed from user space. Windows
24//! has no such gap: the check and `TerminateProcess` use one handle. An exited process that
25//! nobody has reaped yet (a Unix zombie) still reads as present until it is reaped.
26//!
27//! Descendants are found by scanning the process table for parent links, so only descendants
28//! whose parent is still alive can be found: once a parent exits, its children are
29//! re-parented away. Take the list ([`AdoptedIdentity::descendants`]) while the root is
30//! alive, and sweep it ([`sweep`]) later; the identities keep the sweep safe.
31
32use std::{
33    io,
34    num::NonZeroU32,
35    path::PathBuf,
36    thread,
37    time::{Duration, Instant},
38};
39
40/// When a process started, in the host's own unit (macOS: seconds and microseconds; Linux:
41/// clock ticks since boot and 0; Windows: 100 ns ticks since 1601 and 0). Comparable only on
42/// the host that produced it.
43#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
44pub struct StartToken {
45    pub major: u64,
46    pub minor: u64,
47}
48
49/// A pid together with the start time and executable path that make it a name for exactly one
50/// process. See the module documentation.
51#[derive(Clone, Debug, Eq, PartialEq)]
52pub struct AdoptedIdentity {
53    pid: NonZeroU32,
54    start: StartToken,
55    exe: Option<PathBuf>,
56}
57
58/// What a pid names now, compared with a remembered identity.
59#[derive(Clone, Copy, Debug, Eq, PartialEq)]
60pub enum IdentityCheck {
61    /// The pid still names the remembered process.
62    Same,
63    /// No process has this pid.
64    Gone,
65    /// The pid names a different process.
66    Replaced,
67}
68
69/// How hard to stop a process. Windows has one way to stop a process, so both are the same
70/// there.
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub enum AdoptedSignal {
73    /// `SIGTERM`.
74    Graceful,
75    /// `SIGKILL`.
76    Force,
77}
78
79/// The outcome of [`AdoptedIdentity::signal`].
80#[derive(Clone, Copy, Debug, Eq, PartialEq)]
81pub enum SignalResult {
82    /// The identity matched and the signal was sent.
83    Delivered,
84    /// No process has the pid any more.
85    Gone,
86    /// The pid names a different process; nothing was sent.
87    IdentityMismatch,
88}
89
90/// The result of [`sweep`], as pids.
91#[derive(Clone, Debug, Default, Eq, PartialEq)]
92pub struct SweepReport {
93    /// Signalled and no longer present.
94    pub terminated: Vec<u32>,
95    /// Already absent when the sweep reached them.
96    pub gone: Vec<u32>,
97    /// The pid names a different process now; nothing was sent.
98    pub refused: Vec<u32>,
99    /// Signalled (forced) and still present at the end of the sweep.
100    pub survivors: Vec<u32>,
101    /// The signal could not be sent (for example, permission denied).
102    pub failed: Vec<u32>,
103}
104
105impl AdoptedIdentity {
106    /// Reads the identity of `pid`. `Ok(None)` when no process has that pid. The caller is
107    /// responsible for `pid` naming the intended process at this moment (it was just spawned
108    /// by a launcher, or it was just read from the owner's own record).
109    pub fn capture(pid: NonZeroU32) -> io::Result<Option<Self>> {
110        Ok(sys::read(pid.get())?.map(|seen| Self {
111            pid,
112            start: seen.start,
113            exe: seen.exe,
114        }))
115    }
116
117    /// Rebuilds an identity persisted earlier (a pid file that also records
118    /// [`start_token`](Self::start_token) and [`exe`](Self::exe)).
119    pub fn from_recorded(pid: NonZeroU32, start: StartToken, exe: Option<PathBuf>) -> Self {
120        Self { pid, start, exe }
121    }
122
123    pub fn pid(&self) -> u32 {
124        self.pid.get()
125    }
126
127    pub fn start_token(&self) -> StartToken {
128        self.start
129    }
130
131    pub fn exe(&self) -> Option<&std::path::Path> {
132        self.exe.as_deref()
133    }
134
135    fn matches(&self, seen: &sys::Seen) -> bool {
136        // pid plus start time is the identity. A process keeps both across `exec`, and the
137        // executable path changes then (on macOS `/bin/sh` and other launchers re-exec the
138        // real program after the pid was captured), so the path is recorded for the caller
139        // but never compared: a recycled pid has a different start time.
140        seen.start == self.start
141    }
142
143    /// Whether the pid still names the remembered process.
144    pub fn verify(&self) -> io::Result<IdentityCheck> {
145        Ok(match sys::read(self.pid())? {
146            None => IdentityCheck::Gone,
147            Some(seen) if self.matches(&seen) => IdentityCheck::Same,
148            Some(_) => IdentityCheck::Replaced,
149        })
150    }
151
152    /// Sends `signal` only if the pid still names the remembered process. Never signals this
153    /// process: that is `InvalidInput`.
154    pub fn signal(&self, signal: AdoptedSignal) -> io::Result<SignalResult> {
155        if self.pid() == std::process::id() {
156            return Err(io::Error::new(
157                io::ErrorKind::InvalidInput,
158                "refusing to signal this process",
159            ));
160        }
161        sys::signal(self, signal)
162    }
163
164    /// The descendants of this process, parents before children, each with its own identity.
165    /// Empty when this process is gone or the pid now names a different one. A child is
166    /// accepted only if it did not start before its parent, so a stale parent link to a
167    /// recycled pid adds nothing.
168    pub fn descendants(&self) -> io::Result<Vec<AdoptedIdentity>> {
169        if self.verify()? != IdentityCheck::Same {
170            return Ok(Vec::new());
171        }
172        let pairs = sys::process_pairs()?;
173        let mut found: Vec<AdoptedIdentity> = Vec::new();
174        // (pid, start) of every process whose children are still to be looked up.
175        let mut frontier = vec![(self.pid(), self.start)];
176        let mut next = 0;
177        while next < frontier.len() {
178            let (parent, parent_start) = frontier[next];
179            next += 1;
180            for &(child, claimed_parent) in &pairs {
181                if claimed_parent != parent
182                    || child == parent
183                    || child == self.pid()
184                    || frontier.iter().any(|(pid, _)| *pid == child)
185                {
186                    continue;
187                }
188                let (Some(pid), Ok(Some(seen))) = (NonZeroU32::new(child), sys::read(child)) else {
189                    continue;
190                };
191                if seen.parent.is_some_and(|actual| actual != parent) || seen.start < parent_start {
192                    continue; // the pid changed hands since the table was read
193                }
194                frontier.push((child, seen.start));
195                found.push(AdoptedIdentity {
196                    pid,
197                    start: seen.start,
198                    exe: seen.exe,
199                });
200            }
201        }
202        // The root must have stayed itself for the whole scan.
203        if self.verify()? != IdentityCheck::Same {
204            return Ok(Vec::new());
205        }
206        Ok(found)
207    }
208}
209
210const SWEEP_POLL: Duration = Duration::from_millis(20);
211const FORCE_WAIT: Duration = Duration::from_millis(500);
212
213/// Stops every process in `targets` that is still the process it was remembered as: a graceful
214/// signal to each, up to `grace` for them to leave, then a forced signal to those still
215/// present. A target whose pid names a different process is `refused`, never signalled.
216/// Targets are signalled in the order given; pass leaves first (the reverse of
217/// [`AdoptedIdentity::descendants`]).
218pub fn sweep(targets: &[AdoptedIdentity], grace: Duration) -> io::Result<SweepReport> {
219    let mut report = SweepReport::default();
220    let mut pending: Vec<&AdoptedIdentity> = Vec::new();
221    for target in targets {
222        match target.signal(AdoptedSignal::Graceful) {
223            Ok(SignalResult::Delivered) => pending.push(target),
224            Ok(SignalResult::Gone) => report.gone.push(target.pid()),
225            Ok(SignalResult::IdentityMismatch) => report.refused.push(target.pid()),
226            Err(_) => report.failed.push(target.pid()),
227        }
228    }
229    drain(&mut pending, grace, &mut report)?;
230    let mut forced: Vec<&AdoptedIdentity> = Vec::new();
231    for target in pending {
232        match target.signal(AdoptedSignal::Force) {
233            Ok(SignalResult::Delivered) => forced.push(target),
234            Ok(SignalResult::Gone) => report.terminated.push(target.pid()),
235            Ok(SignalResult::IdentityMismatch) => report.refused.push(target.pid()),
236            Err(_) => report.failed.push(target.pid()),
237        }
238    }
239    drain(&mut forced, FORCE_WAIT, &mut report)?;
240    report
241        .survivors
242        .extend(forced.iter().map(|target| target.pid()));
243    Ok(report)
244}
245
246/// [`sweep`] of `root`'s current descendants, leaves first. The root itself is left alone.
247pub fn sweep_descendants(root: &AdoptedIdentity, grace: Duration) -> io::Result<SweepReport> {
248    let mut targets = root.descendants()?;
249    targets.reverse();
250    sweep(&targets, grace)
251}
252
253/// Waits until `deadline` for each pending target to stop being the remembered process; those
254/// that did are moved to `report.terminated`.
255fn drain(
256    pending: &mut Vec<&AdoptedIdentity>,
257    wait: Duration,
258    report: &mut SweepReport,
259) -> io::Result<()> {
260    let deadline = Instant::now().checked_add(wait);
261    loop {
262        let mut still = Vec::new();
263        for target in pending.drain(..) {
264            if target.verify()? == IdentityCheck::Same {
265                still.push(target);
266            } else {
267                report.terminated.push(target.pid());
268            }
269        }
270        *pending = still;
271        let expired = deadline.is_none_or(|limit| Instant::now() >= limit);
272        if pending.is_empty() || expired {
273            return Ok(());
274        }
275        thread::sleep(SWEEP_POLL);
276    }
277}
278
279mod sys {
280    use super::StartToken;
281    #[cfg(not(windows))]
282    use super::{AdoptedIdentity, AdoptedSignal, SignalResult};
283    #[cfg(not(windows))]
284    use std::io;
285    use std::path::PathBuf;
286
287    /// One look at a process.
288    pub(super) struct Seen {
289        pub(super) start: StartToken,
290        pub(super) exe: Option<PathBuf>,
291        /// The parent pid when the host reports it with the process (Unix); Windows reads it
292        /// from the process table only.
293        pub(super) parent: Option<u32>,
294    }
295
296    #[cfg(unix)]
297    pub(super) fn read(pid: u32) -> io::Result<Option<Seen>> {
298        Ok(crate::identity::describe(pid)?.map(|identity| Seen {
299            start: StartToken {
300                major: identity.started_sec,
301                minor: identity.started_usec,
302            },
303            exe: exe_path(pid),
304            parent: Some(identity.parent),
305        }))
306    }
307
308    #[cfg(target_os = "macos")]
309    fn exe_path(pid: u32) -> Option<PathBuf> {
310        use std::os::unix::ffi::OsStringExt;
311        // PROC_PIDPATHINFO_MAXSIZE is 4 * MAXPATHLEN.
312        let mut buffer = vec![0u8; 4096];
313        // SAFETY: the buffer is writable for exactly `buffer.len()` bytes.
314        let written = unsafe {
315            libc::proc_pidpath(
316                pid as libc::c_int,
317                buffer.as_mut_ptr().cast(),
318                buffer.len() as u32,
319            )
320        };
321        if written <= 0 {
322            return None;
323        }
324        buffer.truncate(written as usize);
325        Some(PathBuf::from(std::ffi::OsString::from_vec(buffer)))
326    }
327
328    #[cfg(any(target_os = "linux", target_os = "android"))]
329    fn exe_path(pid: u32) -> Option<PathBuf> {
330        std::fs::read_link(format!("/proc/{pid}/exe")).ok()
331    }
332
333    #[cfg(all(
334        unix,
335        not(any(target_os = "macos", target_os = "linux", target_os = "android"))
336    ))]
337    fn exe_path(_pid: u32) -> Option<PathBuf> {
338        None
339    }
340
341    #[cfg(unix)]
342    pub(super) fn signal(
343        identity: &AdoptedIdentity,
344        signal: AdoptedSignal,
345    ) -> io::Result<SignalResult> {
346        use super::IdentityCheck;
347        match identity.verify()? {
348            IdentityCheck::Gone => return Ok(SignalResult::Gone),
349            IdentityCheck::Replaced => return Ok(SignalResult::IdentityMismatch),
350            IdentityCheck::Same => {}
351        }
352        let pid = i32::try_from(identity.pid()).map_err(io::Error::other)?;
353        let number = match signal {
354            AdoptedSignal::Graceful => libc::SIGTERM,
355            AdoptedSignal::Force => libc::SIGKILL,
356        };
357        // SAFETY: kill takes two integers; the pid was proven to be the remembered process
358        // just above (see the module documentation for the residual gap).
359        if unsafe { libc::kill(pid, number) } == 0 {
360            return Ok(SignalResult::Delivered);
361        }
362        let error = io::Error::last_os_error();
363        match error.raw_os_error() {
364            Some(libc::ESRCH) => Ok(SignalResult::Gone),
365            _ => Err(error),
366        }
367    }
368
369    /// Every (pid, parent pid) pair in the process table.
370    #[cfg(unix)]
371    pub(super) fn process_pairs() -> io::Result<Vec<(u32, u32)>> {
372        let mut pairs = Vec::new();
373        for pid in list_pids()? {
374            if let Ok(Some(identity)) = crate::identity::describe(pid) {
375                pairs.push((pid, identity.parent));
376            }
377        }
378        Ok(pairs)
379    }
380
381    #[cfg(target_os = "macos")]
382    fn list_pids() -> io::Result<Vec<u32>> {
383        /// `PROC_ALL_PIDS` from `<libproc.h>`.
384        const PROC_ALL_PIDS: u32 = 1;
385        let size = std::mem::size_of::<libc::pid_t>();
386        // SAFETY: a null buffer asks only for the size needed.
387        let needed = unsafe { libc::proc_listpids(PROC_ALL_PIDS, 0, std::ptr::null_mut(), 0) };
388        if needed <= 0 {
389            return Err(io::Error::last_os_error());
390        }
391        // Room for processes that start between the two calls.
392        let mut pids = vec![0 as libc::pid_t; needed as usize / size + 64];
393        // SAFETY: the buffer holds `pids.len()` pids and the byte size passed says so.
394        let written = unsafe {
395            libc::proc_listpids(
396                PROC_ALL_PIDS,
397                0,
398                pids.as_mut_ptr().cast(),
399                (pids.len() * size) as libc::c_int,
400            )
401        };
402        if written <= 0 {
403            return Err(io::Error::last_os_error());
404        }
405        pids.truncate(written as usize / size);
406        Ok(pids
407            .into_iter()
408            .filter(|pid| *pid > 0)
409            .map(|pid| pid as u32)
410            .collect())
411    }
412
413    #[cfg(any(target_os = "linux", target_os = "android"))]
414    fn list_pids() -> io::Result<Vec<u32>> {
415        Ok(std::fs::read_dir("/proc")?
416            .flatten()
417            .filter_map(|entry| entry.file_name().to_str()?.parse::<u32>().ok())
418            .collect())
419    }
420
421    #[cfg(all(
422        unix,
423        not(any(target_os = "macos", target_os = "linux", target_os = "android"))
424    ))]
425    fn list_pids() -> io::Result<Vec<u32>> {
426        Err(io::Error::from(io::ErrorKind::Unsupported))
427    }
428
429    #[cfg(windows)]
430    mod windows_impl {
431        use super::super::{AdoptedIdentity, AdoptedSignal, SignalResult, StartToken};
432        use super::Seen;
433        use crate::process_windows::os_error;
434        use std::{io, path::PathBuf};
435        use windows::{
436            core::PWSTR,
437            Win32::{
438                Foundation::{CloseHandle, ERROR_INVALID_PARAMETER, FILETIME, HANDLE},
439                System::{
440                    Diagnostics::ToolHelp::{
441                        CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
442                        TH32CS_SNAPPROCESS,
443                    },
444                    Threading::{
445                        GetProcessTimes, OpenProcess, QueryFullProcessImageNameW, TerminateProcess,
446                        WaitForSingleObject, PROCESS_ACCESS_RIGHTS, PROCESS_NAME_WIN32,
447                        PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_SYNCHRONIZE, PROCESS_TERMINATE,
448                    },
449                },
450            },
451        };
452
453        /// A kernel handle closed on drop.
454        struct Owned(HANDLE);
455        impl Drop for Owned {
456            fn drop(&mut self) {
457                // SAFETY: the handle came from a successful open or snapshot and is closed once.
458                let _ = unsafe { CloseHandle(self.0) };
459            }
460        }
461
462        fn open(pid: u32, access: PROCESS_ACCESS_RIGHTS) -> io::Result<Option<Owned>> {
463            // SAFETY: OpenProcess takes plain values and returns a new handle.
464            match unsafe { OpenProcess(access, false, pid) } {
465                Ok(handle) => Ok(Some(Owned(handle))),
466                Err(error) if error.code() == ERROR_INVALID_PARAMETER.to_hresult() => Ok(None),
467                Err(error) => Err(os_error(error)),
468            }
469        }
470
471        /// `None` once the process has exited (its handle can outlive it).
472        fn observe(process: &Owned) -> io::Result<Option<Seen>> {
473            // SAFETY: the handle is open with SYNCHRONIZE.
474            if unsafe { WaitForSingleObject(process.0, 0) }.0 == 0 {
475                return Ok(None);
476            }
477            let (mut created, mut exited, mut kernel, mut user) = (
478                FILETIME::default(),
479                FILETIME::default(),
480                FILETIME::default(),
481                FILETIME::default(),
482            );
483            // SAFETY: the handle is open with QUERY_LIMITED_INFORMATION; the outputs are valid.
484            unsafe {
485                GetProcessTimes(process.0, &mut created, &mut exited, &mut kernel, &mut user)
486            }
487            .map_err(os_error)?;
488            let ticks =
489                (u64::from(created.dwHighDateTime) << 32) | u64::from(created.dwLowDateTime);
490            let mut buffer = vec![0u16; 32768];
491            let mut length = buffer.len() as u32;
492            // SAFETY: the buffer holds `length` UTF-16 units.
493            let exe = unsafe {
494                QueryFullProcessImageNameW(
495                    process.0,
496                    PROCESS_NAME_WIN32,
497                    PWSTR(buffer.as_mut_ptr()),
498                    &mut length,
499                )
500            }
501            .ok()
502            .map(|()| PathBuf::from(String::from_utf16_lossy(&buffer[..length as usize])));
503            Ok(Some(Seen {
504                start: StartToken {
505                    major: ticks,
506                    minor: 0,
507                },
508                exe,
509                parent: None,
510            }))
511        }
512
513        pub(in super::super) fn read(pid: u32) -> io::Result<Option<Seen>> {
514            let access = PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE;
515            match open(pid, access)? {
516                Some(process) => observe(&process),
517                None => Ok(None),
518            }
519        }
520
521        pub(in super::super) fn signal(
522            identity: &AdoptedIdentity,
523            _signal: AdoptedSignal,
524        ) -> io::Result<SignalResult> {
525            let access =
526                PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE | PROCESS_TERMINATE;
527            let Some(process) = open(identity.pid(), access)? else {
528                return Ok(SignalResult::Gone);
529            };
530            // Proof and act use the same handle: the pid cannot change hands in between.
531            let Some(seen) = observe(&process)? else {
532                return Ok(SignalResult::Gone);
533            };
534            if !identity.matches(&seen) {
535                return Ok(SignalResult::IdentityMismatch);
536            }
537            // SAFETY: the handle is open with PROCESS_TERMINATE.
538            unsafe { TerminateProcess(process.0, 1) }.map_err(os_error)?;
539            Ok(SignalResult::Delivered)
540        }
541
542        pub(in super::super) fn process_pairs() -> io::Result<Vec<(u32, u32)>> {
543            // SAFETY: a snapshot request takes plain values.
544            let snapshot = Owned(
545                unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }.map_err(os_error)?,
546            );
547            // SAFETY: PROCESSENTRY32W is plain data; dwSize is set below as the API requires.
548            let mut entry: PROCESSENTRY32W = unsafe { std::mem::zeroed() };
549            entry.dwSize = std::mem::size_of::<PROCESSENTRY32W>() as u32;
550            // SAFETY: the snapshot is open and `entry` is a valid, sized entry.
551            unsafe { Process32FirstW(snapshot.0, &mut entry) }.map_err(os_error)?;
552            let mut pairs = Vec::new();
553            loop {
554                pairs.push((entry.th32ProcessID, entry.th32ParentProcessID));
555                // SAFETY: as above.
556                if unsafe { Process32NextW(snapshot.0, &mut entry) }.is_err() {
557                    break;
558                }
559            }
560            Ok(pairs)
561        }
562    }
563
564    #[cfg(windows)]
565    pub(super) use windows_impl::{process_pairs, read, signal};
566
567    #[cfg(not(any(unix, windows)))]
568    pub(super) fn read(_pid: u32) -> io::Result<Option<Seen>> {
569        Err(io::Error::from(io::ErrorKind::Unsupported))
570    }
571    #[cfg(not(any(unix, windows)))]
572    pub(super) fn signal(_: &AdoptedIdentity, _: AdoptedSignal) -> io::Result<SignalResult> {
573        Err(io::Error::from(io::ErrorKind::Unsupported))
574    }
575    #[cfg(not(any(unix, windows)))]
576    pub(super) fn process_pairs() -> io::Result<Vec<(u32, u32)>> {
577        Err(io::Error::from(io::ErrorKind::Unsupported))
578    }
579}