Skip to main content

microsandbox_utils/
process.rs

1//! Process-state helpers shared by host-side lifecycle code.
2
3#[cfg(windows)]
4use windows_sys::Win32::Foundation::{
5    CloseHandle, ERROR_ACCESS_DENIED, GetLastError, STILL_ACTIVE,
6};
7#[cfg(windows)]
8use windows_sys::Win32::System::Threading::{
9    GetExitCodeProcess, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
10};
11
12//--------------------------------------------------------------------------------------------------
13// Functions
14//--------------------------------------------------------------------------------------------------
15
16/// Return whether `pid` names a live, runnable process.
17///
18/// This intentionally treats zombies as not alive. `kill(pid, 0)` reports
19/// success for zombies because the PID still exists. This is not a resource-teardown
20/// barrier: on Linux a zombie leader can still have another thread releasing the shared
21/// file table, including disk locks. Lifecycle callers must fence those resources separately.
22pub fn pid_is_alive(pid: i32) -> bool {
23    if pid <= 0 {
24        return false;
25    }
26
27    pid_is_alive_platform(pid)
28}
29
30#[cfg(unix)]
31fn pid_is_alive_platform(pid: i32) -> bool {
32    if !pid_exists(pid) {
33        return false;
34    }
35
36    !pid_is_zombie(pid).unwrap_or(false)
37}
38
39#[cfg(windows)]
40fn pid_is_alive_platform(pid: i32) -> bool {
41    let Ok(pid) = u32::try_from(pid) else {
42        return false;
43    };
44
45    let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
46    if handle.is_null() {
47        // Protected processes can deny query access while still proving that
48        // the PID is live enough for cleanup to leave it alone.
49        let error = unsafe { GetLastError() };
50        return error == ERROR_ACCESS_DENIED;
51    }
52
53    let mut exit_code = 0;
54    let ok = unsafe { GetExitCodeProcess(handle, &mut exit_code) };
55    unsafe { CloseHandle(handle) };
56
57    ok != 0 && exit_code == STILL_ACTIVE as u32
58}
59
60#[cfg(not(any(unix, windows)))]
61fn pid_is_alive_platform(_pid: i32) -> bool {
62    false
63}
64
65/// Return whether `pid` exists, regardless of whether it can still run.
66#[cfg(unix)]
67pub fn pid_exists(pid: i32) -> bool {
68    if pid <= 0 {
69        return false;
70    }
71
72    let result = unsafe { libc::kill(pid, 0) };
73    if result == 0 {
74        return true;
75    }
76
77    matches!(
78        std::io::Error::last_os_error().raw_os_error(),
79        Some(code) if code == libc::EPERM
80    )
81}
82
83/// Return whether `pid` exists, regardless of whether it can still run.
84#[cfg(windows)]
85pub fn pid_exists(pid: i32) -> bool {
86    let Ok(pid) = u32::try_from(pid) else {
87        return false;
88    };
89
90    let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
91    if handle.is_null() {
92        let error = unsafe { GetLastError() };
93        return error == ERROR_ACCESS_DENIED;
94    }
95
96    unsafe { CloseHandle(handle) };
97    true
98}
99
100/// Return whether `pid` exists, regardless of whether it can still run.
101#[cfg(not(any(unix, windows)))]
102pub fn pid_exists(_pid: i32) -> bool {
103    false
104}
105
106/// Return whether `pid` is currently a zombie process.
107///
108/// Returns `None` when the platform cannot report process state or when the
109/// process disappears between the existence check and the state probe.
110pub fn pid_is_zombie(pid: i32) -> Option<bool> {
111    if pid <= 0 {
112        return Some(false);
113    }
114
115    pid_is_zombie_platform(pid)
116}
117
118#[cfg(target_os = "linux")]
119fn pid_is_zombie_platform(pid: i32) -> Option<bool> {
120    let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
121    let close_paren = stat.rfind(')')?;
122    let state = stat
123        .get(close_paren + 1..)?
124        .bytes()
125        .find(|byte| !byte.is_ascii_whitespace())?;
126    Some(state == b'Z')
127}
128
129#[cfg(target_os = "macos")]
130fn pid_is_zombie_platform(pid: i32) -> Option<bool> {
131    // `proc_pidinfo(PROC_PIDTBSDINFO)` returns no record for zombies on
132    // Darwin, but the kern.proc.pid sysctl still exposes `extern_proc.p_stat`.
133    // On 64-bit Darwin the offset is stable:
134    // p_un(16) + p_vmspace(8) + p_sigacts(8) + p_flag(4) = 36.
135    const KINFO_PROC_P_STAT_OFFSET: usize = 36;
136
137    let mut mib = [libc::CTL_KERN, libc::KERN_PROC, libc::KERN_PROC_PID, pid];
138    let mut len: libc::size_t = 0;
139    let size_result = unsafe {
140        libc::sysctl(
141            mib.as_mut_ptr(),
142            mib.len() as libc::c_uint,
143            std::ptr::null_mut(),
144            &mut len,
145            std::ptr::null_mut(),
146            0,
147        )
148    };
149    if size_result != 0 || len <= KINFO_PROC_P_STAT_OFFSET {
150        return None;
151    }
152
153    let mut buf = vec![0u8; len];
154    let read_result = unsafe {
155        libc::sysctl(
156            mib.as_mut_ptr(),
157            mib.len() as libc::c_uint,
158            buf.as_mut_ptr().cast::<libc::c_void>(),
159            &mut len,
160            std::ptr::null_mut(),
161            0,
162        )
163    };
164    if read_result != 0 || len <= KINFO_PROC_P_STAT_OFFSET {
165        return None;
166    }
167
168    Some(buf[KINFO_PROC_P_STAT_OFFSET] == libc::SZOMB as u8)
169}
170
171#[cfg(not(any(target_os = "linux", target_os = "macos")))]
172fn pid_is_zombie_platform(_pid: i32) -> Option<bool> {
173    None
174}
175
176//--------------------------------------------------------------------------------------------------
177// Tests
178//--------------------------------------------------------------------------------------------------
179
180#[cfg(all(test, unix))]
181mod tests {
182    use std::process::Command;
183    use std::time::{Duration, Instant};
184
185    use super::*;
186
187    #[test]
188    fn pid_liveness_treats_zombies_as_dead() {
189        let mut child = Command::new("sh")
190            .arg("-c")
191            .arg("exit 0")
192            .spawn()
193            .expect("spawn short-lived child");
194        let pid = child.id() as i32;
195        let deadline = Instant::now() + Duration::from_secs(5);
196
197        while Instant::now() < deadline {
198            if pid_is_zombie(pid) == Some(true) {
199                assert!(
200                    !pid_is_alive(pid),
201                    "zombie process should not count as alive"
202                );
203                let _ = child.wait();
204                return;
205            }
206            std::thread::sleep(Duration::from_millis(10));
207        }
208
209        let status = child.try_wait().expect("poll child");
210        let _ = child.wait();
211        panic!("child did not become observable as a zombie; last status: {status:?}");
212    }
213}