Skip to main content

rch_common/
process_identity.rs

1//! Kernel process birth identity shared by durable clients and the daemon.
2//!
3//! A PID alone, or a display timestamp rounded to seconds, cannot establish
4//! ownership after a restart. Failed inspection never proves that an owner died.
5
6use 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    /// Linux's existing durable daemon representation remains byte-compatible.
48    /// The Darwin marker is versioned by kind and retains kernel precision.
49    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(&current.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
108/// Only call this with the PID whose identity was recorded in the journal.
109/// The result authorizes observation/recovery, never an unfenced kill(pid).
110pub 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
129/// Read our own birth marker without accidentally inspecting an unrelated
130/// /proc/<getpid> when procfs belongs to a different PID namespace.
131pub fn current_process_identity() -> Option<ProcessIdentity> {
132    let observation = observe_process(std::process::id());
133    match observation {
134        ProcessObservation::Present(identity) => Some(identity),
135        _ => None,
136    }
137}
138
139/// Machine-readable Darwin group witness for the remote shell protocol.
140/// Keep kernel microsecond precision, encoded as one decimal receipt field.
141/// Read birth identity again after group membership to fence exit/PID reuse.
142#[cfg(target_os = "macos")]
143pub fn remote_process_witness(pid: u32) -> Option<String> {
144    if pid <= 1 || i32::try_from(pid).is_err() {
145        return None;
146    }
147    let native_pid = i32::try_from(pid).ok()?;
148    let info = libproc::proc_pid::pidinfo::<libproc::bsd_info::BSDInfo>(native_pid, 0).ok()?;
149    let boot = boot_identity()?;
150    let first = darwin_info(
151        pid,
152        info.pbi_pid,
153        info.pbi_status,
154        info.pbi_start_tvsec,
155        info.pbi_start_tvusec,
156        &boot,
157    );
158    let ProcessObservation::Present(identity) = first else {
159        return None;
160    };
161    let confirmed = libproc::proc_pid::pidinfo::<libproc::bsd_info::BSDInfo>(native_pid, 0).ok()?;
162    if confirmed.pbi_pgid != info.pbi_pgid
163        || darwin_info(
164            pid,
165            confirmed.pbi_pid,
166            confirmed.pbi_status,
167            confirmed.pbi_start_tvsec,
168            confirmed.pbi_start_tvusec,
169            &boot,
170        ) != ProcessObservation::Present(identity)
171    {
172        return None;
173    }
174    let start = info
175        .pbi_start_tvsec
176        .checked_mul(1_000_000)?
177        .checked_add(info.pbi_start_tvusec)?;
178    Some(format!("{boot} {start} {}", info.pbi_pgid))
179}
180
181#[cfg(not(target_os = "macos"))]
182pub fn remote_process_witness(_pid: u32) -> Option<String> {
183    None
184}
185
186#[cfg(target_os = "linux")]
187fn boot_identity() -> Option<String> {
188    let boot = std::fs::read_to_string("/proc/sys/kernel/random/boot_id").ok()?;
189    let boot = boot.trim();
190    uuid::Uuid::parse_str(boot).ok()?;
191    Some(boot.into())
192}
193
194#[cfg(target_os = "macos")]
195fn boot_identity() -> Option<String> {
196    static BOOT: std::sync::OnceLock<String> = std::sync::OnceLock::new();
197    if let Some(boot) = BOOT.get() {
198        return Some(boot.clone());
199    }
200    let output = std::process::Command::new("/usr/sbin/sysctl")
201        .args(["-n", "kern.bootsessionuuid"])
202        .output()
203        .ok()?;
204    if !output.status.success() {
205        return None;
206    }
207    let text = std::str::from_utf8(&output.stdout).ok()?.trim();
208    let boot = uuid::Uuid::parse_str(text).ok()?.to_string();
209    // Do not cache failures: a transient inspection error is not permanent.
210    let _ = BOOT.set(boot.clone());
211    Some(boot)
212}
213
214#[cfg(target_os = "linux")]
215pub fn observe_process(pid: u32) -> ProcessObservation {
216    if pid <= 1 || i32::try_from(pid).is_err() {
217        return ProcessObservation::Unknown;
218    }
219    // /proc may have been mounted by a different PID namespace. Neither a
220    // matching numeric entry nor ENOENT there describes our signal namespace.
221    // In particular, never release ownership because a live wrapper's PID is
222    // absent from a foreign procfs view.
223    let namespace_matches = match (
224        std::fs::read_to_string("/proc/self/stat"),
225        std::fs::read_to_string("/proc/self/status"),
226    ) {
227        (Ok(stat), Ok(status)) => linux_namespace_matches(std::process::id(), &stat, &status),
228        _ => false,
229    };
230    if !namespace_matches {
231        return ProcessObservation::Unknown;
232    }
233    let Some(boot) = boot_identity() else {
234        return ProcessObservation::Unknown;
235    };
236    observe_linux_process(
237        &std::path::PathBuf::from(format!("/proc/{pid}")),
238        pid,
239        &boot,
240    )
241}
242
243#[cfg(target_os = "linux")]
244fn observe_linux_process(process: &std::path::Path, pid: u32, boot: &str) -> ProcessObservation {
245    match std::fs::read_to_string(process.join("stat")) {
246        Ok(stat) => {
247            let observed = linux_stat(&stat, pid, boot);
248            if observed != ProcessObservation::Unknown {
249                return observed;
250            }
251            let Some(leader) = parse_linux_stat(&stat, pid) else {
252                return ProcessObservation::Unknown;
253            };
254            observe_linux_threads(process, pid, boot, leader)
255        }
256        // hidepid and restricted procfs views can hide a LIVE process. Only
257        // a signal-free kernel ESRCH confirms absence after a missing entry.
258        Err(error) if error.kind() == std::io::ErrorKind::NotFound => kernel_absence(pid),
259        Err(_) => ProcessObservation::Unknown,
260    }
261}
262
263#[cfg(any(target_os = "linux", test))]
264fn linux_namespace_matches(pid: u32, stat: &str, status: &str) -> bool {
265    if parse_linux_stat(stat, pid).is_none() {
266        return false;
267    }
268    // The numeric PID can coincide in a parent-mounted procfs. NSpid lists
269    // the namespace hierarchy, so equality of the first number is insufficient.
270    // Missing, duplicate, malformed or multi-level evidence fails closed.
271    let mut records = status
272        .lines()
273        .filter_map(|line| line.strip_prefix("NSpid:"));
274    let Some(record) = records.next() else {
275        return false;
276    };
277    if records.next().is_some() {
278        return false;
279    }
280    let mut ids = record.split_whitespace();
281    ids.next().and_then(decimal) == Some(u64::from(pid)) && ids.next().is_none()
282}
283
284/// A successful permission probe says nothing about birth identity; it cannot
285/// adopt a live PID. Only ESRCH independently establishes process absence.
286#[cfg(any(target_os = "linux", target_os = "macos"))]
287fn kernel_absence(pid: u32) -> ProcessObservation {
288    let Some(pid) = i32::try_from(pid)
289        .ok()
290        .filter(|pid| *pid > 1)
291        .and_then(rustix::process::Pid::from_raw)
292    else {
293        return ProcessObservation::Unknown;
294    };
295    match rustix::process::test_kill_process(pid) {
296        Err(rustix::io::Errno::SRCH) => ProcessObservation::Absent,
297        _ => ProcessObservation::Unknown,
298    }
299}
300
301#[cfg(any(target_os = "linux", test))]
302#[derive(Clone, Copy)]
303struct LinuxTaskStat {
304    executing: bool,
305    threads: u32,
306    ticks: u64,
307}
308
309#[cfg(any(target_os = "linux", test))]
310fn parse_linux_stat(stat: &str, pid: u32) -> Option<LinuxTaskStat> {
311    let (prefix, rest) = stat.rsplit_once(") ")?;
312    if prefix.split_once(" (").and_then(|(pid, _)| decimal(pid)) != Some(u64::from(pid)) {
313        return None;
314    }
315    let mut fields = rest.split_whitespace();
316    let executing = match fields.next()? {
317        "Z" | "X" | "x" => false,
318        "R" | "S" | "D" | "T" | "t" | "K" | "W" | "P" | "I" => true,
319        _ => return None,
320    };
321    // After state (field 3), num_threads is field 20 and starttime is 22.
322    // A zero/malformed count cannot establish the death of the whole process.
323    let threads = u32::try_from(fields.nth(16).and_then(decimal)?).ok()?;
324    let ticks = fields.nth(1).and_then(decimal)?;
325    (threads > 0).then_some(LinuxTaskStat {
326        executing,
327        threads,
328        ticks,
329    })
330}
331
332#[cfg(any(target_os = "linux", test))]
333fn linux_stat(stat: &str, pid: u32, boot: &str) -> ProcessObservation {
334    let Some(task) = parse_linux_stat(stat, pid) else {
335        return ProcessObservation::Unknown;
336    };
337    if task.executing {
338        ProcessObservation::Present(ProcessIdentity {
339            boot_id: boot.into(),
340            start: ProcessStart::Linux { ticks: task.ticks },
341        })
342    } else if task.threads == 1 {
343        ProcessObservation::Absent
344    } else {
345        // pthread_exit() can leave /proc/<pid>/stat in Z while other threads
346        // still execute. Never release a lease from the leader's state alone.
347        ProcessObservation::Unknown
348    }
349}
350
351/// Task enumeration supplies POSITIVE liveness evidence only. An empty,
352/// inaccessible, changing or entirely terminal task inventory never proves
353/// absence. Keep the recorded group leader's birth identity, not a thread's.
354#[cfg(target_os = "linux")]
355fn observe_linux_threads(
356    process: &std::path::Path,
357    pid: u32,
358    boot: &str,
359    leader: LinuxTaskStat,
360) -> ProcessObservation {
361    const MAX_THREAD_OBSERVATIONS: usize = 4096;
362    let Ok(entries) = std::fs::read_dir(process.join("task")) else {
363        return ProcessObservation::Unknown;
364    };
365    for (index, entry) in entries.enumerate() {
366        if index >= MAX_THREAD_OBSERVATIONS {
367            return ProcessObservation::Unknown;
368        }
369        let Ok(entry) = entry else {
370            return ProcessObservation::Unknown;
371        };
372        let Some(tid) = entry
373            .file_name()
374            .to_str()
375            .and_then(decimal)
376            .and_then(|tid| u32::try_from(tid).ok())
377            .filter(|tid| *tid > 0 && i32::try_from(*tid).is_ok())
378        else {
379            return ProcessObservation::Unknown;
380        };
381        let Ok(stat) = std::fs::read_to_string(entry.path().join("stat")) else {
382            return ProcessObservation::Unknown;
383        };
384        let Some(task) = parse_linux_stat(&stat, tid) else {
385            return ProcessObservation::Unknown;
386        };
387        if task.executing {
388            // The leader may have been reaped and this numeric PID reused
389            // while reading tasks. Such evidence cannot adopt the old lease.
390            let confirmed = std::fs::read_to_string(process.join("stat"))
391                .ok()
392                .and_then(|stat| parse_linux_stat(&stat, pid));
393            return if confirmed.is_some_and(|current| current.ticks == leader.ticks) {
394                ProcessObservation::Present(ProcessIdentity {
395                    boot_id: boot.into(),
396                    start: ProcessStart::Linux {
397                        ticks: leader.ticks,
398                    },
399                })
400            } else {
401                ProcessObservation::Unknown
402            };
403        }
404    }
405    ProcessObservation::Unknown
406}
407
408#[cfg(target_os = "macos")]
409pub fn observe_process(pid: u32) -> ProcessObservation {
410    if pid <= 1 || i32::try_from(pid).is_err() {
411        return ProcessObservation::Unknown;
412    }
413    let Some(boot) = boot_identity() else {
414        return ProcessObservation::Unknown;
415    };
416    match libproc::proc_pid::pidinfo::<libproc::bsd_info::BSDInfo>(pid as i32, 0) {
417        Ok(info) => darwin_info(
418            pid,
419            info.pbi_pid,
420            info.pbi_status,
421            info.pbi_start_tvsec,
422            info.pbi_start_tvusec,
423            &boot,
424        ),
425        Err(_) => {
426            // libproc's error is textual. Do not infer ESRCH from text or a
427            // stale errno: perform a safe, signal-free kernel existence probe.
428            kernel_absence(pid)
429        }
430    }
431}
432
433#[cfg(any(target_os = "macos", test))]
434fn darwin_info(
435    requested: u32,
436    actual: u32,
437    state: u32,
438    seconds: u64,
439    microseconds: u64,
440    boot: &str,
441) -> ProcessObservation {
442    if actual != requested || seconds == 0 || microseconds >= 1_000_000 {
443        return ProcessObservation::Unknown;
444    }
445    // Darwin bsd/sys/proc.h: SIDL=1, SRUN=2, SSLEEP=3, SSTOP=4, SZOMB=5.
446    // proc_info.c fills pbi_start_tv{sec,usec} directly from p_start.
447    match state {
448        5 => ProcessObservation::Absent,
449        1..=4 => ProcessObservation::Present(ProcessIdentity {
450            boot_id: boot.into(),
451            start: ProcessStart::Darwin {
452                seconds,
453                microseconds: microseconds as u32,
454            },
455        }),
456        _ => ProcessObservation::Unknown,
457    }
458}
459
460#[cfg(not(any(target_os = "linux", target_os = "macos")))]
461pub fn observe_process(_pid: u32) -> ProcessObservation {
462    ProcessObservation::Unknown
463}
464
465#[cfg(test)]
466mod tests {
467    use super::*;
468
469    const BOOT: &str = "3f1c2a9e-5b7d-4e2a-9c1f-0a1b2c3d4e5f";
470
471    #[test]
472    fn native_precision_survives_records_and_rejects_second_resolution_history() {
473        let linux = format!("{BOOT}:123456789");
474        let darwin = format!("{BOOT}:darwin:1791280000:123456");
475        for record in [&linux, &darwin] {
476            let identity = ProcessIdentity::from_record(record).unwrap();
477            assert_eq!(identity.to_record().as_ref(), Some(record));
478            assert_eq!(
479                serde_json::from_str::<ProcessIdentity>(&serde_json::to_string(&identity).unwrap())
480                    .unwrap(),
481                identity
482            );
483        }
484        for record in [
485            format!("{BOOT}:Mon Sep 28 10:48:17 2026"),
486            format!("{BOOT}:darwin:1791280000:1000000"),
487            format!("{BOOT}:darwin:0:123"),
488            format!("{BOOT}:darwin:1791280000:123:4"),
489            format!("{BOOT}:+123"),
490            "unverified-boot:123".into(),
491            format!("{}:123", BOOT.to_uppercase()),
492        ] {
493            assert!(ProcessIdentity::from_record(&record).is_none(), "{record}");
494        }
495    }
496
497    #[test]
498    fn matching_distinguishes_microseconds_reboots_and_unknown_observation() {
499        let owner =
500            ProcessIdentity::from_record(&format!("{BOOT}:darwin:1791280000:123456")).unwrap();
501        assert_eq!(
502            owner.presence(&ProcessObservation::Present(owner.clone())),
503            OwnerPresence::Live
504        );
505        let mut reused = owner.clone();
506        reused.start = ProcessStart::Darwin {
507            seconds: 1791280000,
508            microseconds: 123457,
509        };
510        assert_eq!(
511            owner.presence(&ProcessObservation::Present(reused)),
512            OwnerPresence::Absent
513        );
514        let mut rebooted = owner.clone();
515        rebooted.boot_id = "00000000-0000-0000-0000-000000000001".into();
516        assert_eq!(
517            owner.presence(&ProcessObservation::Present(rebooted)),
518            OwnerPresence::Absent
519        );
520        assert_eq!(
521            owner.presence(&ProcessObservation::Unknown),
522            OwnerPresence::Unknown
523        );
524        assert_eq!(
525            owner.presence(&ProcessObservation::Absent),
526            OwnerPresence::Absent
527        );
528        let linux = ProcessIdentity::from_record(&format!("{BOOT}:123")).unwrap();
529        assert_eq!(
530            owner.presence(&ProcessObservation::Present(linux)),
531            OwnerPresence::Unknown
532        );
533    }
534
535    #[test]
536    fn absent_pid_is_independent_proof_but_unknown_or_legacy_birth_cannot_adopt_a_live_pid() {
537        let current =
538            ProcessIdentity::from_record(&format!("{BOOT}:darwin:1791280000:123456")).unwrap();
539        let legacy = ProcessIdentity::from_record(&format!("{BOOT}:Mon Sep 28 10:48:17 2026"));
540        assert!(legacy.is_none());
541        for expected in [None, legacy.as_ref(), Some(&current)] {
542            assert_eq!(
543                observed_owner_presence(expected, &ProcessObservation::Absent),
544                OwnerPresence::Absent
545            );
546            assert_eq!(
547                observed_owner_presence(expected, &ProcessObservation::Unknown),
548                OwnerPresence::Unknown
549            );
550        }
551        assert_eq!(
552            observed_owner_presence(None, &ProcessObservation::Present(current)),
553            OwnerPresence::Unknown
554        );
555    }
556
557    #[test]
558    fn proc_stat_handles_command_parentheses_and_refuses_wrong_pid_namespace() {
559        let stat = |state| linux_record(123, state, "1", 42);
560        let expected = ProcessIdentity::from_record(&format!("{BOOT}:42")).unwrap();
561        assert_eq!(
562            expected.presence(&linux_stat(&stat("S"), 123, BOOT)),
563            OwnerPresence::Live
564        );
565        for state in ["Z", "X", "x"] {
566            assert_eq!(
567                linux_stat(&stat(state), 123, BOOT),
568                ProcessObservation::Absent
569            );
570        }
571        assert_eq!(
572            linux_stat(&stat("S"), 456, BOOT),
573            ProcessObservation::Unknown
574        );
575        assert_eq!(
576            linux_stat(&stat("?"), 123, BOOT),
577            ProcessObservation::Unknown
578        );
579        assert_eq!(
580            linux_stat("truncated", 123, BOOT),
581            ProcessObservation::Unknown
582        );
583    }
584
585    fn linux_record(pid: u32, state: &str, threads: &str, ticks: u64) -> String {
586        format!(
587            "{pid} (rch (wrapper)) {state} {} {threads} 0 {ticks}",
588            vec!["0"; 16].join(" ")
589        )
590    }
591
592    #[test]
593    fn namespace_validation_rejects_coincident_and_ambiguous_ids() {
594        let stat = linux_record(123, "S", "1", 42);
595        assert!(linux_namespace_matches(
596            123,
597            &stat,
598            "Name:\trch\nNSpid:\t123\n"
599        ));
600        for status in [
601            "NSpid:\t123\t123\n",
602            "NSpid:\t123\t7\n",
603            "NSpid:\t999\t123\n",
604            "NSpid:\t999\n",
605            "NSpid:\t123\nNSpid:\t123\n",
606            "NSpid:\n",
607            "NSpid:\t+123\n",
608            "NSpid:\t123 invalid\n",
609            "Name:\trch\n",
610            "",
611        ] {
612            assert!(!linux_namespace_matches(123, &stat, status), "{status:?}");
613        }
614        assert!(!linux_namespace_matches(456, &stat, "NSpid:\t456\n"));
615        assert!(!linux_namespace_matches(123, "truncated", "NSpid:\t123\n"));
616    }
617
618    #[cfg(target_os = "linux")]
619    #[test]
620    fn a_missing_proc_entry_cannot_release_a_live_owner() {
621        let hidden = tempfile::tempdir().unwrap();
622        let pid = std::process::id();
623        let expected = current_process_identity().expect("native observer identity");
624        // Model a restricted procfs VIEW, not the kernel. The signal-free
625        // probe still observes this actual live process and must veto release.
626        let observed = observe_linux_process(hidden.path(), pid, &expected.boot_id);
627        assert_eq!(observed, ProcessObservation::Unknown);
628        assert_eq!(expected.presence(&observed), OwnerPresence::Unknown);
629        assert_eq!(
630            observed_owner_presence(None, &observed),
631            OwnerPresence::Unknown
632        );
633    }
634
635    #[cfg(any(target_os = "linux", target_os = "macos"))]
636    #[test]
637    fn signal_free_probe_never_supplies_a_live_birth_identity() {
638        assert_eq!(
639            kernel_absence(std::process::id()),
640            ProcessObservation::Unknown
641        );
642        for invalid in [0, 1, u32::MAX] {
643            assert_eq!(kernel_absence(invalid), ProcessObservation::Unknown);
644        }
645        let mut child = std::process::Command::new("/bin/sleep")
646            .arg("20")
647            .spawn()
648            .unwrap();
649        let observed = kernel_absence(child.id());
650        child.kill().unwrap();
651        child.wait().unwrap();
652        assert_eq!(observed, ProcessObservation::Unknown);
653        assert_eq!(kernel_absence(child.id()), ProcessObservation::Absent);
654    }
655
656    #[test]
657    fn a_terminal_leader_with_other_threads_is_not_an_absent_owner() {
658        for state in ["Z", "X", "x"] {
659            assert_eq!(
660                linux_stat(&linux_record(123, state, "2", 42), 123, BOOT),
661                ProcessObservation::Unknown
662            );
663        }
664        for threads in ["0", "-1", "invalid", "4294967296"] {
665            for state in ["Z", "S"] {
666                assert_eq!(
667                    linux_stat(&linux_record(123, state, threads, 42), 123, BOOT),
668                    ProcessObservation::Unknown
669                );
670            }
671        }
672    }
673
674    #[cfg(target_os = "linux")]
675    fn thread_fixture() -> (tempfile::TempDir, LinuxTaskStat) {
676        let directory = tempfile::tempdir().unwrap();
677        let stat = linux_record(123, "Z", "2", 42);
678        std::fs::write(directory.path().join("stat"), &stat).unwrap();
679        let leader = parse_linux_stat(&stat, 123).unwrap();
680        (directory, leader)
681    }
682
683    #[cfg(target_os = "linux")]
684    fn write_task(process: &std::path::Path, tid: u32, state: &str) {
685        let directory = process.join("task").join(tid.to_string());
686        std::fs::create_dir_all(&directory).unwrap();
687        std::fs::write(directory.join("stat"), linux_record(tid, state, "2", 900)).unwrap();
688    }
689
690    #[cfg(target_os = "linux")]
691    #[test]
692    fn surviving_thread_retains_the_group_leaders_birth_identity() {
693        let (directory, leader) = thread_fixture();
694        write_task(directory.path(), 456, "S");
695        let observed = observe_linux_threads(directory.path(), 123, BOOT, leader);
696        let expected = ProcessIdentity::from_record(&format!("{BOOT}:42")).unwrap();
697        assert_eq!(observed, ProcessObservation::Present(expected.clone()));
698        assert_eq!(expected.presence(&observed), OwnerPresence::Live);
699        assert_eq!(
700            observed_owner_presence(None, &observed),
701            OwnerPresence::Unknown
702        );
703    }
704
705    #[cfg(target_os = "linux")]
706    #[test]
707    fn incomplete_and_dead_task_inventories_never_authorize_release() {
708        let (directory, leader) = thread_fixture();
709        let observe = || observe_linux_threads(directory.path(), 123, BOOT, leader);
710        assert_eq!(observe(), ProcessObservation::Unknown);
711        std::fs::create_dir(directory.path().join("task")).unwrap();
712        assert_eq!(observe(), ProcessObservation::Unknown);
713        write_task(directory.path(), 123, "Z");
714        assert_eq!(observe(), ProcessObservation::Unknown);
715        write_task(directory.path(), 456, "Z");
716        assert_eq!(observe(), ProcessObservation::Unknown);
717        std::fs::write(directory.path().join("task/456/stat"), "truncated").unwrap();
718        assert_eq!(observe(), ProcessObservation::Unknown);
719    }
720
721    #[cfg(target_os = "linux")]
722    #[test]
723    fn a_reused_leader_cannot_inherit_a_threads_liveness_evidence() {
724        let (directory, leader) = thread_fixture();
725        write_task(directory.path(), 456, "S");
726        std::fs::write(
727            directory.path().join("stat"),
728            linux_record(123, "S", "2", 43),
729        )
730        .unwrap();
731        assert_eq!(
732            observe_linux_threads(directory.path(), 123, BOOT, leader),
733            ProcessObservation::Unknown
734        );
735    }
736
737    #[test]
738    fn darwin_kernel_records_validate_pid_status_and_fractional_time() {
739        assert!(matches!(
740            darwin_info(42, 42, 2, 1791280000, 123456, BOOT),
741            ProcessObservation::Present(_)
742        ));
743        assert_eq!(
744            darwin_info(42, 42, 5, 1791280000, 123456, BOOT),
745            ProcessObservation::Absent
746        );
747        for (pid, state, seconds, micros) in [
748            (43, 2, 1791280000, 123456),
749            (42, 99, 1791280000, 123456),
750            (42, 2, 0, 1),
751            (42, 2, 1791280000, 1000000),
752        ] {
753            assert_eq!(
754                darwin_info(42, pid, state, seconds, micros, BOOT),
755                ProcessObservation::Unknown
756            );
757        }
758    }
759
760    #[cfg(target_os = "macos")]
761    #[test]
762    fn darwin_native_birth_is_stable_and_a_reaped_child_is_absent() {
763        let own = current_process_identity().expect("native own process identity");
764        assert!(matches!(own.start, ProcessStart::Darwin { .. }));
765        assert_eq!(current_process_identity(), Some(own.clone()));
766        let mut child = std::process::Command::new("/bin/sleep")
767            .arg("30")
768            .spawn()
769            .unwrap();
770        let observed = observe_process(child.id());
771        let witness = remote_process_witness(child.id()).expect("native shell group witness");
772        let fields: Vec<_> = witness.split_whitespace().collect();
773        assert_eq!(fields.len(), 3);
774        assert_eq!(fields[0], own.boot_id);
775        let ProcessObservation::Present(ref identity) = observed else {
776            panic!("live child identity unavailable: {observed:?}");
777        };
778        let ProcessStart::Darwin {
779            seconds,
780            microseconds,
781        } = &identity.start
782        else {
783            panic!("wrong platform identity");
784        };
785        assert_eq!(
786            fields[1].parse::<u64>().unwrap(),
787            *seconds * 1_000_000 + u64::from(*microseconds)
788        );
789        child.kill().unwrap();
790        child.wait().unwrap();
791        assert!(
792            matches!(observed, ProcessObservation::Present(_)),
793            "{observed:?}"
794        );
795        assert_eq!(observe_process(child.id()), ProcessObservation::Absent);
796        assert!(remote_process_witness(child.id()).is_none());
797    }
798}