Skip to content

Commit 6925bea

Browse files
committed
Implement the "linux_pidfd" feature for FreeBSD
FreeBSD's procdesc(4)[^1] is a file-descriptor-oriented interface to process signalling and control. It's a race-free replacement for fork() and kill(), and it's compatible with capsicum[^2]. Its drawbacks are that there isn't yet a way to use it with posix_spawn, and there's no capsicum-compatible way to use it with wait(). But it's still sufficient for the linux_pidfd feature. [^1]: https://man.freebsd.org/cgi/man.cgi?query=procdesc [^2]: https://man.freebsd.org/cgi/man.cgi?query=capsicum
1 parent 8c52f73 commit 6925bea

10 files changed

Lines changed: 389 additions & 29 deletions

File tree

library/std/src/os/freebsd/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44

55
pub mod fs;
66
pub mod net;
7+
pub mod process;
78
pub mod raw;
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
//! FreeBSD-specific extensions to primitives in the [`std::process`] module.
2+
//!
3+
//! [`std::process`]: crate::process
4+
// FIXME this file is identical to src/os/linux/process, except for documentation. Can we combine
5+
// the two?
6+
7+
#![unstable(feature = "linux_pidfd", issue = "82971")]
8+
9+
use crate::io::Result;
10+
use crate::os::unix::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd, RawFd};
11+
use crate::process::{self, ExitStatus};
12+
use crate::sealed::Sealed;
13+
use crate::sys::{AsInner, AsInnerMut, FromInner, IntoInner};
14+
#[cfg(not(doc))]
15+
use crate::sys::{fd::FileDesc, freebsd::pidfd::PidFd as InnerPidFd};
16+
17+
#[cfg(doc)]
18+
struct InnerPidFd;
19+
20+
/// This type represents a file descriptor that refers to a process.
21+
///
22+
/// A `PidFd` can be obtained by setting the corresponding option on [`Command`]
23+
/// with [`create_pidfd`]. Subsequently, the created pidfd can be retrieved
24+
/// from the [`Child`] by calling [`pidfd`] or [`into_pidfd`].
25+
///
26+
/// Example:
27+
/// ```no_run
28+
/// #![feature(linux_pidfd)]
29+
/// use std::os::freebsd::process::{CommandExt, ChildExt};
30+
/// use std::process::Command;
31+
///
32+
/// let mut child = Command::new("echo")
33+
/// .create_pidfd(true)
34+
/// .spawn()
35+
/// .expect("Failed to spawn child");
36+
///
37+
/// let pidfd = child
38+
/// .into_pidfd()
39+
/// .expect("Failed to retrieve pidfd");
40+
///
41+
/// // The file descriptor will be closed when `pidfd` is dropped.
42+
/// ```
43+
/// Refer to the man page of [`pdfork(2)`] for further details.
44+
///
45+
/// [`Command`]: process::Command
46+
/// [`create_pidfd`]: CommandExt::create_pidfd
47+
/// [`Child`]: process::Child
48+
/// [`pidfd`]: fn@ChildExt::pidfd
49+
/// [`into_pidfd`]: ChildExt::into_pidfd
50+
/// [`pdfork(2)`]: https://man.freebsd.org/cgi/man.cgi?query=pdfork
51+
#[derive(Debug)]
52+
#[repr(transparent)]
53+
pub struct PidFd {
54+
inner: InnerPidFd,
55+
}
56+
57+
impl PidFd {
58+
/// Forces the child process to exit.
59+
///
60+
/// Unlike [`Child::kill`] it is possible to attempt to kill
61+
/// reaped children since PidFd does not suffer from pid recycling
62+
/// races. But doing so will return an Error.
63+
///
64+
/// [`Child::kill`]: process::Child::kill
65+
pub fn kill(&self) -> Result<()> {
66+
self.inner.kill()
67+
}
68+
69+
/// Waits for the child to exit completely, returning the status that it exited with.
70+
pub fn wait(&self) -> Result<ExitStatus> {
71+
self.inner.wait().map(FromInner::from_inner)
72+
}
73+
74+
/// Attempts to collect the exit status of the child if it has already exited.
75+
pub fn try_wait(&self) -> Result<Option<ExitStatus>> {
76+
Ok(self.inner.try_wait()?.map(FromInner::from_inner))
77+
}
78+
}
79+
80+
impl AsInner<InnerPidFd> for PidFd {
81+
#[inline]
82+
fn as_inner(&self) -> &InnerPidFd {
83+
&self.inner
84+
}
85+
}
86+
87+
impl FromInner<InnerPidFd> for PidFd {
88+
fn from_inner(inner: InnerPidFd) -> PidFd {
89+
PidFd { inner }
90+
}
91+
}
92+
93+
impl IntoInner<InnerPidFd> for PidFd {
94+
fn into_inner(self) -> InnerPidFd {
95+
self.inner
96+
}
97+
}
98+
99+
impl AsRawFd for PidFd {
100+
#[inline]
101+
fn as_raw_fd(&self) -> RawFd {
102+
self.as_inner().as_inner().as_raw_fd()
103+
}
104+
}
105+
106+
impl FromRawFd for PidFd {
107+
unsafe fn from_raw_fd(fd: RawFd) -> Self {
108+
Self::from_inner(InnerPidFd::from_raw_fd(fd))
109+
}
110+
}
111+
112+
impl IntoRawFd for PidFd {
113+
fn into_raw_fd(self) -> RawFd {
114+
self.into_inner().into_inner().into_raw_fd()
115+
}
116+
}
117+
118+
impl AsFd for PidFd {
119+
fn as_fd(&self) -> BorrowedFd<'_> {
120+
self.as_inner().as_inner().as_fd()
121+
}
122+
}
123+
124+
impl From<OwnedFd> for PidFd {
125+
fn from(fd: OwnedFd) -> Self {
126+
Self::from_inner(InnerPidFd::from_inner(FileDesc::from_inner(fd)))
127+
}
128+
}
129+
130+
impl From<PidFd> for OwnedFd {
131+
fn from(pid_fd: PidFd) -> Self {
132+
pid_fd.into_inner().into_inner().into_inner()
133+
}
134+
}
135+
136+
/// Os-specific extensions for [`Child`]
137+
///
138+
/// [`Child`]: process::Child
139+
pub trait ChildExt: Sealed {
140+
/// Obtains a reference to the [`PidFd`] created for this [`Child`], if available.
141+
///
142+
/// A pidfd will only be available if its creation was requested with
143+
/// [`create_pidfd`] when the corresponding [`Command`] was created.
144+
///
145+
/// Even if requested, a pidfd may not be available if some other error occurred.
146+
///
147+
/// [`Command`]: process::Command
148+
/// [`create_pidfd`]: CommandExt::create_pidfd
149+
/// [`Child`]: process::Child
150+
fn pidfd(&self) -> Result<&PidFd>;
151+
152+
/// Returns the [`PidFd`] created for this [`Child`], if available.
153+
/// Otherwise self is returned.
154+
///
155+
/// A pidfd will only be available if its creation was requested with
156+
/// [`create_pidfd`] when the corresponding [`Command`] was created.
157+
///
158+
/// Taking ownership of the PidFd consumes the Child to avoid pid reuse
159+
/// races. Use [`pidfd`] and [`BorrowedFd::try_clone_to_owned`] if
160+
/// you don't want to disassemble the Child yet.
161+
///
162+
/// [`Command`]: process::Command
163+
/// [`create_pidfd`]: CommandExt::create_pidfd
164+
/// [`pidfd`]: ChildExt::pidfd
165+
/// [`Child`]: process::Child
166+
fn into_pidfd(self) -> crate::result::Result<PidFd, Self>
167+
where
168+
Self: Sized;
169+
}
170+
171+
/// Os-specific extensions for [`Command`]
172+
///
173+
/// [`Command`]: process::Command
174+
pub trait CommandExt: Sealed {
175+
/// Sets whether a [`PidFd`](struct@PidFd) should be created for the [`Child`]
176+
/// spawned by this [`Command`].
177+
/// By default, no pidfd will be created.
178+
///
179+
/// The pidfd can be retrieved from the child with [`pidfd`] or [`into_pidfd`].
180+
///
181+
/// A pidfd will only be created if it is possible to do so
182+
/// in a guaranteed race-free manner. Otherwise, [`pidfd`] will return an error.
183+
///
184+
/// If a pidfd has been successfully created and not been taken from the `Child`
185+
/// then calls to `kill()`, `wait()` and `try_wait()` will use the pidfd
186+
/// instead of the pid. This can prevent pid recycling races, e.g.
187+
/// those caused by rogue libraries in the same process prematurely reaping
188+
/// zombie children via `waitpid(-1, ...)` calls.
189+
///
190+
/// [`Command`]: process::Command
191+
/// [`Child`]: process::Child
192+
/// [`pidfd`]: fn@ChildExt::pidfd
193+
/// [`into_pidfd`]: ChildExt::into_pidfd
194+
fn create_pidfd(&mut self, val: bool) -> &mut process::Command;
195+
}
196+
197+
impl CommandExt for process::Command {
198+
fn create_pidfd(&mut self, val: bool) -> &mut process::Command {
199+
self.as_inner_mut().create_pidfd(val);
200+
self
201+
}
202+
}

library/std/src/process/tests.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -478,8 +478,11 @@ fn env_empty() {
478478
#[cfg(not(windows))]
479479
#[cfg_attr(any(target_os = "emscripten", target_env = "sgx"), ignore)]
480480
fn debug_print() {
481-
const PIDFD: &'static str =
482-
if cfg!(target_os = "linux") { " create_pidfd: false,\n" } else { "" };
481+
const PIDFD: &'static str = if cfg!(any(target_os = "freebsd", target_os = "linux")) {
482+
" create_pidfd: false,\n"
483+
} else {
484+
""
485+
};
483486

484487
let mut command = Command::new("some-boring-name");
485488

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod pidfd;
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
//! [`procdesc(4)`] support for FreeBSD
2+
//!
3+
//! `procdesc` is a file-descriptor-oriented interface to process signalling and control. It's
4+
//! been available since FreeBSD 9.0, which is older than the oldest version of FreeBSD ever
5+
//! supported by Rust, so there is no need for backwards-compatibility shims.
6+
//!
7+
//! Compared to Linux's process descriptors, there are a few differences:
8+
//!
9+
//! Feature | FreeBSD | Linux
10+
//! ---------------+-------------------------------+---------------------------------------------
11+
//! Creation | pdfork() | Any process-creation syscall plus pidfd_open
12+
//! Monitoring | kevent(), poll(), select() | epoll(), poll(), select()
13+
//! Convert to pid | pdgetpid() | ioctl(PIDFD_GET_INFO)
14+
//! Signalling | pdkill() | pidfd_send_signal
15+
//! Waiting | Any wait() variant, with pid | waitid(P_PIDFD)
16+
//! Reaping | Any wait() variant, or close()| Any wait() variant
17+
//! ---------------+-------------------------------+---------------------------------------------
18+
//!
19+
//! [`procdesc(4)`]: https://man.freebsd.org/cgi/man.cgi?query=procdesc
20+
use crate::io;
21+
use crate::os::fd::{AsRawFd, FromRawFd, IntoRawFd, RawFd};
22+
use crate::sys::fd::FileDesc;
23+
use crate::sys::process::ExitStatus;
24+
use crate::sys::{AsInner, FromInner, IntoInner, cvt};
25+
26+
#[cfg(test)]
27+
mod tests;
28+
29+
#[derive(Debug)]
30+
pub(crate) struct PidFd(FileDesc);
31+
32+
impl PidFd {
33+
pub fn kill(&self) -> io::Result<()> {
34+
self.send_signal(libc::SIGKILL)
35+
}
36+
37+
pub(crate) fn pid(&self) -> io::Result<u32> {
38+
let mut pid = 0;
39+
cvt(unsafe { libc::pdgetpid(self.0.as_raw_fd(), &mut pid) })?;
40+
Ok(pid as u32)
41+
}
42+
43+
fn waitid(&self, options: libc::c_int) -> io::Result<Option<ExitStatus>> {
44+
// FreeBSD's wait(2) family of functions doesn't yet work with process descriptors
45+
// directly. Using waitpid and pdgetpid in combination is technically racy, because the
46+
// pid might get recycled after waitpid and before dropping the PidFd. That will be fixed
47+
// in FreeBSD 16.0.
48+
//
49+
// A race-free method for older releases of FreeBSD would be to use kevent with
50+
// EVFILT_PROCDESC to get the process's exit status, and then close the process descriptor
51+
// to reap it. But closing the process descriptor would require a &mut self reference,
52+
// which this API does not allow.
53+
//
54+
// The process descriptor will eventually be closed by OwnedFd::drop .
55+
let mut siginfo: libc::siginfo_t = unsafe { crate::mem::zeroed() };
56+
cvt(unsafe {
57+
libc::waitid(libc::P_PID, self.pid()? as libc::id_t, &mut siginfo, options)
58+
})?;
59+
if unsafe { siginfo.si_pid() } == 0 {
60+
Ok(None)
61+
} else {
62+
Ok(Some(ExitStatus::from_waitid_siginfo(siginfo)))
63+
}
64+
}
65+
66+
pub(crate) fn send_signal(&self, signal: i32) -> io::Result<()> {
67+
cvt(unsafe { libc::pdkill(self.0.as_raw_fd(), signal) }).map(drop)
68+
}
69+
70+
pub fn wait(&self) -> io::Result<ExitStatus> {
71+
let r = self.waitid(libc::WEXITED)?;
72+
match r {
73+
Some(exit_status) => Ok(exit_status),
74+
None => unreachable!("waitid with WEXITED should not return None"),
75+
}
76+
}
77+
78+
pub fn try_wait(&self) -> io::Result<Option<ExitStatus>> {
79+
self.waitid(libc::WEXITED | libc::WNOHANG)
80+
}
81+
}
82+
83+
impl AsInner<FileDesc> for PidFd {
84+
fn as_inner(&self) -> &FileDesc {
85+
&self.0
86+
}
87+
}
88+
89+
impl IntoInner<FileDesc> for PidFd {
90+
fn into_inner(self) -> FileDesc {
91+
self.0
92+
}
93+
}
94+
95+
impl FromInner<FileDesc> for PidFd {
96+
fn from_inner(inner: FileDesc) -> Self {
97+
Self(inner)
98+
}
99+
}
100+
101+
impl FromRawFd for PidFd {
102+
unsafe fn from_raw_fd(fd: RawFd) -> Self {
103+
Self(FileDesc::from_raw_fd(fd))
104+
}
105+
}
106+
107+
impl IntoRawFd for PidFd {
108+
fn into_raw_fd(self) -> RawFd {
109+
self.0.into_raw_fd()
110+
}
111+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
include!("../../linux/pidfd/tests.rs");

library/std/src/sys/pal/unix/linux/pidfd/tests.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
use super::PidFd as InternalPidFd;
21
use crate::assert_matches::assert_matches;
32
use crate::os::fd::AsRawFd;
3+
#[cfg(target_os = "freebsd")]
4+
use crate::os::freebsd::process::{ChildExt, CommandExt as _};
5+
#[cfg(target_os = "linux")]
46
use crate::os::linux::process::{ChildExt, CommandExt as _};
57
use crate::os::unix::process::{CommandExt as _, ExitStatusExt};
68
use crate::process::Command;
@@ -107,6 +109,12 @@ fn test_pidfd() {
107109
assert_matches!(res, Err(e) if e.raw_os_error() == Some(libc::ESRCH));
108110
}
109111

112+
#[cfg(target_os = "freebsd")]
110113
fn probe_pidfd_support() -> bool {
111-
InternalPidFd::current_process().is_ok()
114+
true
115+
}
116+
117+
#[cfg(target_os = "linux")]
118+
fn probe_pidfd_support() -> bool {
119+
super::PidFd::current_process().is_ok()
112120
}

library/std/src/sys/pal/unix/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
use crate::io;
44

5+
#[cfg(target_os = "freebsd")]
6+
pub mod freebsd;
57
#[cfg(target_os = "fuchsia")]
68
pub mod fuchsia;
79
pub mod futex;

0 commit comments

Comments
 (0)