Skip to main content

ic_host_process/observation/procfs/
mod.rs

1//! Linux procfs observations. Callers select a mount and own read budgets.
2use super::{ChildProcesses, ProcessObservation};
3use std::{collections::BTreeSet, fs, path::Path, sync::OnceLock};
4
5fn ticks_per_second() -> u64 {
6    static VALUE: OnceLock<u64> = OnceLock::new();
7    *VALUE.get_or_init(|| {
8        #[cfg(unix)]
9        return positive_sysconf(libc::_SC_CLK_TCK, 100);
10        #[cfg(not(unix))]
11        100
12    })
13}
14
15fn page_size() -> u64 {
16    static VALUE: OnceLock<u64> = OnceLock::new();
17    *VALUE.get_or_init(|| {
18        #[cfg(unix)]
19        return positive_sysconf(libc::_SC_PAGESIZE, 4096);
20        #[cfg(not(unix))]
21        4096
22    })
23}
24
25#[cfg(unix)]
26#[expect(
27    unsafe_code,
28    reason = "read signed sysconf results for the existing fallback contract"
29)]
30fn positive_sysconf(name: libc::c_int, fallback: u64) -> u64 {
31    // Keep the signed result: Rustix's unsigned parameter helpers cannot retain
32    // sysconf's negative failure sentinel for this existing fallback contract.
33    // SAFETY: both callers use defined sysconf names; no pointers are involved.
34    let value = unsafe { libc::sysconf(name) };
35    if value > 0 {
36        u64::try_from(value).ok().unwrap_or_default()
37    } else {
38        fallback
39    }
40}
41
42/// Read the Linux creation token from a caller-selected procfs mount.
43#[must_use]
44pub fn process_identity(proc_root: &Path, pid: u32) -> Option<u64> {
45    let stat = fs::read_to_string(proc_root.join(pid.to_string()).join("stat")).ok()?;
46    let (_, fields) = stat_parts(&stat)?;
47    let mut fields = fields.split_whitespace();
48    fields.next()?; // state
49    fields.next()?.parse::<u64>().ok()?; // preserve full-stat PPID validation
50    fields.nth(17)?.parse().ok() // starttime is field 19 after the command
51}
52
53/// Read base Linux process facts from a caller-selected procfs mount.
54#[must_use]
55pub fn basic_process(proc_root: &Path, pid: u32) -> Option<ProcessObservation> {
56    let stat = fs::read_to_string(proc_root.join(pid.to_string()).join("stat")).ok()?;
57    let (name, fields) = parse_stat(&stat)?;
58    let number = |index: usize| fields.get(index)?.parse::<u64>().ok();
59    let ns = |index| number(index).unwrap_or(0).saturating_mul(1_000_000_000) / ticks_per_second();
60    Some(ProcessObservation {
61        pid,
62        ppid: u32::try_from(number(1)?).ok()?,
63        start_ticks: number(19)?,
64        name: name.to_owned(),
65        user_ns: ns(11),
66        system_ns: ns(12),
67        waited_user_ns: ns(13),
68        waited_system_ns: ns(14),
69        rss_bytes: number(21).unwrap_or(0).saturating_mul(page_size()),
70        pss_bytes: None,
71        read_bytes: None,
72        write_bytes: None,
73        minor_faults: number(7)
74            .unwrap_or(0)
75            .saturating_add(number(8).unwrap_or(0)),
76        major_faults: number(9)
77            .unwrap_or(0)
78            .saturating_add(number(10).unwrap_or(0)),
79        threads: number(17).unwrap_or(0),
80    })
81}
82
83fn stat_parts(stat: &str) -> Option<(&str, &str)> {
84    let begin = stat.find('(')?;
85    let end = stat.rfind(')')?;
86    Some((stat.get(begin + 1..end)?, stat.get(end + 1..)?))
87}
88
89fn parse_stat(stat: &str) -> Option<(&str, [&str; 22])> {
90    let (name, fields) = stat_parts(stat)?;
91    let mut fields = fields.split_whitespace();
92    // Only the first 22 fields are used. Keep them borrowed and on the stack;
93    // samples and identity checks need neither the rest nor a temporary Vec.
94    Some((name, std::array::from_fn(|_| fields.next().unwrap_or(""))))
95}
96
97fn field_value(text: &str, name: &str) -> Option<u64> {
98    text.lines()
99        .filter_map(|line| {
100            let mut fields = line.split_whitespace();
101            if fields.next()?.trim_end_matches(':') != name {
102                return None;
103            }
104            fields.next()?.parse().ok()
105        })
106        .next_back()
107}
108
109/// Read Linux I/O and optionally PSS for an already selected observation.
110/// This does not revalidate identity; consumers retain before/after checks.
111pub fn read_details(proc_root: &Path, result: &mut ProcessObservation, memory_detail: bool) {
112    let io = fs::read_to_string(proc_root.join(result.pid.to_string()).join("io")).ok();
113    result.read_bytes = io
114        .as_deref()
115        .and_then(|text| field_value(text, "read_bytes"));
116    result.write_bytes = io
117        .as_deref()
118        .and_then(|text| field_value(text, "write_bytes"));
119    if memory_detail {
120        result.pss_bytes =
121            fs::read_to_string(proc_root.join(result.pid.to_string()).join("smaps_rollup"))
122                .ok()
123                .and_then(|text| field_value(&text, "Pss"))
124                .map(|kb| kb.saturating_mul(1024));
125    }
126}
127/// Read all thread child lists from an explicitly selected procfs mount.
128#[must_use]
129pub fn children(proc_root: &Path, pid: u32) -> ChildProcesses {
130    let mut result = ChildProcesses::default();
131    // A Linux task's children file only lists children forked by that task.
132    // Inspect every thread, including nonleaders that spawn sandboxes/adapters.
133    // Never enumerate the top-level /proc directory to discover unrelated PIDs.
134    let tasks = proc_root.join(pid.to_string()).join("task");
135    let Ok(entries) = fs::read_dir(&tasks) else {
136        result.unavailable = true;
137        return result;
138    };
139    let mut children = BTreeSet::new();
140    let mut saw_task = false;
141    for entry in entries {
142        let Ok(entry) = entry else {
143            result.incomplete = true;
144            continue;
145        };
146        if entry
147            .file_name()
148            .to_str()
149            .and_then(|s| s.parse::<u32>().ok())
150            .is_none()
151        {
152            continue;
153        }
154        saw_task = true;
155        match fs::read_to_string(entry.path().join("children")) {
156            Ok(value) => {
157                for child in value.split_whitespace() {
158                    if let Ok(child) = child.parse::<u32>() {
159                        children.insert(child);
160                    } else {
161                        result.incomplete = true;
162                    }
163                }
164            }
165            Err(_) => {
166                // A thread can disappear during the scan; even that race can
167                // hide children reparented to a previously inspected thread.
168                result.incomplete = true;
169            }
170        }
171    }
172    if !saw_task {
173        result.unavailable = true;
174    }
175    result.pids = children.into_iter().collect();
176    result
177}
178
179#[cfg(all(test, unix))]
180mod tests;