Skip to main content

yarli_cli/yarli-exec/src/
pidfd.rs

1//! pidfd-based process handle for race-free signal delivery.
2//!
3//! On Linux 5.3+, `pidfd_open(2)` returns a file descriptor tied to a
4//! specific process. Sending signals through the pidfd avoids PID-recycling
5//! races that plague `kill(2)`.
6//!
7//! [`ProcessHandle`] tries pidfd first and falls back to raw PID when the
8//! syscall is unavailable (older kernels, non-Linux).
9
10#[cfg(unix)]
11mod inner {
12    use std::os::unix::io::RawFd;
13
14    /// A file descriptor obtained from `pidfd_open(2)`, uniquely identifying a process.
15    pub struct PidFd {
16        fd: RawFd,
17    }
18
19    impl PidFd {
20        /// Open a pidfd for the given PID.
21        ///
22        /// Returns `Err` if the syscall is unavailable or the PID is invalid.
23        #[cfg(target_os = "linux")]
24        pub fn open(pid: u32) -> std::io::Result<Self> {
25            let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid as libc::c_int, 0) };
26            if fd < 0 {
27                Err(std::io::Error::last_os_error())
28            } else {
29                Ok(Self { fd: fd as RawFd })
30            }
31        }
32
33        #[cfg(not(target_os = "linux"))]
34        pub fn open(_pid: u32) -> std::io::Result<Self> {
35            Err(std::io::Error::new(
36                std::io::ErrorKind::Unsupported,
37                "pidfd_open is only available on Linux 5.3+",
38            ))
39        }
40
41        /// Send a signal to the process via `pidfd_send_signal(2)`.
42        #[cfg(target_os = "linux")]
43        pub fn send_signal(&self, sig: i32) -> std::io::Result<()> {
44            let rc = unsafe {
45                libc::syscall(
46                    libc::SYS_pidfd_send_signal,
47                    self.fd,
48                    sig,
49                    std::ptr::null::<libc::siginfo_t>(),
50                    0u32,
51                )
52            };
53            if rc == 0 {
54                Ok(())
55            } else {
56                Err(std::io::Error::last_os_error())
57            }
58        }
59
60        #[cfg(not(target_os = "linux"))]
61        pub fn send_signal(&self, _sig: i32) -> std::io::Result<()> {
62            Err(std::io::Error::new(
63                std::io::ErrorKind::Unsupported,
64                "pidfd_send_signal is only available on Linux 5.3+",
65            ))
66        }
67
68        /// Return the raw file descriptor.
69        pub fn as_raw_fd(&self) -> RawFd {
70            self.fd
71        }
72    }
73
74    impl Drop for PidFd {
75        fn drop(&mut self) {
76            unsafe {
77                libc::close(self.fd);
78            }
79        }
80    }
81
82    /// Process handle that uses pidfd when available, falling back to raw PID.
83    pub enum ProcessHandle {
84        /// Race-free handle via pidfd.
85        PidFd(PidFd),
86        /// Fallback: raw PID (subject to PID recycling races).
87        RawPid(u32),
88    }
89
90    impl ProcessHandle {
91        /// Acquire a handle for the given PID. Tries pidfd first, falls back to raw PID.
92        pub fn acquire(pid: u32) -> Self {
93            match PidFd::open(pid) {
94                Ok(fd) => ProcessHandle::PidFd(fd),
95                Err(_) => ProcessHandle::RawPid(pid),
96            }
97        }
98
99        /// Send a signal to the process.
100        pub fn send_signal(&self, sig: i32) -> std::io::Result<()> {
101            match self {
102                ProcessHandle::PidFd(fd) => fd.send_signal(sig),
103                ProcessHandle::RawPid(pid) => {
104                    let rc = unsafe { libc::kill(*pid as i32, sig) };
105                    if rc == 0 {
106                        Ok(())
107                    } else {
108                        let err = std::io::Error::last_os_error();
109                        // ESRCH means the process already exited — treat as success.
110                        if matches!(err.raw_os_error(), Some(code) if code == libc::ESRCH) {
111                            Ok(())
112                        } else {
113                            Err(err)
114                        }
115                    }
116                }
117            }
118        }
119
120        /// Returns `true` if this handle uses a pidfd (race-free).
121        pub fn is_pidfd(&self) -> bool {
122            matches!(self, ProcessHandle::PidFd(_))
123        }
124    }
125
126    /// Send a signal to an entire process group via negative PID.
127    ///
128    /// This is separate from `ProcessHandle` because pidfd_send_signal doesn't
129    /// support process groups — we always use `kill(-pgid, sig)` for group signals.
130    pub fn signal_process_group(pgid: i32, signal: i32) -> std::io::Result<()> {
131        let rc = unsafe { libc::kill(-pgid, signal) };
132        if rc == 0 {
133            return Ok(());
134        }
135        let err = std::io::Error::last_os_error();
136        if matches!(err.raw_os_error(), Some(code) if code == libc::ESRCH) {
137            return Ok(());
138        }
139        Err(err)
140    }
141}
142
143// On non-unix platforms, provide stub types so the rest of the crate compiles.
144#[cfg(not(unix))]
145mod inner {
146    /// Stub process handle for non-unix platforms.
147    pub enum ProcessHandle {}
148
149    impl ProcessHandle {
150        pub fn acquire(_pid: u32) -> Option<Self> {
151            None
152        }
153
154        pub fn send_signal(&self, _sig: i32) -> std::io::Result<()> {
155            match *self {}
156        }
157
158        pub fn is_pidfd(&self) -> bool {
159            match *self {}
160        }
161    }
162
163    pub fn signal_process_group(_pgid: i32, _signal: i32) -> std::io::Result<()> {
164        Ok(())
165    }
166}
167
168pub use inner::*;
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[cfg(target_os = "linux")]
175    #[test]
176    fn pidfd_open_current_process() {
177        let pid = std::process::id();
178        let fd = PidFd::open(pid).expect("should open pidfd for own process");
179        assert!(fd.as_raw_fd() >= 0);
180    }
181
182    #[cfg(target_os = "linux")]
183    #[test]
184    fn pidfd_send_signal_zero() {
185        let pid = std::process::id();
186        let fd = PidFd::open(pid).expect("should open pidfd");
187        // Signal 0 is a no-op probe — just checks if the process exists.
188        fd.send_signal(0).expect("signal 0 to self should succeed");
189    }
190
191    #[cfg(target_os = "linux")]
192    #[test]
193    fn pidfd_open_invalid_pid() {
194        // PID 0 refers to the calling process in kill(), but pidfd_open
195        // does not accept it. We try an unlikely high PID.
196        let result = PidFd::open(u32::MAX);
197        assert!(result.is_err());
198    }
199
200    #[cfg(unix)]
201    #[test]
202    fn process_handle_acquire_returns_some_variant() {
203        let pid = std::process::id();
204        let handle = ProcessHandle::acquire(pid);
205        // On Linux 5.3+ this should be PidFd; on older kernels RawPid.
206        // Either way, sending signal 0 should work.
207        handle.send_signal(0).expect("signal 0 to self should work");
208    }
209
210    #[cfg(unix)]
211    #[test]
212    fn process_handle_send_signal_zero() {
213        let pid = std::process::id();
214        let handle = ProcessHandle::acquire(pid);
215        handle
216            .send_signal(0)
217            .expect("signal probe via handle should succeed");
218    }
219
220    #[cfg(unix)]
221    #[test]
222    fn process_handle_raw_pid_variant_send_signal_zero() {
223        let handle = ProcessHandle::RawPid(std::process::id());
224        assert!(!handle.is_pidfd());
225        handle
226            .send_signal(0)
227            .expect("signal probe via raw pid should succeed");
228    }
229}