1use serde::{Deserialize, Serialize};
7
8#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(tag = "platform", rename_all = "snake_case")]
10pub enum ProcessStart {
11 Linux { ticks: u64 },
12 Darwin { seconds: u64, microseconds: u32 },
13}
14
15#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
16pub struct ProcessIdentity {
17 pub boot_id: String,
18 pub start: ProcessStart,
19}
20
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub enum ProcessObservation {
23 Present(ProcessIdentity),
24 Absent,
25 Unknown,
26}
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum OwnerPresence {
30 Live,
31 Absent,
32 Unknown,
33}
34
35impl ProcessIdentity {
36 pub fn is_valid(&self) -> bool {
37 uuid::Uuid::parse_str(&self.boot_id).is_ok_and(|uuid| uuid.to_string() == self.boot_id)
38 && match self.start {
39 ProcessStart::Linux { .. } => true,
40 ProcessStart::Darwin {
41 seconds,
42 microseconds,
43 } => seconds > 0 && microseconds < 1_000_000,
44 }
45 }
46
47 pub fn to_record(&self) -> Option<String> {
50 self.is_valid().then(|| match self.start {
51 ProcessStart::Linux { ticks } => format!("{}:{ticks}", self.boot_id),
52 ProcessStart::Darwin {
53 seconds,
54 microseconds,
55 } => format!("{}:darwin:{seconds}:{microseconds}", self.boot_id),
56 })
57 }
58
59 pub fn from_record(record: &str) -> Option<Self> {
60 let (boot, marker) = record.split_once(':')?;
61 let start = if let Some(time) = marker.strip_prefix("darwin:") {
62 let (seconds, microseconds) = time.split_once(':')?;
63 ProcessStart::Darwin {
64 seconds: decimal(seconds)?,
65 microseconds: u32::try_from(decimal(microseconds)?).ok()?,
66 }
67 } else {
68 ProcessStart::Linux {
69 ticks: decimal(marker)?,
70 }
71 };
72 let identity = Self {
73 boot_id: boot.into(),
74 start,
75 };
76 identity.is_valid().then_some(identity)
77 }
78
79 pub fn presence(&self, observation: &ProcessObservation) -> OwnerPresence {
80 if !self.is_valid() {
81 return OwnerPresence::Unknown;
82 }
83 match observation {
84 ProcessObservation::Absent => OwnerPresence::Absent,
85 ProcessObservation::Unknown => OwnerPresence::Unknown,
86 ProcessObservation::Present(current) => {
87 if !current.is_valid()
88 || std::mem::discriminant(&self.start) != std::mem::discriminant(¤t.start)
89 {
90 return OwnerPresence::Unknown;
91 }
92 if self == current {
93 OwnerPresence::Live
94 } else {
95 OwnerPresence::Absent
96 }
97 }
98 }
99 }
100}
101
102fn decimal(value: &str) -> Option<u64> {
103 (!value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit()))
104 .then(|| value.parse().ok())
105 .flatten()
106}
107
108pub fn owner_presence(pid: u32, expected: Option<&ProcessIdentity>) -> OwnerPresence {
111 if pid <= 1 || i32::try_from(pid).is_err() {
112 return OwnerPresence::Unknown;
113 }
114 observed_owner_presence(expected, &observe_process(pid))
115}
116
117fn observed_owner_presence(
118 expected: Option<&ProcessIdentity>,
119 observed: &ProcessObservation,
120) -> OwnerPresence {
121 if *observed == ProcessObservation::Absent {
122 return OwnerPresence::Absent;
123 }
124 expected.map_or(OwnerPresence::Unknown, |identity| {
125 identity.presence(observed)
126 })
127}
128
129pub fn current_process_identity() -> Option<ProcessIdentity> {
132 #[cfg(target_os = "linux")]
133 let observation = linux_stat(
134 &std::fs::read_to_string("/proc/self/stat").ok()?,
135 std::process::id(),
136 &boot_identity()?,
137 );
138 #[cfg(not(target_os = "linux"))]
139 let observation = observe_process(std::process::id());
140 match observation {
141 ProcessObservation::Present(identity) => Some(identity),
142 _ => None,
143 }
144}
145
146#[cfg(target_os = "linux")]
147fn boot_identity() -> Option<String> {
148 let boot = std::fs::read_to_string("/proc/sys/kernel/random/boot_id").ok()?;
149 let boot = boot.trim();
150 uuid::Uuid::parse_str(boot).ok()?;
151 Some(boot.into())
152}
153
154#[cfg(target_os = "macos")]
155fn boot_identity() -> Option<String> {
156 static BOOT: std::sync::OnceLock<String> = std::sync::OnceLock::new();
157 if let Some(boot) = BOOT.get() {
158 return Some(boot.clone());
159 }
160 let output = std::process::Command::new("/usr/sbin/sysctl")
161 .args(["-n", "kern.bootsessionuuid"])
162 .output()
163 .ok()?;
164 if !output.status.success() {
165 return None;
166 }
167 let text = std::str::from_utf8(&output.stdout).ok()?.trim();
168 let boot = uuid::Uuid::parse_str(text).ok()?.to_string();
169 let _ = BOOT.set(boot.clone());
171 Some(boot)
172}
173
174#[cfg(target_os = "linux")]
175pub fn observe_process(pid: u32) -> ProcessObservation {
176 if pid <= 1 || i32::try_from(pid).is_err() {
177 return ProcessObservation::Unknown;
178 }
179 if !std::fs::read_to_string("/proc/self/stat").is_ok_and(|stat| {
184 stat.split_once(" (").and_then(|(pid, _)| decimal(pid))
185 == Some(u64::from(std::process::id()))
186 }) {
187 return ProcessObservation::Unknown;
188 }
189 let Some(boot) = boot_identity() else {
190 return ProcessObservation::Unknown;
191 };
192 match std::fs::read_to_string(format!("/proc/{pid}/stat")) {
193 Ok(stat) => linux_stat(&stat, pid, &boot),
194 Err(error) if error.kind() == std::io::ErrorKind::NotFound => ProcessObservation::Absent,
195 Err(_) => ProcessObservation::Unknown,
196 }
197}
198
199#[cfg(any(target_os = "linux", test))]
200fn linux_stat(stat: &str, pid: u32, boot: &str) -> ProcessObservation {
201 let Some((prefix, rest)) = stat.rsplit_once(") ") else {
202 return ProcessObservation::Unknown;
203 };
204 if prefix.split_once(" (").and_then(|(pid, _)| decimal(pid)) != Some(u64::from(pid)) {
205 return ProcessObservation::Unknown;
206 }
207 let mut fields = rest.split_whitespace();
208 let state = fields.next();
209 let Some(ticks) = fields.nth(18).and_then(decimal) else {
210 return ProcessObservation::Unknown;
211 };
212 match state {
213 Some("Z" | "X" | "x") => ProcessObservation::Absent,
214 Some("R" | "S" | "D" | "T" | "t" | "K" | "W" | "P" | "I") => {
215 ProcessObservation::Present(ProcessIdentity {
216 boot_id: boot.into(),
217 start: ProcessStart::Linux { ticks },
218 })
219 }
220 _ => ProcessObservation::Unknown,
221 }
222}
223
224#[cfg(target_os = "macos")]
225pub fn observe_process(pid: u32) -> ProcessObservation {
226 if pid <= 1 || i32::try_from(pid).is_err() {
227 return ProcessObservation::Unknown;
228 }
229 let Some(boot) = boot_identity() else {
230 return ProcessObservation::Unknown;
231 };
232 match libproc::proc_pid::pidinfo::<libproc::bsd_info::BSDInfo>(pid as i32, 0) {
233 Ok(info) => darwin_info(
234 pid,
235 info.pbi_pid,
236 info.pbi_status,
237 info.pbi_start_tvsec,
238 info.pbi_start_tvusec,
239 &boot,
240 ),
241 Err(_) => {
242 match rustix::process::test_kill_process(
245 rustix::process::Pid::from_raw(pid as i32).expect("validated positive PID"),
246 ) {
247 Err(rustix::io::Errno::SRCH) => ProcessObservation::Absent,
248 _ => ProcessObservation::Unknown,
249 }
250 }
251 }
252}
253
254#[cfg(any(target_os = "macos", test))]
255fn darwin_info(
256 requested: u32,
257 actual: u32,
258 state: u32,
259 seconds: u64,
260 microseconds: u64,
261 boot: &str,
262) -> ProcessObservation {
263 if actual != requested || seconds == 0 || microseconds >= 1_000_000 {
264 return ProcessObservation::Unknown;
265 }
266 match state {
269 5 => ProcessObservation::Absent,
270 1..=4 => ProcessObservation::Present(ProcessIdentity {
271 boot_id: boot.into(),
272 start: ProcessStart::Darwin {
273 seconds,
274 microseconds: microseconds as u32,
275 },
276 }),
277 _ => ProcessObservation::Unknown,
278 }
279}
280
281#[cfg(not(any(target_os = "linux", target_os = "macos")))]
282pub fn observe_process(_pid: u32) -> ProcessObservation {
283 ProcessObservation::Unknown
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 const BOOT: &str = "3f1c2a9e-5b7d-4e2a-9c1f-0a1b2c3d4e5f";
291
292 #[test]
293 fn native_precision_survives_records_and_rejects_second_resolution_history() {
294 let linux = format!("{BOOT}:123456789");
295 let darwin = format!("{BOOT}:darwin:1791280000:123456");
296 for record in [&linux, &darwin] {
297 let identity = ProcessIdentity::from_record(record).unwrap();
298 assert_eq!(identity.to_record().as_ref(), Some(record));
299 assert_eq!(
300 serde_json::from_str::<ProcessIdentity>(&serde_json::to_string(&identity).unwrap())
301 .unwrap(),
302 identity
303 );
304 }
305 for record in [
306 format!("{BOOT}:Mon Sep 28 10:48:17 2026"),
307 format!("{BOOT}:darwin:1791280000:1000000"),
308 format!("{BOOT}:darwin:0:123"),
309 format!("{BOOT}:darwin:1791280000:123:4"),
310 format!("{BOOT}:+123"),
311 "unverified-boot:123".into(),
312 format!("{}:123", BOOT.to_uppercase()),
313 ] {
314 assert!(ProcessIdentity::from_record(&record).is_none(), "{record}");
315 }
316 }
317
318 #[test]
319 fn matching_distinguishes_microseconds_reboots_and_unknown_observation() {
320 let owner =
321 ProcessIdentity::from_record(&format!("{BOOT}:darwin:1791280000:123456")).unwrap();
322 assert_eq!(
323 owner.presence(&ProcessObservation::Present(owner.clone())),
324 OwnerPresence::Live
325 );
326 let mut reused = owner.clone();
327 reused.start = ProcessStart::Darwin {
328 seconds: 1791280000,
329 microseconds: 123457,
330 };
331 assert_eq!(
332 owner.presence(&ProcessObservation::Present(reused)),
333 OwnerPresence::Absent
334 );
335 let mut rebooted = owner.clone();
336 rebooted.boot_id = "00000000-0000-0000-0000-000000000001".into();
337 assert_eq!(
338 owner.presence(&ProcessObservation::Present(rebooted)),
339 OwnerPresence::Absent
340 );
341 assert_eq!(
342 owner.presence(&ProcessObservation::Unknown),
343 OwnerPresence::Unknown
344 );
345 assert_eq!(
346 owner.presence(&ProcessObservation::Absent),
347 OwnerPresence::Absent
348 );
349 let linux = ProcessIdentity::from_record(&format!("{BOOT}:123")).unwrap();
350 assert_eq!(
351 owner.presence(&ProcessObservation::Present(linux)),
352 OwnerPresence::Unknown
353 );
354 }
355
356 #[test]
357 fn absent_pid_is_independent_proof_but_unknown_or_legacy_birth_cannot_adopt_a_live_pid() {
358 let current =
359 ProcessIdentity::from_record(&format!("{BOOT}:darwin:1791280000:123456")).unwrap();
360 let legacy = ProcessIdentity::from_record(&format!("{BOOT}:Mon Sep 28 10:48:17 2026"));
361 assert!(legacy.is_none());
362 for expected in [None, legacy.as_ref(), Some(¤t)] {
363 assert_eq!(
364 observed_owner_presence(expected, &ProcessObservation::Absent),
365 OwnerPresence::Absent
366 );
367 assert_eq!(
368 observed_owner_presence(expected, &ProcessObservation::Unknown),
369 OwnerPresence::Unknown
370 );
371 }
372 assert_eq!(
373 observed_owner_presence(None, &ProcessObservation::Present(current)),
374 OwnerPresence::Unknown
375 );
376 }
377
378 #[test]
379 fn proc_stat_handles_command_parentheses_and_refuses_wrong_pid_namespace() {
380 let stat = |state| format!("123 (rch (wrapper)) {state} {} 42", vec!["0"; 18].join(" "));
381 let expected = ProcessIdentity::from_record(&format!("{BOOT}:42")).unwrap();
382 assert_eq!(
383 expected.presence(&linux_stat(&stat("S"), 123, BOOT)),
384 OwnerPresence::Live
385 );
386 for state in ["Z", "X", "x"] {
387 assert_eq!(
388 linux_stat(&stat(state), 123, BOOT),
389 ProcessObservation::Absent
390 );
391 }
392 assert_eq!(
393 linux_stat(&stat("S"), 456, BOOT),
394 ProcessObservation::Unknown
395 );
396 assert_eq!(
397 linux_stat(&stat("?"), 123, BOOT),
398 ProcessObservation::Unknown
399 );
400 assert_eq!(
401 linux_stat("truncated", 123, BOOT),
402 ProcessObservation::Unknown
403 );
404 }
405
406 #[test]
407 fn darwin_kernel_records_validate_pid_status_and_fractional_time() {
408 assert!(matches!(
409 darwin_info(42, 42, 2, 1791280000, 123456, BOOT),
410 ProcessObservation::Present(_)
411 ));
412 assert_eq!(
413 darwin_info(42, 42, 5, 1791280000, 123456, BOOT),
414 ProcessObservation::Absent
415 );
416 for (pid, state, seconds, micros) in [
417 (43, 2, 1791280000, 123456),
418 (42, 99, 1791280000, 123456),
419 (42, 2, 0, 1),
420 (42, 2, 1791280000, 1000000),
421 ] {
422 assert_eq!(
423 darwin_info(42, pid, state, seconds, micros, BOOT),
424 ProcessObservation::Unknown
425 );
426 }
427 }
428
429 #[cfg(target_os = "macos")]
430 #[test]
431 fn darwin_native_birth_is_stable_and_a_reaped_child_is_absent() {
432 let own = current_process_identity().expect("native own process identity");
433 assert!(matches!(own.start, ProcessStart::Darwin { .. }));
434 assert_eq!(current_process_identity(), Some(own));
435 let mut child = std::process::Command::new("/bin/sleep")
436 .arg("30")
437 .spawn()
438 .unwrap();
439 let observed = observe_process(child.id());
440 child.kill().unwrap();
441 child.wait().unwrap();
442 assert!(
443 matches!(observed, ProcessObservation::Present(_)),
444 "{observed:?}"
445 );
446 assert_eq!(observe_process(child.id()), ProcessObservation::Absent);
447 }
448}