Skip to main content

running_process_platform_internal/platform_linux/
process_inspect.rs

1//! Asking this host about another process (Linux).
2
3use std::io;
4use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
5use std::path::PathBuf;
6
7use crate::platform::process::{ProcessInspectError, ProcessInspectErrorKind};
8
9/// A live reference to another process, good for as long as it is held.
10///
11/// Where the kernel offers one, this holds a pidfd: a PID can be recycled
12/// between two questions, but a pidfd cannot, so a handle opened once keeps
13/// naming the process it was opened for even after that process exits. Older
14/// kernels have no such thing, and there the handle falls back to asking
15/// about the PID -- which is the best this host can do, not an equivalent.
16pub struct ProcessLiveness {
17    pid: u32,
18    pid_fd: Option<OwnedFd>,
19}
20
21impl std::fmt::Debug for ProcessLiveness {
22    /// Names the process, not the handle.
23    ///
24    /// The underlying descriptor or handle value is an artefact of this
25    /// process's own table; printing it invites a reader to compare two
26    /// numbers that were never comparable.
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        f.debug_struct("ProcessLiveness")
29            .field("pid", &self.pid)
30            .finish_non_exhaustive()
31    }
32}
33
34impl ProcessLiveness {
35    /// Acquire a kernel-pinned handle suitable for identity-safe control.
36    pub fn open_for_control(pid: u32) -> Result<Self, ProcessInspectError> {
37        Self::open_pinned(pid).map_err(|source| ProcessInspectError {
38            kind: ProcessInspectErrorKind::Host,
39            source,
40        })
41    }
42
43    /// Force termination through the held pidfd, never by reopening the PID.
44    pub fn force_kill(&self) -> io::Result<()> {
45        self.signal_pinned(libc::SIGKILL)
46    }
47
48    /// Confirm terminal state through the held pidfd without reopening the PID.
49    pub fn has_exited(&self) -> io::Result<bool> {
50        let fd = self.pid_fd.as_ref().ok_or_else(|| {
51            io::Error::new(
52                io::ErrorKind::Unsupported,
53                "exit confirmation requires a held pidfd",
54            )
55        })?;
56        let mut poll_fd = libc::pollfd {
57            fd: fd.as_raw_fd(),
58            events: libc::POLLIN,
59            revents: 0,
60        };
61        // SAFETY: one initialized descriptor; no blocking wait.
62        if unsafe { libc::poll(&mut poll_fd, 1, 0) } < 0 {
63            return Err(io::Error::last_os_error());
64        }
65        if poll_fd.revents & (libc::POLLERR | libc::POLLNVAL) != 0 {
66            return Err(io::Error::other("held pidfd poll failed"));
67        }
68        Ok(poll_fd.revents & libc::POLLIN != 0)
69    }
70
71    /// Independent launches require a kernel-pinned identity. Unlike the
72    /// compatibility observer, this path never falls back to a bare PID.
73    pub(crate) fn open_pinned(pid: u32) -> io::Result<Self> {
74        if pid == 0 || pid > libc::pid_t::MAX as u32 {
75            return Err(io::Error::from(io::ErrorKind::InvalidInput));
76        }
77        // SAFETY: a validated positive pid and flags zero are passed by value.
78        let raw = unsafe { libc::syscall(libc::SYS_pidfd_open, pid as libc::pid_t, 0_u32) };
79        if raw < 0 {
80            let error = io::Error::last_os_error();
81            return Err(match error.raw_os_error() {
82                Some(libc::ENOSYS | libc::EINVAL) => io::Error::new(
83                    io::ErrorKind::Unsupported,
84                    "kernel-pinned process control is unavailable",
85                ),
86                _ => error,
87            });
88        }
89        // SAFETY: successful pidfd_open returned a newly owned descriptor.
90        let pid_fd = unsafe { OwnedFd::from_raw_fd(raw as i32) };
91        Ok(Self {
92            pid,
93            pid_fd: Some(pid_fd),
94        })
95    }
96
97    /// Signal the process named by the held kernel handle, never by PID.
98    pub(crate) fn signal_pinned(&self, signal: i32) -> io::Result<()> {
99        let fd = self.pid_fd.as_ref().ok_or_else(|| {
100            io::Error::new(
101                io::ErrorKind::Unsupported,
102                "process identity is not kernel-pinned",
103            )
104        })?;
105        // SAFETY: a live pidfd is borrowed; null siginfo requests ordinary
106        // signal delivery and flags zero selects the kernel's default policy.
107        let result = unsafe {
108            libc::syscall(
109                libc::SYS_pidfd_send_signal,
110                fd.as_raw_fd(),
111                signal,
112                std::ptr::null::<libc::siginfo_t>(),
113                0_u32,
114            )
115        };
116        if result < 0 {
117            Err(io::Error::last_os_error())
118        } else {
119            Ok(())
120        }
121    }
122
123    /// Take a reference to `pid`, failing if no such process is running.
124    pub fn open(pid: u32) -> Result<Self, ProcessInspectError> {
125        validate_pid(pid)?;
126        if !process_exists(pid) {
127            return Err(not_found());
128        }
129        Ok(Self {
130            pid,
131            pid_fd: try_pidfd_open(pid)?,
132        })
133    }
134
135    /// The process ID this handle was opened for.
136    pub fn pid(&self) -> u32 {
137        self.pid
138    }
139
140    /// Whether that process is still running.
141    pub fn is_alive(&self) -> bool {
142        match self.pid_fd.as_ref() {
143            Some(pid_fd) => pidfd_is_alive(pid_fd),
144            None => process_exists(self.pid),
145        }
146    }
147}
148
149/// Resolve the on-disk image a running process was started from.
150pub fn process_executable_path(pid: u32) -> Result<PathBuf, io::Error> {
151    std::fs::read_link(format!("/proc/{pid}/exe"))
152}
153
154/// Ask a process to stop.
155pub fn process_signal_terminate(pid: u32) -> Result<(), ProcessInspectError> {
156    signal(pid, libc::SIGTERM)
157}
158
159/// Stop a process without asking.
160pub fn process_force_kill(pid: u32) -> Result<(), ProcessInspectError> {
161    signal(pid, libc::SIGKILL)
162}
163
164fn signal(pid: u32, signal: libc::c_int) -> Result<(), ProcessInspectError> {
165    let native_pid = validate_pid(pid)?;
166    // SAFETY: `native_pid` is in range and the signal number is a constant.
167    let rc = unsafe { libc::kill(native_pid, signal) };
168    if rc == 0 {
169        Ok(())
170    } else {
171        Err(ProcessInspectError::last_os_error(
172            ProcessInspectErrorKind::Host,
173        ))
174    }
175}
176
177/// Signal zero: the permission and existence checks run, nothing is delivered.
178///
179/// `EPERM` counts as alive. A process we are not allowed to signal is still a
180/// process, and reporting it dead would invite a caller to reuse its PID.
181fn process_exists(pid: u32) -> bool {
182    let Ok(native_pid) = validate_pid(pid) else {
183        return false;
184    };
185    // SAFETY: `native_pid` is in range; signal 0 delivers nothing.
186    let rc = unsafe { libc::kill(native_pid, 0) };
187    if rc == 0 {
188        return true;
189    }
190    matches!(io::Error::last_os_error().raw_os_error(), Some(libc::EPERM))
191}
192
193fn validate_pid(pid: u32) -> Result<libc::pid_t, ProcessInspectError> {
194    if pid == 0 || pid > libc::pid_t::MAX as u32 {
195        Err(ProcessInspectError::stated(
196            ProcessInspectErrorKind::InvalidPid,
197            "pid outside the range this host issues",
198        ))
199    } else {
200        Ok(pid as libc::pid_t)
201    }
202}
203
204/// Open a pidfd, or report that this kernel will not give us one.
205///
206/// A kernel without the syscall, a seccomp filter that hides it, and a denial
207/// are all the same answer to the caller: no pidfd, fall back to the PID.
208/// Only `ESRCH` is different -- that is the process being gone, which is
209/// worth failing on rather than falling back to asking about a dead PID.
210fn try_pidfd_open(pid: u32) -> Result<Option<OwnedFd>, ProcessInspectError> {
211    // SAFETY: the syscall takes a pid and a flags word, both passed by value.
212    let raw = unsafe { libc::syscall(libc::SYS_pidfd_open, pid as libc::pid_t, 0_u32) };
213    if raw >= 0 {
214        // SAFETY: the syscall succeeded, so `raw` is a fresh descriptor this
215        // handle now solely owns.
216        return Ok(Some(unsafe { OwnedFd::from_raw_fd(raw as i32) }));
217    }
218
219    match io::Error::last_os_error().raw_os_error() {
220        Some(libc::ESRCH) => Err(not_found()),
221        _ => Ok(None),
222    }
223}
224
225/// A pidfd becomes readable exactly when its process exits.
226fn pidfd_is_alive(pid_fd: &OwnedFd) -> bool {
227    let mut poll_fd = libc::pollfd {
228        fd: pid_fd.as_raw_fd(),
229        events: libc::POLLIN,
230        revents: 0,
231    };
232    // SAFETY: one initialised pollfd is described, and the zero timeout makes
233    // this a poll rather than a wait.
234    let rc = unsafe { libc::poll(&mut poll_fd, 1, 0) };
235    rc == 0
236}
237
238fn not_found() -> ProcessInspectError {
239    ProcessInspectError::stated(ProcessInspectErrorKind::NotFound, "no such process")
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    /// PID zero names no process on any host, and is rejected before the
247    /// kernel is asked -- signal(0, ...) would mean "the whole process group".
248    #[test]
249    fn pid_zero_is_never_valid() {
250        let error = ProcessLiveness::open(0).expect_err("pid 0");
251        assert_eq!(error.kind, ProcessInspectErrorKind::InvalidPid);
252        assert!(!process_exists(0));
253    }
254
255    #[test]
256    fn pinned_control_rejects_missing_handle_without_signalling_pid() {
257        let unpinned = ProcessLiveness {
258            pid: std::process::id(),
259            pid_fd: None,
260        };
261        assert_eq!(
262            unpinned.signal_pinned(libc::SIGKILL).unwrap_err().kind(),
263            io::ErrorKind::Unsupported
264        );
265        assert_eq!(
266            ProcessLiveness::open_pinned(0).unwrap_err().kind(),
267            io::ErrorKind::InvalidInput
268        );
269    }
270
271    #[test]
272    fn pinned_control_keeps_identity_after_exit() {
273        let mut command = std::process::Command::new("/bin/sh");
274        command.args(["-c", "exec sleep 60"]);
275        let mut child = crate::spawn_sync(
276            &mut command,
277            crate::platform::process::SpawnStdio::default(),
278            crate::platform::process::SyncEnvironment::Inherit,
279        )
280        .unwrap();
281        let pinned = ProcessLiveness::open_pinned(child.id()).unwrap();
282        pinned.signal_pinned(libc::SIGKILL).unwrap();
283        child.wait().unwrap();
284        assert!(!pinned.is_alive());
285        assert_eq!(
286            pinned.signal_pinned(0).unwrap_err().raw_os_error(),
287            Some(libc::ESRCH)
288        );
289    }
290
291    /// This process is alive, and knows where it was started from.
292    #[test]
293    fn this_process_is_alive_and_locatable() {
294        let me = std::process::id();
295        let handle = ProcessLiveness::open(me).expect("open self");
296        assert_eq!(handle.pid(), me);
297        assert!(handle.is_alive());
298        assert_eq!(
299            process_executable_path(me).expect("exe"),
300            std::env::current_exe().expect("current_exe")
301        );
302    }
303
304    /// A handle keeps naming the process it was opened for. With a pidfd the
305    /// kernel guarantees this; without one, the PID could in principle be
306    /// recycled, which is exactly why the pidfd is preferred.
307    #[test]
308    fn a_dead_process_reports_dead() {
309        let child = std::process::Command::new("/bin/sh")
310            .args(["-c", "exit 0"])
311            .spawn()
312            .expect("spawn");
313        let pid = child.id();
314        let handle = ProcessLiveness::open(pid).expect("open child");
315        let mut child = child;
316        child.wait().expect("reap");
317        assert!(!handle.is_alive(), "a reaped child must report dead");
318    }
319}
320
321/// Whether two spellings name the same executable image on this host.
322///
323/// This host's paths are case-sensitive and distinguish nothing else, so once
324/// both sides are resolved the comparison is exact. A path that cannot be
325/// canonicalised is compared as written rather than treated as a mismatch,
326/// because "the file moved" and "the caller lacks permission to resolve it"
327/// arrive here identically.
328pub fn process_same_executable_path(actual: &std::path::Path, expected: &std::path::Path) -> bool {
329    let resolve =
330        |path: &std::path::Path| std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
331    resolve(actual) == resolve(expected)
332}
333
334#[cfg(test)]
335mod path_tests {
336    use super::*;
337    use std::path::Path;
338
339    /// Case is meaningful here; two spellings that differ by it are two files.
340    #[test]
341    fn case_distinguishes_two_images() {
342        assert!(!process_same_executable_path(
343            Path::new("/tmp/Daemon"),
344            Path::new("/tmp/daemon"),
345        ));
346    }
347
348    /// A path resolves to itself, canonicalisable or not.
349    #[test]
350    fn a_path_matches_itself() {
351        assert!(process_same_executable_path(
352            Path::new("/tmp/rp-does-not-exist/daemon"),
353            Path::new("/tmp/rp-does-not-exist/daemon"),
354        ));
355        let me = std::env::current_exe().expect("current_exe");
356        assert!(process_same_executable_path(&me, &me));
357    }
358}