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