Skip to main content

processkit/
lookup.rs

1//! Free-standing identity & reuse-safe liveness queries for an **arbitrary** pid
2//! — one the caller holds *outside* any [`ProcessGroup`](crate::ProcessGroup).
3//!
4//! [`process_info`] answers "does this pid name a process, and what is it?" with
5//! the same best-effort fields a group member carries in a
6//! [`MemberInfo`]; [`process_is_alive`] answers "is the *same*
7//! process I saw earlier still running?" — reuse-safe, by pairing the pid with the
8//! start-time token, so a recycled number is not mistaken for the original.
9//!
10//! Both reuse the crate's existing per-platform readers rather than a second
11//! implementation, and both keep the crate's standing rules: **never** read a
12//! process's argv/environment, and honestly tell **"no such process"** (a negative
13//! answer) apart from **"not allowed to look"** (an error).
14
15use crate::member::MemberInfo;
16use crate::{Error, Result};
17
18/// Look up the identity and best-effort metadata of an **arbitrary** process by
19/// pid — the standalone companion to
20/// [`ProcessGroup::members_info`](crate::ProcessGroup::members_info), for a pid the
21/// caller holds *outside* any group (a pid saved to disk across runs, a launch
22/// registry, an e2e probe watching a process from outside its container).
23///
24/// Returns the very fields a group member's [`MemberInfo`] carries — parent pid,
25/// image name, and the start-time identity token — read through the **same**
26/// per-platform readers (`/proc/<pid>/stat` on Linux, `proc_pidinfo` on macOS,
27/// `Toolhelp32` + the creation `FILETIME` on Windows), with the same honest
28/// `Option` policy: a field the platform can't report is `None`, never fabricated.
29///
30/// # The three outcomes
31///
32/// - **`Ok(Some(info))`** — the process exists; inspect it via
33///   [`MemberInfo::ppid`], [`exe_name`](MemberInfo::exe_name), and
34///   [`start_time`](MemberInfo::start_time) (each `None` where unavailable).
35/// - **`Ok(None)`** — the pid names **no** process. An honest negative, *not* an
36///   error: this is the "it's gone" answer a liveness check wants.
37/// - **`Err`** — the process may well exist, but its state couldn't be determined:
38///   the caller lacks permission to inspect it, or the OS read failed. **Never**
39///   read this as "dead" — that is the whole reason it is an error rather than
40///   `Ok(None)`.
41///
42/// # No command line
43///
44/// The raw argv / environment is **deliberately never** read, on any platform — a
45/// command line routinely carries secrets, and redaction is the consumer's policy
46/// to own (the crate's standing "never argv/env" stance, the same one
47/// [`MemberInfo`] documents).
48///
49/// # Point-in-time
50///
51/// A snapshot taken now: the process may exit immediately afterwards, and the pid
52/// is only as stable as the OS's reuse policy. To tell a *recycled* number apart
53/// from the original process later, pair the returned
54/// [`start_time`](MemberInfo::start_time) with the pid and use
55/// [`process_is_alive`] — do not trust the bare number.
56///
57/// # Platform notes
58///
59/// - **Linux / Android** — one `/proc/<pid>/stat` read; world-readable for other
60///   users' processes on a default mount, so a foreign process is reported. A
61///   `hidepid` mount that denies the read surfaces as `Err`, not a false "gone".
62/// - **Windows** — `OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)` is the
63///   existence/permission oracle (the least-privilege query right, grantable across
64///   sessions and integrity levels for ordinary processes); ppid and image name
65///   come from one system-wide `Toolhelp32` snapshot. A **protected / higher-integrity
66///   process** (an anti-malware PPL, the `System` process) the caller may not query
67///   yields `Err` (access denied), distinct from a non-existent pid's `Ok(None)`.
68/// - **macOS** — one `proc_pidinfo(PROC_PIDTBSDINFO)` fill; a process the caller may
69///   not inspect yields `Err`, a gone pid `Ok(None)`.
70/// - **the bare BSDs** — no per-process reader is wired up, so existence is probed
71///   with a zero-signal `kill(pid, 0)` and the pid is reported with every enriching
72///   field `None` (`Ok(Some(_))`). That is a correct best-effort result, not an
73///   error, and never a false "gone".
74///
75/// # Errors
76///
77/// [`ErrorReason::Io`](crate::ErrorReason::Io) when the process may exist but couldn't be inspected — a
78/// permission denial (a Windows protected process, a Linux `hidepid` mount, a macOS
79/// restricted process) or another OS read failure.
80///
81/// # Examples
82///
83/// ```no_run
84/// # fn main() -> processkit::Result<()> {
85/// let pid = 4321;
86/// match processkit::process_info(pid)? {
87///     Some(info) => println!(
88///         "pid={} ppid={:?} exe={:?} start={:?}",
89///         info.pid(),
90///         info.ppid(),
91///         info.exe_name(),
92///         info.start_time(),
93///     ),
94///     None => println!("pid {pid} is not running"),
95/// }
96/// # Ok(())
97/// # }
98/// ```
99pub fn process_info(pid: u32) -> Result<Option<MemberInfo>> {
100    crate::sys::process_info(pid).map_err(Error::io)
101}
102
103/// Reuse-safe liveness: is the process at `pid` **still the same instance** you saw
104/// earlier — the one whose [`start_time`](MemberInfo::start_time) you saved?
105///
106/// Pass the pid together with the `start_time` token from an earlier
107/// [`process_info`] (or [`MemberInfo::start_time`]). Because the OS reuses pid
108/// *numbers*, a bare pid check would answer "alive" for a stranger that recycled
109/// the number after your process exited; pairing it with the start time — fixed at
110/// creation and distinct for a later occupant — tells the original apart from a
111/// recycled number. This is the same anti-reuse discipline the crate applies
112/// internally to its own kills and stats reads, exposed for a pid you hold.
113///
114/// # Result
115///
116/// - **`Ok(true)`** — the process at `pid` exists **and** its current start time
117///   matches the one you saved: your process is still running.
118/// - **`Ok(false)`** — the process is gone: either the pid names nothing, or it
119///   names a **different** process now (a recycled number — the start times
120///   differ), which means *your* process is no longer alive.
121/// - **`Err`** — the pid may name a live process but it couldn't be inspected
122///   (permission denied, or an OS read failure). As with [`process_info`], never
123///   read this as "dead".
124///
125/// # Reuse protection degrades honestly
126///
127/// The recycle check needs a start-time token on **both** sides. When one is
128/// missing it can't *prove* a recycle, so it degrades to bare-pid liveness — a live
129/// process at the number reads as `Ok(true)`:
130/// - `start_time` is `None` (you saved no token — e.g. it originated on a
131///   [bare BSD](process_info#platform-notes), which reports none), or
132/// - the platform can't report a current token for the live process at `pid` (the
133///   same structural `None`).
134///
135/// So on platforms that *do* report a start time (Windows, Linux, macOS), passing
136/// the saved `Some(token)` gives full reuse protection; on a platform that reports
137/// none, this is exactly the number-only liveness a caller would otherwise write by
138/// hand — no weaker, and never a false "dead".
139///
140/// # Errors
141///
142/// [`ErrorReason::Io`](crate::ErrorReason::Io) when the pid may name a live process but couldn't be inspected —
143/// the same permission/OS-error surface as [`process_info`].
144///
145/// # Examples
146///
147/// ```no_run
148/// # fn main() -> processkit::Result<()> {
149/// // Earlier: record a process's identity.
150/// let pid = 4321;
151/// let saved_start = processkit::process_info(pid)?.and_then(|i| i.start_time());
152///
153/// // Later (perhaps after a restart): is that same process still running?
154/// if processkit::process_is_alive(pid, saved_start)? {
155///     println!("the original process {pid} is still alive");
156/// } else {
157///     println!("process {pid} is gone (exited, or its number was recycled)");
158/// }
159/// # Ok(())
160/// # }
161/// ```
162pub fn process_is_alive(pid: u32, start_time: Option<u64>) -> Result<bool> {
163    match process_info(pid)? {
164        // No process at the number — the honest "gone" answer.
165        None => Ok(false),
166        // A process is present: it is the same instance iff the start-time tokens
167        // agree (or the check degrades to bare-pid liveness when a token is
168        // missing — see `same_process_instance`).
169        Some(info) => Ok(same_process_instance(start_time, info.start_time())),
170    }
171}
172
173/// Reuse-safe identity comparison for a process known to be **present** at the pid:
174/// decide whether the live process is the *same instance* the caller saved.
175///
176/// - Both tokens known → the instances match iff they are **equal** (a difference
177///   is positive proof the number was recycled by a different process).
178/// - Either token `None` → a recycle cannot be *proven*, so degrade to bare-pid
179///   liveness: the process at the number is live, so report `true`. This mirrors
180///   the crate's internal `is_recycled` stance (`None` is never proof) and keeps
181///   the bare-BSD / number-only path exactly as strong as a hand-written liveness
182///   check — never a false "dead".
183///
184/// Pure and platform-agnostic, so the reuse discipline is unit-tested directly
185/// (the modelled "same pid, different start time" case) without waiting on a real
186/// pid recycle.
187fn same_process_instance(expected: Option<u64>, current: Option<u64>) -> bool {
188    match (expected, current) {
189        (Some(a), Some(b)) => a == b,
190        _ => true,
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::same_process_instance;
197
198    #[test]
199    fn matching_tokens_are_the_same_live_instance() {
200        // Same pid, same start time → the original process, still alive.
201        assert!(same_process_instance(Some(987_654_321), Some(987_654_321)));
202    }
203
204    #[test]
205    fn differing_tokens_model_a_recycled_number() {
206        // The modelled pid-reuse case (no real recycle needed): the number is live
207        // but its current start time differs from the saved one, so a *different*
208        // process holds it now — the saved instance is gone.
209        assert!(!same_process_instance(Some(987_654_321), Some(123_456_789)));
210        // Symmetric: order of the two tokens must not matter to the verdict.
211        assert!(!same_process_instance(Some(123_456_789), Some(987_654_321)));
212    }
213
214    #[test]
215    fn a_missing_token_degrades_to_bare_pid_liveness() {
216        // No saved token (e.g. a bare-BSD origin), or the platform reports none for
217        // the live process now: a recycle can't be proven, so a live process at the
218        // number reads as "same/alive" — never a false "dead".
219        assert!(same_process_instance(None, Some(987_654_321)));
220        assert!(same_process_instance(Some(987_654_321), None));
221        assert!(same_process_instance(None, None));
222    }
223}