Add possibility to receive without duplicating the fds

This is unsafe for roughly the same reasons as `OwnedFd::from_raw_fd`.

Technically the duplication is also unsafe as it is theorically possible
that the fd will be closed before being taken but fcntl should error out
in this case so we can safely ignore that.
This commit is contained in:
Mathieu Trossevin 2023-12-08 10:03:55 +01:00
parent da5aba2882
commit 7e8f706a1d
Signed by: mtrossevin
GPG key ID: D1DBB7EA828374E9
2 changed files with 157 additions and 24 deletions

View file

@ -3,6 +3,8 @@ use core::num::ParseIntError;
use std::error::Error;
use std::ffi::OsString;
use rustix::io::Errno;
use crate::{FD_NAMES_VAR, FD_NUMBER_VAR, PID_VAR};
#[allow(clippy::module_name_repetitions)]
@ -110,3 +112,52 @@ impl Display for GetFdsError {
}
impl Error for GetFdsError {}
#[derive(Debug)]
pub enum DupError {
InvalidFd(Errno),
InvalidArgument(Errno),
NoAvailableFd(Errno),
Other(Errno),
}
impl From<Errno> for DupError {
fn from(value: Errno) -> Self {
if value == Errno::INVAL {
Self::InvalidArgument(value)
} else if value == Errno::MFILE {
Self::NoAvailableFd(value)
} else if value == Errno::BADF {
Self::InvalidFd(value)
} else {
Self::Other(value)
}
}
}
impl Display for DupError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Couldn't duplicate FD : ")?;
match self {
DupError::InvalidFd(_) => {
f.write_str("The file descriptor isn't valid (probably closed in the meantime).")
}
DupError::InvalidArgument(_) => f.write_str("Invalid argument."),
DupError::NoAvailableFd(_) => {
f.write_str("Too many file descriptors are already open.")
}
DupError::Other(err) => Display::fmt(err, f),
}
}
}
impl Error for DupError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
DupError::InvalidArgument(error)
| DupError::InvalidFd(error)
| DupError::NoAvailableFd(error)
| DupError::Other(error) => Some(error),
}
}
}

View file

@ -5,9 +5,10 @@ use std::os::fd::RawFd;
use std::os::unix::net::{UnixDatagram, UnixListener, UnixStream};
use std::{env, process};
use error::{GetFdsError, ReceiveError, ReceiveNameError};
use rustix::fd::{AsRawFd, BorrowedFd, OwnedFd};
use error::{DupError, GetFdsError, ReceiveError, ReceiveNameError};
use rustix::fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
use rustix::fs::FileType;
use rustix::io::FdFlags;
use rustix::net::SocketType;
mod error;
@ -78,12 +79,45 @@ impl FileDescriptor {
/// Get any file descriptor passed by systemd or anything implementing the `LISTEN_FD` protocol.
///
/// This isn't necessarily limited to File descriptor of listening sockets, IPCs or FIFOs but also anything that is in the file descriptor store.
/// The file descriptores are duplicated using [`fcntl_dupfd_cloexec`](rustix::fs::fcntl_dupfd_cloexec) so they can safely be used from rust and will not be propagated to children process automatically.
///
/// The file descriptors are duplicated using [`fcntl_dupfd_cloexec`](rustix::fs::fcntl_dupfd_cloexec) so they can safely be used from rust and will not be propagated to children process automatically.
///
/// # Errors
///
/// This function will fail if no file descriptor colud be received which might not actually be an error. See [`ReceiveError`] for details.
pub fn receive(unset_env: bool) -> Result<impl IntoIterator<Item = Self>, ReceiveError> {
pub fn receive(
unset_env: bool,
) -> Result<impl IntoIterator<Item = Result<Self, DupError>>, ReceiveError> {
let fds = Self::inner_receive(unset_env)?;
match Self::from_fds(fds) {
Ok(fds) => Ok(fds),
Err(error) => Err(ReceiveError::GetFds(error)),
}
}
/// Get any file descriptor passed by systemd or anything implementing the `LISTEN_FD` protocol.
///
/// This isn't necessarily limited to File descriptor of listening sockets, IPCs or FIFOs but also anything that is in the file descriptor store.
///
/// The file descriptors are taken directly as [`OwnedFd`]s instead of being duplicated. In order to limit unsoundness this function therefore always unset the environment.
///
/// # Safety
///
/// This function is safe if (and only if) the received file descriptors weren't already taken into a owned rust struct.
/// (In short it needs to follow the safety constraints of [`OwnedFd::from_raw_fd`](std::os::fd::FromRawFd)).
/// The simplest way to insure that it is so is to only use functions from this crate to get these file descriptors.
///
/// # Errors
///
/// This function will fail if no file descriptor colud be received which might not actually be an error. See [`ReceiveError`] for details.
pub unsafe fn receive_no_dup() -> Result<impl IntoIterator<Item = Self>, ReceiveError> {
let fds = Self::inner_receive(true)?;
Self::from_fds_no_dup(fds).map_err(ReceiveError::GetFds)
}
fn inner_receive(unset_env: bool) -> Result<usize, ReceiveError> {
let pid = env::var_os(PID_VAR).ok_or(ReceiveError::NoListenPID)?;
let fds = env::var_os(FD_NUMBER_VAR).ok_or(ReceiveError::NoListenFD)?;
tracing::trace!("{PID_VAR} = {pid:?}; {FD_NUMBER_VAR} = {fds:?}");
@ -113,26 +147,56 @@ impl FileDescriptor {
});
}
match Self::from_fds(fds) {
Ok(fds) => Ok(fds),
Err(error) => Err(ReceiveError::GetFds(error)),
}
Ok(fds)
}
/// Get any file descriptor passed using `LISTEN_FD` and their names.
///
/// This isn't necessarily limited to File descriptor of listening sockets, IPCs or FIFOs but also anything that is in the file descriptor store.
/// The file descriptores are duplicated using [`fcntl_dupfd_cloexec`](rustix::fs::fcntl_dupfd_cloexec) so they can safely be used from rust and will not be propagated to children process automatically.
///
/// The file descriptors are duplicated using [`fcntl_dupfd_cloexec`](rustix::fs::fcntl_dupfd_cloexec) so they can safely be used from rust and will not be propagated to children process automatically.
///
/// # Errors
///
/// This function will fail if no file descriptors could be obtained or the names associated with them couldn't be obtained. See [`ReceiveNameError`] for details.
pub fn receive_with_names(
unset_env: bool,
) -> Result<impl IntoIterator<Item = Result<(Self, OsString), DupError>>, ReceiveNameError>
{
let fd_names = Self::get_names()?;
Ok(Self::receive(unset_env)?
.into_iter()
.zip(fd_names)
.map(|(res, name)| res.map(|fd| (fd, name))))
}
/// Get any file descriptor passed using `LISTEN_FD` and their names.
///
/// This isn't necessarily limited to File descriptor of listening sockets, IPCs or FIFOs but also anything that is in the file descriptor store.
///
/// The file descriptors are taken directly as [`OwnedFd`]s instead of being duplicated. In order to limit unsoundness this function therefore always unset the environment.
///
/// # Safety
///
/// This function is safe if (and only if) the received file descriptors weren't already taken into a owned rust struct.
/// (In short it needs to follow the safety constraints of [`OwnedFd::from_raw_fd`](std::os::fd::FromRawFd)).
/// The simplest way to insure that it is so is to only use functions from this crate to get these file descriptors.
///
/// # Errors
///
/// This function will fail if no file descriptors could be obtained or the names associated with them couldn't be obtained. See [`ReceiveNameError`] for details.
///
///
pub unsafe fn receive_with_names_no_dup(
) -> Result<impl IntoIterator<Item = (Self, OsString)>, ReceiveNameError> {
let fd_names = Self::get_names()?;
Ok(Self::receive_no_dup()?.into_iter().zip(fd_names))
}
fn get_names() -> Result<impl IntoIterator<Item = OsString>, ReceiveNameError> {
let fd_names = env::var_os(FD_NAMES_VAR).ok_or(ReceiveNameError::NoListenFDName)?;
tracing::trace!("{FD_NAMES_VAR} = {fd_names:?}");
let fd_names: Vec<_> = fd_names
Ok(fd_names
.as_encoded_bytes()
.split(|b| *b == b':')
.map(|name| {
@ -142,13 +206,12 @@ impl FileDescriptor {
unsafe { OsStr::from_encoded_bytes_unchecked(name) }
})
.map(OsStr::to_os_string)
.collect();
Ok(Self::receive(unset_env)?
.into_iter()
.zip(fd_names))
.collect::<Vec<_>>())
}
fn from_fds(num_fds: usize) -> Result<impl IntoIterator<Item = FileDescriptor>, GetFdsError> {
fn from_fds(
num_fds: usize,
) -> Result<impl IntoIterator<Item = Result<Self, DupError>>, GetFdsError> {
if SD_LISTEN_FDS_START.checked_add(num_fds as RawFd).is_none() {
return Err(GetFdsError::TooManyFDs(num_fds));
}
@ -156,19 +219,38 @@ impl FileDescriptor {
Ok((0..num_fds).map(|fd_offset| {
SD_LISTEN_FDS_START
.checked_add(fd_offset as RawFd)
// SAFETY: We are receiving the fd so it should be safe
.map(|fd| FileDescriptor::from_fd(fd, 0))
.map(|fd| {
// SAFETY: The file descriptor won't be closed by the time we duplicate it.
let fd = unsafe { BorrowedFd::borrow_raw(fd) };
rustix::fs::fcntl_dupfd_cloexec(fd, 0).map_err(DupError::from)
})
.expect("Already checked against overflow.")
.map(Self::from_fd)
}))
}
unsafe fn from_fds_no_dup(
num_fds: usize,
) -> Result<impl IntoIterator<Item = Self>, GetFdsError> {
if SD_LISTEN_FDS_START.checked_add(num_fds as RawFd).is_none() {
return Err(GetFdsError::TooManyFDs(num_fds));
}
Ok((0..num_fds).map(|fd_offset| {
SD_LISTEN_FDS_START
.checked_add(fd_offset as RawFd)
.map(|fd| {
let fd = unsafe { OwnedFd::from_raw_fd(fd) };
let flags = rustix::fs::fcntl_getfd(&fd).unwrap();
rustix::fs::fcntl_setfd(&fd, flags.union(FdFlags::CLOEXEC)).unwrap();
fd
})
.map(Self::from_fd)
.expect("Already checked against overflow.")
}))
}
fn from_fd(fd: RawFd, min_new: RawFd) -> Self {
let fd = {
// SAFETY: The file descriptor won't be closed by the time we duplicate it.
let fd = unsafe { BorrowedFd::borrow_raw(fd) };
rustix::fs::fcntl_dupfd_cloexec(fd, min_new)
.expect("Couldn't duplicate the file descriptor")
};
fn from_fd(fd: OwnedFd) -> Self {
let stat = rustix::fs::fstat(&fd)
.expect("This should only ever be called on a valid file descriptor.");
let file_type = FileType::from_raw_mode(stat.st_mode);