diff --git a/changelog/2748.added.md b/changelog/2748.added.md new file mode 100644 index 0000000000..848a76e5ce --- /dev/null +++ b/changelog/2748.added.md @@ -0,0 +1 @@ +Added `pidfd_open(2)` and `pidfd_send_signal(2)` diff --git a/src/sys/mod.rs b/src/sys/mod.rs index bfdf4d07ee..ade7228672 100644 --- a/src/sys/mod.rs +++ b/src/sys/mod.rs @@ -64,6 +64,12 @@ feature! { pub mod personality; } +#[cfg(target_os = "linux")] +feature! { + #![feature = "process"] + pub mod pidfd; +} + #[cfg(target_os = "linux")] feature! { #![feature = "process"] diff --git a/src/sys/pidfd.rs b/src/sys/pidfd.rs new file mode 100644 index 0000000000..b0a7079357 --- /dev/null +++ b/src/sys/pidfd.rs @@ -0,0 +1,137 @@ +//! PID file descriptor APIs. + +use std::{ + hint::unreachable_unchecked, + os::fd::{FromRawFd as _, OwnedFd, RawFd}, + ptr, +}; + +use bitflags::bitflags; + +use crate::{errno::Errno, unistd::Pid}; + +bitflags! { + /// Flags for [`pidfd_open`]. + #[derive(Copy, Clone)] + pub struct PidfdFlags: libc::c_uint { + /// See [`pidfd_open(2)`] for details. + /// + /// [`pidfd_open(2)`]: https://man7.org/linux/man-pages/man2/pidfd_open.2.html + const PIDFD_NONBLOCK = libc::PIDFD_NONBLOCK; + + /// See [`pidfd_open(2)`] for details. + /// + /// [`pidfd_open(2)`]: https://man7.org/linux/man-pages/man2/pidfd_open.2.html + const PIDFD_THREAD = libc::PIDFD_THREAD; + } +} + +/// Open a PIDFD of the provided PID. +/// +/// See [`pidfd_open(2)`] for details. +/// +/// [`pidfd_open(2)`]: https://man7.org/linux/man-pages/man2/pidfd_open.2.html +/// +/// # Safety +/// +/// This can race with the PID in `pid` getting released or recycled, unless the +/// PID is that of the calling process, in which case the only way a race could +/// happen is if the current process exits. +/// +/// This function is [async-signal-safe], although it may modify `errno`. +/// +/// [async-signal-safe]: https://man7.org/linux/man-pages/man7/signal-safety.7.html +pub fn pidfd_open(pid: Pid, flags: PidfdFlags) -> Result { + // SAFETY: + // + // * Arguments passed to the syscall have the correct types. + // * The kernel should not return a value that cannot fit in `RawFd`. + // * The file descriptor returned by the kernel is open, owned, and requires + // only `close` to release its resources. + // * The kernel should not return any negative value other than `-1`. + unsafe { + match libc::syscall(libc::SYS_pidfd_open, pid.as_raw(), flags.bits()) { + fd if fd >= 0 => { + Ok(OwnedFd::from_raw_fd(RawFd::try_from(fd).unwrap_unchecked())) + } + -1 => Err(Errno::last()), + _ => unreachable_unchecked(), + } + } +} + +feature! { +#![feature = "signal"] + +use std::os::fd::{AsFd, AsRawFd}; + +use libc::siginfo_t; + +use crate::sys::signal::Signal; + +bitflags! { + /// Flags for [`pidfd_send_signal`]. + #[derive(Copy, Clone)] + pub struct PidfdSignalFlags: libc::c_uint { + /// See [`pidfd_send_signal(2)`] for details. + /// + /// [`pidfd_send_signal(2)`]: https://man7.org/linux/man-pages/man2/pidfd_send_signal.2.html + const PIDFD_SIGNAL_THREAD = libc::PIDFD_SIGNAL_THREAD; + + /// See [`pidfd_send_signal(2)`] for details. + /// + /// [`pidfd_send_signal(2)`]: https://man7.org/linux/man-pages/man2/pidfd_send_signal.2.html + const PIDFD_SIGNAL_THREAD_GROUP = libc::PIDFD_SIGNAL_THREAD_GROUP; + + /// See [`pidfd_send_signal(2)`] for details. + /// + /// [`pidfd_send_signal(2)`]: https://man7.org/linux/man-pages/man2/pidfd_send_signal.2.html + const PIDFD_SIGNAL_PROCESS_GROUP = libc::PIDFD_SIGNAL_PROCESS_GROUP; + } +} + +/// Send a signal to a process by its PIDFD. +/// +/// See [`pidfd_send_signal(2)`] for details. +/// +/// [`pidfd_send_signal(2)`]: https://man7.org/linux/man-pages/man2/pidfd_send_signal.2.html +/// +/// # Safety +/// +/// This function is [async-signal-safe], although it may modify `errno`. +/// +/// [async-signal-safe]: https://man7.org/linux/man-pages/man7/signal-safety.7.html +pub fn pidfd_send_signal( + pidfd: T, + signal: Signal, + signal_info: Option, + flags: PidfdSignalFlags, +) -> Result<(), Errno> +where + T: AsFd, +{ + let signal_info = match signal_info { + Some(x) => &x, + None => ptr::null(), + }; + + // SAFETY: + // + // * Arguments passed to the syscall have the correct types. + // * The kernel should not return any value other than `0` and `-1`. + unsafe { + match libc::syscall( + libc::SYS_pidfd_send_signal, + pidfd.as_fd().as_raw_fd(), + signal as libc::c_int, + signal_info, + flags.bits(), + ) { + 0 => Ok(()), + -1 => Err(Errno::last()), + _ => unreachable_unchecked(), + } + } +} + +} diff --git a/test/sys/mod.rs b/test/sys/mod.rs index ab7c5167ce..c0b2394002 100644 --- a/test/sys/mod.rs +++ b/test/sys/mod.rs @@ -96,3 +96,6 @@ mod test_resource; // only enable this for FreeBSD for now. #[cfg(target_os = "freebsd")] mod test_memfd; + +#[cfg(all(target_os = "linux", feature = "process"))] +mod test_pidfd; diff --git a/test/sys/test_pidfd.rs b/test/sys/test_pidfd.rs new file mode 100644 index 0000000000..1bd0c7ed20 --- /dev/null +++ b/test/sys/test_pidfd.rs @@ -0,0 +1,73 @@ +use nix::{ + errno::Errno, + sys::pidfd::{pidfd_open, PidfdFlags}, + unistd::getpid, +}; + +#[test] +fn test_pidfd_open() { + match pidfd_open(getpid(), PidfdFlags::empty()) { + Ok(_) => (), + Err(Errno::ENOSYS) => (), + Err(e) => panic!("{e}"), + } +} + +#[cfg(feature = "signal")] +mod send_signal { + use nix::{ + errno::Errno, + sys::{ + pidfd::{ + pidfd_open, pidfd_send_signal, PidfdFlags, PidfdSignalFlags, + }, + signal::Signal, + wait::{waitpid, WaitStatus}, + }, + unistd::{fork, getpid, ForkResult}, + }; + + #[test] + fn test_pidfd_send_signal() { + // NOTE: This function MUST be async-signal-safe. + fn child_process() -> ! { + let pidfd = match pidfd_open(getpid(), PidfdFlags::empty()) { + Ok(x) => x, + Err(Errno::ENOSYS) => std::process::exit(2), + Err(_) => std::process::exit(1), + }; + + // Code beyond this call should be unreachable. + match pidfd_send_signal( + &pidfd, + Signal::SIGKILL, + None, + PidfdSignalFlags::empty(), + ) { + Ok(()) => (), + Err(Errno::ENOSYS) => std::process::exit(2), + Err(_) => std::process::exit(1), + } + + std::process::exit(1) + } + + // SAFETY: `child_process` is async-signal-safe. + let child = unsafe { + match fork().expect("should be able to fork") { + ForkResult::Parent { child } => child, + ForkResult::Child => child_process(), + } + }; + + match waitpid(child, None) { + // Kernel has PIDFD syscalls and they worked. + Ok(WaitStatus::Signaled(x, Signal::SIGKILL, _)) if x == child => (), + + // Kernel does not have PIDFD syscalls. + Ok(WaitStatus::Exited(x, 2)) if x == child => (), + + _ => panic!("unexpected waitpid result"), + } + } +}