Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

kmc-solid: I/O safety #115159

Merged
merged 9 commits into from
Nov 23, 2023
300 changes: 297 additions & 3 deletions library/std/src/os/solid/io.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,309 @@
//! SOLID-specific extensions to general I/O primitives
//!
//! Just like raw pointers, raw SOLID Sockets file descriptors point to
//! resources with dynamic lifetimes, and they can dangle if they outlive their
//! resources or be forged if they're created from invalid values.
//!
//! This module provides three types for representing raw file descriptors
//! with different ownership properties: raw, borrowed, and owned, which are
//! analogous to types used for representing pointers:
//!
//! | Type | Analogous to |
//! | ------------------ | ------------ |
//! | [`RawFd`] | `*const _` |
//! | [`BorrowedFd<'a>`] | `&'a _` |
//! | [`OwnedFd`] | `Box<_>` |
//!
//! Like raw pointers, `RawFd` values are primitive values. And in new code,
//! they should be considered unsafe to do I/O on (analogous to dereferencing
//! them). Rust did not always provide this guidance, so existing code in the
//! Rust ecosystem often doesn't mark `RawFd` usage as unsafe. Once the
//! `io_safety` feature is stable, libraries will be encouraged to migrate,
//! either by adding `unsafe` to APIs that dereference `RawFd` values, or by
//! using to `BorrowedFd` or `OwnedFd` instead.
//!
//! Like references, `BorrowedFd` values are tied to a lifetime, to ensure
//! that they don't outlive the resource they point to. These are safe to
//! use. `BorrowedFd` values may be used in APIs which provide safe access to
//! any system call except for:
//!
//! - `close`, because that would end the dynamic lifetime of the resource
//! without ending the lifetime of the file descriptor.
//!
//! - `dup2`/`dup3`, in the second argument, because this argument is
//! closed and assigned a new resource, which may break the assumptions
//! other code using that file descriptor.
//!
//! `BorrowedFd` values may be used in APIs which provide safe access to `dup`
//! system calls, so types implementing `AsFd` or `From<OwnedFd>` should not
//! assume they always have exclusive access to the underlying file
//! description.
//!
//! Like boxes, `OwnedFd` values conceptually own the resource they point to,
//! and free (close) it when they are dropped.
//!
//! [`BorrowedFd<'a>`]: crate::os::solid::io::BorrowedFd

#![deny(unsafe_op_in_unsafe_fn)]
#![unstable(feature = "solid_ext", issue = "none")]

use crate::fmt;
use crate::marker::PhantomData;
use crate::mem::forget;
use crate::net;
use crate::sys;
use crate::sys_common::{self, AsInner, FromInner, IntoInner};

/// Raw file descriptors.
pub type RawFd = i32;

/// A borrowed SOLID Sockets file descriptor.
///
/// This has a lifetime parameter to tie it to the lifetime of something that
/// owns the socket.
///
/// This uses `repr(transparent)` and has the representation of a host file
/// descriptor, so it can be used in FFI in places where a socket is passed as
/// an argument, it is not captured or consumed, and it never has the value
/// `SOLID_NET_INVALID_FD`.
///
/// This type's `.to_owned()` implementation returns another `BorrowedFd`
/// rather than an `OwnedFd`. It just makes a trivial copy of the raw
/// socket, which is then borrowed under the same lifetime.
#[derive(Copy, Clone)]
#[repr(transparent)]
#[rustc_layout_scalar_valid_range_start(0)]
// This is -2, in two's complement. -1 is `SOLID_NET_INVALID_FD`.
#[rustc_layout_scalar_valid_range_end(0xFF_FF_FF_FE)]
#[rustc_nonnull_optimization_guaranteed]
pub struct BorrowedFd<'socket> {
fd: RawFd,
_phantom: PhantomData<&'socket OwnedFd>,
}
Comment on lines +73 to +82
Copy link
Member

@workingjubilee workingjubilee Oct 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This layout optimization has come into question on Haiku:

I would appreciate it if you weighed in with your thoughts, either here or in that PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This BorrowedFd is specific to the SOLID socket subsystem, so technically it's closer to std::os::windows::io::BorrowedSocket than std::os::fd::BorrowedFd, the POSIX file descriptor that is in question in the referred issue.

Personally, AT_FDCWD feels like a special value that can only be handled by a few supported functions. It's also unusual in that it doesn't point to one static entity over its lifetime, which I suspect may break assumption of some code. I'd be fine with fd::BorrowedFd only being able to hold a "valid" value and leaving the ability to hold such special values unspecified.

Even the POSIX specification's wordings suggest that it's not a file descriptor:

The <fcntl.h> header shall define the following symbolic constant [AT_FDCWD] as a special value used in place of a file descriptor [...]

Copy link
Member

@workingjubilee workingjubilee Oct 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, interesting! I hadn't realized that SOLID was that divergent!


/// An owned SOLID Sockets file descriptor.
///
/// This closes the file descriptor on drop.
///
/// This uses `repr(transparent)` and has the representation of a host file
/// descriptor, so it can be used in FFI in places where a socket is passed as
/// an argument, it is not captured or consumed, and it never has the value
/// `SOLID_NET_INVALID_FD`.
#[repr(transparent)]
#[rustc_layout_scalar_valid_range_start(0)]
// This is -2, in two's complement. -1 is `SOLID_NET_INVALID_FD`.
#[rustc_layout_scalar_valid_range_end(0xFF_FF_FF_FE)]
#[rustc_nonnull_optimization_guaranteed]
pub struct OwnedFd {
fd: RawFd,
}

impl BorrowedFd<'_> {
/// Return a `BorrowedFd` holding the given raw file descriptor.
///
/// # Safety
///
/// The resource pointed to by `fd` must remain open for the duration of
/// the returned `BorrowedFd`, and it must not have the value
/// `SOLID_NET_INVALID_FD`.
#[inline]
pub const unsafe fn borrow_raw(fd: RawFd) -> Self {
assert!(fd != -1 as RawFd);
// SAFETY: we just asserted that the value is in the valid range and
// isn't `-1` (the only value bigger than `0xFF_FF_FF_FE` unsigned)
unsafe { Self { fd, _phantom: PhantomData } }
}
}

impl OwnedFd {
/// Creates a new `OwnedFd` instance that shares the same underlying file
/// description as the existing `OwnedFd` instance.
pub fn try_clone(&self) -> crate::io::Result<Self> {
self.as_fd().try_clone_to_owned()
}
}

impl BorrowedFd<'_> {
/// Creates a new `OwnedFd` instance that shares the same underlying file
/// description as the existing `BorrowedFd` instance.
pub fn try_clone_to_owned(&self) -> crate::io::Result<OwnedFd> {
let fd = sys::net::cvt(unsafe { sys::net::netc::dup(self.as_raw_fd()) })?;
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
}
}

impl AsRawFd for BorrowedFd<'_> {
#[inline]
fn as_raw_fd(&self) -> RawFd {
self.fd
}
}

impl AsRawFd for OwnedFd {
#[inline]
fn as_raw_fd(&self) -> RawFd {
self.fd
}
}

impl IntoRawFd for OwnedFd {
#[inline]
fn into_raw_fd(self) -> RawFd {
let fd = self.fd;
forget(self);
fd
}
}

impl FromRawFd for OwnedFd {
/// Constructs a new instance of `Self` from the given raw file descriptor.
///
/// # Safety
///
/// The resource pointed to by `fd` must be open and suitable for assuming
/// ownership. The resource must not require any cleanup other than `close`.
#[inline]
unsafe fn from_raw_fd(fd: RawFd) -> Self {
assert_ne!(fd, -1 as RawFd);
// SAFETY: we just asserted that the value is in the valid range and
// isn't `-1` (the only value bigger than `0xFF_FF_FF_FE` unsigned)
unsafe { Self { fd } }
}
}

impl Drop for OwnedFd {
#[inline]
fn drop(&mut self) {
unsafe { sys::net::netc::close(self.fd) };
}
}

impl fmt::Debug for BorrowedFd<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BorrowedFd").field("fd", &self.fd).finish()
}
}

impl fmt::Debug for OwnedFd {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OwnedFd").field("fd", &self.fd).finish()
}
}

macro_rules! impl_is_terminal {
($($t:ty),*$(,)?) => {$(
#[unstable(feature = "sealed", issue = "none")]
impl crate::sealed::Sealed for $t {}

#[stable(feature = "is_terminal", since = "1.70.0")]
impl crate::io::IsTerminal for $t {
#[inline]
fn is_terminal(&self) -> bool {
crate::sys::io::is_terminal(self)
}
}
)*}
}

impl_is_terminal!(BorrowedFd<'_>, OwnedFd);

/// A trait to borrow the SOLID Sockets file descriptor from an underlying
/// object.
pub trait AsFd {
/// Borrows the file descriptor.
fn as_fd(&self) -> BorrowedFd<'_>;
}

impl<T: AsFd> AsFd for &T {
#[inline]
fn as_fd(&self) -> BorrowedFd<'_> {
T::as_fd(self)
}
}

impl<T: AsFd> AsFd for &mut T {
#[inline]
fn as_fd(&self) -> BorrowedFd<'_> {
T::as_fd(self)
}
}

impl AsFd for BorrowedFd<'_> {
#[inline]
fn as_fd(&self) -> BorrowedFd<'_> {
*self
}
}

impl AsFd for OwnedFd {
#[inline]
fn as_fd(&self) -> BorrowedFd<'_> {
// Safety: `OwnedFd` and `BorrowedFd` have the same validity
// invariants, and the `BorrowedFd` is bounded by the lifetime
// of `&self`.
unsafe { BorrowedFd::borrow_raw(self.as_raw_fd()) }
}
}

macro_rules! impl_owned_fd_traits {
($($t:ident)*) => {$(
impl AsFd for net::$t {
#[inline]
fn as_fd(&self) -> BorrowedFd<'_> {
self.as_inner().socket().as_fd()
}
}

impl From<net::$t> for OwnedFd {
#[inline]
fn from(socket: net::$t) -> OwnedFd {
socket.into_inner().into_socket().into_inner()
}
}

impl From<OwnedFd> for net::$t {
#[inline]
fn from(owned_fd: OwnedFd) -> Self {
Self::from_inner(FromInner::from_inner(FromInner::from_inner(owned_fd)))
}
}
)*};
}
impl_owned_fd_traits! { TcpStream TcpListener UdpSocket }

/// This impl allows implementing traits that require `AsFd` on Arc.
/// ```
/// # #[cfg(target_os = "solid_asp3")] mod group_cfg {
/// # use std::os::solid::io::AsFd;
/// use std::net::UdpSocket;
/// use std::sync::Arc;
///
/// trait MyTrait: AsFd {}
/// impl MyTrait for Arc<UdpSocket> {}
/// impl MyTrait for Box<UdpSocket> {}
/// # }
/// ```
impl<T: AsFd> AsFd for crate::sync::Arc<T> {
#[inline]
fn as_fd(&self) -> BorrowedFd<'_> {
(**self).as_fd()
}
}

impl<T: AsFd> AsFd for crate::rc::Rc<T> {
#[inline]
fn as_fd(&self) -> BorrowedFd<'_> {
(**self).as_fd()
}
}

impl<T: AsFd> AsFd for Box<T> {
#[inline]
fn as_fd(&self) -> BorrowedFd<'_> {
(**self).as_fd()
}
}

/// A trait to extract the raw SOLID Sockets file descriptor from an underlying
/// object.
pub trait AsRawFd {
Expand Down Expand Up @@ -84,7 +378,7 @@ macro_rules! impl_as_raw_fd {
impl AsRawFd for net::$t {
#[inline]
fn as_raw_fd(&self) -> RawFd {
*self.as_inner().socket().as_inner()
self.as_inner().socket().as_raw_fd()
}
}
)*};
Expand All @@ -97,7 +391,7 @@ macro_rules! impl_from_raw_fd {
impl FromRawFd for net::$t {
#[inline]
unsafe fn from_raw_fd(fd: RawFd) -> net::$t {
let socket = sys::net::Socket::from_inner(fd);
let socket = unsafe { sys::net::Socket::from_raw_fd(fd) };
net::$t::from_inner(sys_common::net::$t::from_inner(socket))
}
}
Expand All @@ -111,7 +405,7 @@ macro_rules! impl_into_raw_fd {
impl IntoRawFd for net::$t {
#[inline]
fn into_raw_fd(self) -> RawFd {
self.into_inner().into_socket().into_inner()
self.into_inner().into_socket().into_raw_fd()
}
}
)*};
Expand Down
2 changes: 1 addition & 1 deletion library/std/src/os/solid/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ pub mod prelude {
pub use super::ffi::{OsStrExt, OsStringExt};
#[doc(no_inline)]
#[stable(feature = "rust1", since = "1.0.0")]
pub use super::io::{AsRawFd, FromRawFd, IntoRawFd, RawFd};
pub use super::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd, RawFd};
}
Loading