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