motivation

Recently Rust has officially incorporated an RFC to bridge the encapsulation boundary hole in Rust by introducing the concept of I/O security and a new set of types and characteristics to provide AsRawFd and related attributes users with assurance about their raw resource handles.

The Rust standard library provides I/O security by ensuring that a program holds a private raw handle that cannot be accessed by other parts. But FromRawFd::from_raw_fd is Unsafe, so you can’t do File::from_raw(7) in Safe Rust. I/O operations are performed on this file descriptor, which may be privately held by other parts of the program.

However, many apis perform I/O operations by accepting raw handles:

pub fn do_some_io<FD: AsRawFd>(input: &FD) -> io::Result<()> {
    some_syscall(input.as_raw_fd())
}
Copy the code

AsRawFd does not limit the return value of as_RAW_fd, so do_some_io can eventually perform I/O operations on any RawFd value. You can even write do_some_io(&7) because RawFd itself implements AsRawFd. This can cause the program to access the wrong resource. It even breaks encapsulation boundaries by creating handle aliases that are private in other parts, leading to some weird actions at a distance.

Action at a distance is an anti-pattern in programming in which the behavior of one part of a program is widely influenced by instructions in other parts of the program, and it is difficult or impossible to find instructions that affect other programs.

In some special cases, I/O security violations can even lead to memory security.

I/O security concepts are introduced

There are several types and attributes in the library: RawFd(Unix)/RawHandle/RawSocket(Windows), which represent the original operating system resource handle. These types do not themselves provide any behavior, but simply represent identifiers that can be passed to the underlying operating system apis.

These primitive handles can be thought of as primitive Pointers and are similarly dangerous. While it is safe to obtain a primitive pointer, dereferencing the primitive pointer may invoke undefined behavior if a primitive pointer is not a valid pointer, or if it exceeds the lifetime of the memory to which it points.

Similarly, it is safe to obtain a raw handle with AsRawFd:: as_RAW_fd and similar methods, but if it is not a valid handle or is used after its resource is closed, using it for I/O may result in corrupted output, input data loss or leakage, or a violation of encapsulation boundaries. In both cases, the impact may be nonlocal and affect other, unrelated parts of the program. Protection against raw pointer danger is called memory safety, so protection against raw handle danger is called I/O security.

Rust’s standard library also has some high-level types, such as File and TcpStream, which act as wrappers to these primitive handles and provide a high-level interface to the operating system API.

These advanced type also realized – like Unix platform FromRawFd and Windows FromRawHandle/FromRawSocket features, these features provide parcel underlying (low level) value to produce the upper (high – level) function. These functions are not secure because they do not guarantee I/O security, and the type system does not limit incoming handles.

use std::fs::File;
use std::os::unix::io::FromRawFd;

// Create a file.
let file = File::open("data.txt")? ;// Construct file from any integer value
// However, this type of check may not identify a valid resource at run time
// Or it may accidentally be aliased somewhere else in the program.
// The unsafe block is added here to allow the caller to avoid the above dangers
let forged = unsafe { File::from_raw_fd(7)};// Obtain a copy of `file`'s inner raw handle.
let raw_fd = file.as_raw_fd();

// Close `file`.
drop(file);

// Open some unrelated file.
let another = File::open("another.txt")? ;// Further use of raw_fd, the internal raw handle to a file, would extend the operating system's associated life cycle
// This may cause it to be accidentally aliased with other encapsulated file instances, such as another
// Therefore, the unsafe block here is for the caller to avoid the above dangers
let dangling = unsafe { File::from_raw_fd(raw_fd) };
Copy the code

The caller must ensure that the value passed in for from_RAW_fd is explicitly returned from the operating system and that the return value of from_RAW_fd does not exceed the operating system’s handle related life cycle.

While the concept of I/O security is new, it reflects a common practice. The Rust ecosystem will gradually support I/O security.

I/O Security Rust solution

OwnedFdBorrowedFd<'fd>

These two types are used instead of RawFd to assign ownership semantics to handle values, representing ownership and borrowing of handle values.

OwnedFd has a FD and closes it on destructor. The life cycle parameter in BorrowedFd<‘fd> indicates how long access to this FD is borrowed.

For Windows, there are similar types, but they are in Handle and Socket form.

type Similar to the
OwnedFd Box<_>
BorrowedFd<'a> &'a _
RawFd *const _

In contrast to other types, I/O types do not distinguish between mutable and immutable. Operating system resources can be shared in a variety of ways outside of Rust’s control, so I/O can be thought of as using internal variability.

AsFd,Into<OwnedFd>andFrom<OwnedFd>

These three concepts are conceptual alternatives to AsRawFd:: AS_RAW_fd, IntoRawFd:: into_RAW_fd, and FromRawFd:: from_RAW_fd, respectively, for most use cases. They work the way OwnedFd and BorrowedFd do, so they automate their I/O security immutability.

pub fn do_some_io<FD: AsFd>(input: &FD) -> io::Result<()> {
    some_syscall(input.as_fd())
}
Copy the code

Using this type, you avoid the previous problem. Because AsFd is only implemented for types that appropriately own or borrow their file descriptors, this version of DO_some_io does not have to worry about being passed fake or dangling file descriptors.

Phase in

I/O security and new types and features do not need to be adopted immediately, but can be adopted gradually.

  • First of all,stdFor all relatedstdType adds new types and attributes and providesimpls. This is a backward compatible change.
  • After that,crateYou can start using new types and implement new attributes for their own types. These changes will be small and semi-compatible, requiring no special coordination.
  • Once the standard library and enough popularcrateRealizing new qualities,crateAt your own pace, you can begin to use new attributes as boundaries for accepting general parameters. These will be associated withsemverIncompatible changes, although most switch to these new featuresAPIUsers don’t need any changes.

Prototype implementation

This RFC content prototype has been implemented, see io-LifeTimes.

Raw API This experimental API
Raw* Borrowed* and Owned*
AsRaw* As*
IntoRaw* Into*
FromRaw* From*

Trait implementation

AsFd is converted into a native FD, which is BorrowedFd<‘_> with life cycle parameters.

#[cfg(any(unix, target_os = "wasi")))
pub trait AsFd {
    /// Borrows the file descriptor.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    / / / # #! [cfg_attr(io_lifetimes_use_std, feature(io_safety))]
    /// use std::fs::File;
    /// # use std::io;
    /// use io_lifetimes::{AsFd, BorrowedFd};
    ///
    /// let mut f = File::open("foo.txt")? ;
    /// let borrowed_fd: BorrowedFd<'_> = f.as_fd();
    /// # Ok::<(), io::Error>(())
    / / / ` ` `
    fn as_fd(&self) -> BorrowedFd<'_>;
}

Copy the code

IntoFd a conversion from a native FD to a secure FD is OwnedFd

#[cfg(any(unix, target_os = "wasi")))
pub trait IntoFd {
    /// Consumes this object, returning the underlying file descriptor.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    / / / # #! [cfg_attr(io_lifetimes_use_std, feature(io_safety))]
    /// use std::fs::File;
    /// # use std::io;
    /// use io_lifetimes::{IntoFd, OwnedFd};
    ///
    /// let f = File::open("foo.txt")? ;
    /// let owned_fd: OwnedFd = f.into_fd();
    /// # Ok::<(), io::Error>(())
    / / / ` ` `
    fn into_fd(self) -> OwnedFd;
}
Copy the code

FromFd constructs OwnedFd from native FD

#[cfg(any(unix, target_os = "wasi")))
pub trait FromFd {
    /// Constructs a new instance of `Self` from the given file descriptor.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    / / / # #! [cfg_attr(io_lifetimes_use_std, feature(io_safety))]
    /// use std::fs::File;
    /// # use std::io;
    /// use io_lifetimes::{FromFd, IntoFd, OwnedFd};
    ///
    /// let f = File::open("foo.txt")? ;
    /// let owned_fd: OwnedFd = f.into_fd();
    /// let f = File::from_fd(owned_fd);
    /// # Ok::<(), io::Error>(())
    / / / ` ` `
    fn from_fd(owned: OwnedFd) -> Self;

    /// Constructs a new instance of `Self` from the given file descriptor
    /// converted from `into_owned`.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    / / / # #! [cfg_attr(io_lifetimes_use_std, feature(io_safety))]
    /// use std::fs::File;
    /// # use std::io;
    /// use io_lifetimes::{FromFd, IntoFd};
    ///
    /// let f = File::open("foo.txt")? ;
    /// let f = File::from_into_fd(f);
    /// # Ok::<(), io::Error>(())
    / / / ` ` `
    #[inline]
    fn from_into_fd<Owned: IntoFd>(into_owned: Owned) -> Self
    where
        Self: Sized,
    {
        Self::from_fd(into_owned.into_fd())
    }
}
Copy the code

The above traits are for Unix, but the library also includes traits for Windows: AsHandle/AsSocket, IntoHandle /IntoSocket, and FromHandle /FromSocket.

Related to the type

BorrowedFd<'fd>

#[cfg(any(unix, target_os = "wasi")))
#[derive(Copy, Clone)]
#[repr(transparent)]
#[cfg_attr(rustc_attrs, rustc_layout_scalar_valid_range_start(0))]
// libstd/os/raw/mod.rs assures me that every libstd-supported platform has a
// 32-bit c_int. Below is -2, in two's complement, but that only works out
// because c_int is 32 bits.
#[cfg_attr(rustc_attrs, rustc_layout_scalar_valid_range_end(0xFF_FF_FF_FE))]
pub struct BorrowedFd<'fd> {
    fd: RawFd,
    _phantom: PhantomData<&'fd OwnedFd>,
}

#[cfg(any(unix, target_os = "wasi")))
#[repr(transparent)]
#[cfg_attr(rustc_attrs, rustc_layout_scalar_valid_range_start(0))]
// libstd/os/raw/mod.rs assures me that every libstd-supported platform has a
// 32-bit c_int. Below is -2, in two's complement, but that only works out
// because c_int is 32 bits.
#[cfg_attr(rustc_attrs, rustc_layout_scalar_valid_range_end(0xFF_FF_FF_FE))]
pub struct OwnedFd {
    fd: RawFd,
}

#[cfg(any(unix, target_os = "wasi")))
impl BorrowedFd<'_> {
    /// Return a `BorrowedFd` holding the given raw file descriptor.
    ///
    /// # Safety
    ///
    /// The resource pointed to by `raw` must remain open for the duration of
    /// the returned `BorrowedFd`, and it must not have the value `-1`.
    #[inline]
    pub unsafe fn borrow_raw_fd(fd: RawFd) -> Self {
        debug_assert_ne!(fd, -1_i32 as RawFd);
        Self {
            fd,
            _phantom: PhantomData,
        }
    }
}

#[cfg(any(unix, target_os = "wasi")))
impl AsRawFd for BorrowedFd<'_> {
    #[inline]
    fn as_raw_fd(&self) -> RawFd {
        self.fd
    }
}

#[cfg(any(unix, target_os = "wasi")))
impl AsRawFd for OwnedFd {
    #[inline]
    fn as_raw_fd(&self) -> RawFd {
        self.fd
    }
}

#[cfg(any(unix, target_os = "wasi")))
impl IntoRawFd for OwnedFd {
    #[inline]
    fn into_raw_fd(self) -> RawFd {
        let fd = self.fd;
        forget(self);
        fd
    }
}

#[cfg(any(unix, target_os = "wasi")))
impl Drop for OwnedFd {
    #[inline]
    fn drop(&mut self) {
        #[cfg(feature = "close")]
        unsafe {
            let _ = libc::close(self.fd as std::os::raw::c_int);
        }

        // If the `close` feature is disabled, we expect users to avoid letting
        // `OwnedFd` instances drop, so that we don't have to call `close`.
        #[cfg(not(feature = "close")))
        {
            unreachable!("drop called without the \"close\" feature in io-lifetimes"); }}}Copy the code

Support secure I/O for STD and other ecolibraries

After building some cross-platform abstraction types, ffI/async_std/ fs_err/ MIO/OS_PIPE/socket2/ tokio/STD to support secure I/O abstraction.

Use case

// From: https://github.com/sunfishcode/io-lifetimes/blob/main/examples/hello.rs

#[cfg(all(rustc_attrs, unix, feature = "close")))
fn main() -> io::Result< > () {// write is a C API, hence the need for unsafe
    let fd = unsafe {
        // Open a file, which returns an `Option<OwnedFd>`, which we can
        // maybe convert into an `OwnedFile`.
        // Have a fd
        let fd: OwnedFd = open("/dev/stdout\0".as_ptr() as *const_, O_WRONLY | O_CLOEXEC) .ok_or_else(io::Error::last_os_error)? ;// Borrow the fd to write to it.
        // borrow this fd
        let result = write(fd.as_fd(), "hello, world\n".as_ptr() as *const _, 13);
        match result {
            -1= >return Err(io::Error::last_os_error()),
            13= > (), _ = >return Err(io::Error::new(io::ErrorKind::Other, "short write")),
        }

        fd
    };

    // Convert into a `File`. No `unsafe` here!
    // Unsafe is no longer needed here
    let mut file = File::from_fd(fd);
    writeln!(&mut file, "greetings, y'all")? ;// We can borrow a `BorrowedFd` from a `File`.
    unsafe {
        / / borrow fd
        let result = write(file.as_fd(), "sup? \n".as_ptr() as *const _, 5);
        match result {
            -1= >return Err(io::Error::last_os_error()),
            5= > (), _ = >return Err(io::Error::new(io::ErrorKind::Other, "short write")),}}// Now back to `OwnedFd`.
    let fd = file.into_fd();

    // It is not necessary to destruct fd automatically
    unsafe {
        // This isn't needed, since `fd` is owned and would close itself on
        // drop automatically, but it makes a nice demo of passing an `OwnedFd`
        // into an FFI call.
        close(fd);
    }

    Ok(())}Copy the code

Reasons and alternatives

Unsafe is for memory safety

Rust draws a line historically, noting that Unsafe is only for memory-security concerns. A better known example is STD ::mem:: Forget, which was expanded to unsafe and later changed to safe.

Declaring unsafe only for memory safety suggests that unsafe should not be used for other Non-memory safe apis, such as indicating that an API should be avoided.

Memory safety takes precedence over other defects, because it’s not just about avoiding unexpected behavior, it’s about avoiding situations where you can’t constrain what a piece of code might do.

I/O security also falls into this category for two reasons:

  1. I/OSecurity errors can cause memory security errors inmmapIn the case of surrounding security wrappers (allowing them to be safe on platforms with OS specific apis).
  2. The I/OErrors also mean that a piece of code can read, write, or delete data used by other parts of the program without having to name them or give them a reference. If you don’t know all the other links in the programcrateIt is impossible to constrain onecrateA collection of things that can be done.

The primitive handle is much like the primitive pointer into a separate address space; They can be suspended or calculated in a false way. I/O security is similar to memory security; Both are designed to prevent weird alienation, and in both cases ownership is the primary basis for robust abstractions, so it is natural to use similar security concepts.

related

  • Github.com/smiller123/…

  • Github.com/bytecodeall…

  • RFC #3128 IO Safety

  • NRC RFC index list