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}