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