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    /// Take a reference to `pid`, failing if no such process is running.
36    pub fn open(pid: u32) -> Result<Self, ProcessInspectError> {
37        validate_pid(pid)?;
38        if !process_exists(pid) {
39            return Err(not_found());
40        }
41        Ok(Self {
42            pid,
43            pid_fd: try_pidfd_open(pid)?,
44        })
45    }
46
47    /// The process ID this handle was opened for.
48    pub fn pid(&self) -> u32 {
49        self.pid
50    }
51
52    /// Whether that process is still running.
53    pub fn is_alive(&self) -> bool {
54        match self.pid_fd.as_ref() {
55            Some(pid_fd) => pidfd_is_alive(pid_fd),
56            None => process_exists(self.pid),
57        }
58    }
59}
60
61/// Resolve the on-disk image a running process was started from.
62pub fn process_executable_path(pid: u32) -> Result<PathBuf, io::Error> {
63    std::fs::read_link(format!("/proc/{pid}/exe"))
64}
65
66/// Ask a process to stop.
67pub fn process_signal_terminate(pid: u32) -> Result<(), ProcessInspectError> {
68    signal(pid, libc::SIGTERM)
69}
70
71/// Stop a process without asking.
72pub fn process_force_kill(pid: u32) -> Result<(), ProcessInspectError> {
73    signal(pid, libc::SIGKILL)
74}
75
76fn signal(pid: u32, signal: libc::c_int) -> Result<(), ProcessInspectError> {
77    let native_pid = validate_pid(pid)?;
78    // SAFETY: `native_pid` is in range and the signal number is a constant.
79    let rc = unsafe { libc::kill(native_pid, signal) };
80    if rc == 0 {
81        Ok(())
82    } else {
83        Err(ProcessInspectError::last_os_error(
84            ProcessInspectErrorKind::Host,
85        ))
86    }
87}
88
89/// Signal zero: the permission and existence checks run, nothing is delivered.
90///
91/// `EPERM` counts as alive. A process we are not allowed to signal is still a
92/// process, and reporting it dead would invite a caller to reuse its PID.
93fn process_exists(pid: u32) -> bool {
94    let Ok(native_pid) = validate_pid(pid) else {
95        return false;
96    };
97    // SAFETY: `native_pid` is in range; signal 0 delivers nothing.
98    let rc = unsafe { libc::kill(native_pid, 0) };
99    if rc == 0 {
100        return true;
101    }
102    matches!(io::Error::last_os_error().raw_os_error(), Some(libc::EPERM))
103}
104
105fn validate_pid(pid: u32) -> Result<libc::pid_t, ProcessInspectError> {
106    if pid == 0 || pid > libc::pid_t::MAX as u32 {
107        Err(ProcessInspectError::stated(
108            ProcessInspectErrorKind::InvalidPid,
109            "pid outside the range this host issues",
110        ))
111    } else {
112        Ok(pid as libc::pid_t)
113    }
114}
115
116/// Open a pidfd, or report that this kernel will not give us one.
117///
118/// A kernel without the syscall, a seccomp filter that hides it, and a denial
119/// are all the same answer to the caller: no pidfd, fall back to the PID.
120/// Only `ESRCH` is different -- that is the process being gone, which is
121/// worth failing on rather than falling back to asking about a dead PID.
122fn try_pidfd_open(pid: u32) -> Result<Option<OwnedFd>, ProcessInspectError> {
123    // SAFETY: the syscall takes a pid and a flags word, both passed by value.
124    let raw = unsafe { libc::syscall(libc::SYS_pidfd_open, pid as libc::pid_t, 0_u32) };
125    if raw >= 0 {
126        // SAFETY: the syscall succeeded, so `raw` is a fresh descriptor this
127        // handle now solely owns.
128        return Ok(Some(unsafe { OwnedFd::from_raw_fd(raw as i32) }));
129    }
130
131    match io::Error::last_os_error().raw_os_error() {
132        Some(libc::ESRCH) => Err(not_found()),
133        _ => Ok(None),
134    }
135}
136
137/// A pidfd becomes readable exactly when its process exits.
138fn pidfd_is_alive(pid_fd: &OwnedFd) -> bool {
139    let mut poll_fd = libc::pollfd {
140        fd: pid_fd.as_raw_fd(),
141        events: libc::POLLIN,
142        revents: 0,
143    };
144    // SAFETY: one initialised pollfd is described, and the zero timeout makes
145    // this a poll rather than a wait.
146    let rc = unsafe { libc::poll(&mut poll_fd, 1, 0) };
147    rc == 0
148}
149
150fn not_found() -> ProcessInspectError {
151    ProcessInspectError::stated(ProcessInspectErrorKind::NotFound, "no such process")
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    /// PID zero names no process on any host, and is rejected before the
159    /// kernel is asked -- signal(0, ...) would mean "the whole process group".
160    #[test]
161    fn pid_zero_is_never_valid() {
162        let error = ProcessLiveness::open(0).expect_err("pid 0");
163        assert_eq!(error.kind, ProcessInspectErrorKind::InvalidPid);
164        assert!(!process_exists(0));
165    }
166
167    /// This process is alive, and knows where it was started from.
168    #[test]
169    fn this_process_is_alive_and_locatable() {
170        let me = std::process::id();
171        let handle = ProcessLiveness::open(me).expect("open self");
172        assert_eq!(handle.pid(), me);
173        assert!(handle.is_alive());
174        assert_eq!(
175            process_executable_path(me).expect("exe"),
176            std::env::current_exe().expect("current_exe")
177        );
178    }
179
180    /// A handle keeps naming the process it was opened for. With a pidfd the
181    /// kernel guarantees this; without one, the PID could in principle be
182    /// recycled, which is exactly why the pidfd is preferred.
183    #[test]
184    fn a_dead_process_reports_dead() {
185        let child = std::process::Command::new("/bin/sh")
186            .args(["-c", "exit 0"])
187            .spawn()
188            .expect("spawn");
189        let pid = child.id();
190        let handle = ProcessLiveness::open(pid).expect("open child");
191        let mut child = child;
192        child.wait().expect("reap");
193        assert!(!handle.is_alive(), "a reaped child must report dead");
194    }
195}
196
197/// Whether two spellings name the same executable image on this host.
198///
199/// This host's paths are case-sensitive and distinguish nothing else, so once
200/// both sides are resolved the comparison is exact. A path that cannot be
201/// canonicalised is compared as written rather than treated as a mismatch,
202/// because "the file moved" and "the caller lacks permission to resolve it"
203/// arrive here identically.
204pub fn process_same_executable_path(actual: &std::path::Path, expected: &std::path::Path) -> bool {
205    let resolve =
206        |path: &std::path::Path| std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
207    resolve(actual) == resolve(expected)
208}
209
210#[cfg(test)]
211mod path_tests {
212    use super::*;
213    use std::path::Path;
214
215    /// Case is meaningful here; two spellings that differ by it are two files.
216    #[test]
217    fn case_distinguishes_two_images() {
218        assert!(!process_same_executable_path(
219            Path::new("/tmp/Daemon"),
220            Path::new("/tmp/daemon"),
221        ));
222    }
223
224    /// A path resolves to itself, canonicalisable or not.
225    #[test]
226    fn a_path_matches_itself() {
227        assert!(process_same_executable_path(
228            Path::new("/tmp/rp-does-not-exist/daemon"),
229            Path::new("/tmp/rp-does-not-exist/daemon"),
230        ));
231        let me = std::env::current_exe().expect("current_exe");
232        assert!(process_same_executable_path(&me, &me));
233    }
234}