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