Rust 構建安全的 I-O

動機

最近 Rust 官方合併了一個 RFC [1],通過引入 I/O 安全的概念和一套新的類型和特質,爲AsRawFd和相關特質的用戶提供關於其原始資源句柄的保證,從而彌補 Rust 中封裝邊界的漏洞。

Rust 標準庫提供了 I/O 安全性,保證程序持有私有的原始句柄(raw handle),其他部分無法訪問它。但是 FromRawFd::from_raw_fd 是 Unsafe 的,所以在 Safe Rust 中無法做到 File::from_raw(7) 這種事。在這個文件描述符上面進行I/O 操作,而這個文件描述符可能被程序的其他部分私自持有。

但是,很多 API 通過接受 原始句柄 來進行 I/O 操作:

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

AsRawFd並沒有限制as_raw_fd的返回值,所以do_some_io最終可以在任意的RawFd值上進行 I/O操作。甚至可以寫do_some_io(&7),因爲RawFd本身實現了AsRawFd。這可能會導致程序訪問錯誤的資源。甚至通過創建在其他部分私有的句柄別名來打破封裝邊界,導致一些詭異的 遠隔作用(Action at a distance)。

遠隔作用Action at a distance)是一種程式設計中的反模式 [2],是指程式某一部分的行爲會廣泛的受到程式其他部分指令 [3] 的影響,而且要找到影響其他程式的指令很困難,甚至根本無法進行。

在一些特殊的情況下,違反 I/O 安全甚至會導致內存安全。

I/O   安全概念引入

標準庫中有一些類型和特質:RawFd(Unix) / RawHandle/RawSocket(Windows) ,它們代表原始的操作系統資源句柄。這些類型本身並不提供任何行爲,而只是代表可以傳遞給底層操作系統 API 的標識符。

這些原始句柄可以被認爲是原始指針,具有類似的危險性。雖然獲得一個原始指針是安全的,但是如果一個原始指針不是一個有效的指針,或者如果它超過了它所指向的內存的生命週期,那麼解引用原始指針可能會調用未定義的行爲。

同樣,通過AsRawFd::as_raw_fd和類似的方式獲得一個原始句柄是安全的,但是如果它不是一個有效的句柄或者在其資源關閉後使用,使用它來做I/O可能會導致輸出損壞、輸入數據丟失或泄漏,或者違反封裝邊界。而在這兩種情況下,影響可能是非局部的且影響到程序中其他不相關的部分。對原始指針危險的保護被稱爲內存安全,所以對原始句柄危險的保護被稱爲I/O安全

Rust 的標準庫也有一些高級類型,如FileTcpStream,它們是這些原始句柄的封裝器,提供了操作系統 API 的高級接口。

這些高級類型也實現了Unix-like平臺上的FromRawFdWindows上的FromRawHandle/FromRawSocket的特性,這些特性提供了包裹底層 (low-level) 值以產生上層(high-level)值的函數。這些函數是不安全的,因爲它們無法保證I/O安全,類型系統並不限制傳入的句柄。

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

// Create a file.
let file = File::open("data.txt")?;

// 從任意的整數值構造 file
// 然而這種類型的檢查在運行時可能無法識別一個合法存活的資源
// 或者它可能意外地在程序的其他地方被以別名方式封裝處理(此處無法判斷)
// 這裏添加  unsafe 塊 是讓調用者來避免上述危險
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")?;

// 進一步使用 raw_fd ,也就是 file 的內部原始句柄,將超出操作系統與之相關的生命週期
// 這可能會導致它意外地與其他封裝好的 file 實例發生別名,比如 another  
// 因此,這裏 unsafe 塊是讓調用者避免上述危險
let dangling = unsafe { File::from_raw_fd(raw_fd) };

調用者必須確保傳入from_raw_fd的值是明確地從操作系統返回的,而且from_raw_fd的返回值不會超過操作系統與句柄相關的生命週期。

I/O 安全的概念雖然是新的,但它反映出了一個普遍的做法。Rust 生態系統將會逐步支持 I/O 安全。

I/O 安全 Rust 解決方案

OwnedFdBorrowedFd<'fd>

這兩種類型用於替代 RawFd ,對句柄值賦予所有權語義,代表句柄值的 擁有和借用。

OwnedFd 擁有一個 fd ,會在析構的時候關閉它。BorrowedFd<'fd> 中的生命週期參數表示對這個 fd 的訪問被借用多長時間。

對於 Windows 來說,也有類似的類型,但都是HandleSocket形式。

iYAfKI

和其他類型相比,I/O  類型並不區分可變和不可變。操作系統資源可以在Rust的控制之外以各種方式共享,所以I/O可以被認爲是使用內部可變性。

AsFdInto<OwnedFd>From<OwnedFd>

這三個概念是AsRawFd::as_raw_fdIntoRawFd::into_raw_fdFromRawFd::from_raw_fd的概念性替代,分別適用於大多數使用情況。它們以OwnedFdBorrowedFd的方式工作,所以它們自動執行其I/O安全不變性。

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

使用這個類型,就會避免之前那個問題。由於AsFd只針對那些適當擁有或借用其文件描述符的類型實現,這個版本的do_some_io不必擔心被傳遞假的或懸空的文件描述符。

逐步採用

I/O安全和新的類型和特性不需要立即被採用,可以逐步採用。

原型實現

該 RFC 內容原型已經實現,參見 io-lifetimes[4] 。

6b8p3o

trait 實現

AsFd 轉換爲 原生 fd ,是帶有生命週期參數的 BorrowedFd<'_>

#[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<'_>;
}

IntoFd從 原生 fd 轉爲 安全的 fd,是 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;
}

FromFd 從原生 fd 構造 OwnedFd

#[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())
    }
}

上述爲針對 Unix 平臺的 trait,該庫也包含 Windows 平臺的相關 triat :AsHandle / AsSocketIntoHandle /IntoSocket  、FromHandle /FromSocket

相關類型

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");
        }
    }
}

爲 std 和其他生態庫 支持安全 I/O

再構建一些跨平臺抽象類型之後,爲  ffi / async_std/ fs_err/ mio/ os_pipe/ socket2/ tokio / std 來支持 安全 I/O 抽象。

使用案例

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

#[cfg(all(rustc_attrs, unix, feature = "close"))]
fn main() -> io::Result<(){
    // write 是 c api,所以用 unsafe
    let fd = unsafe {
        // Open a file, which returns an `Option<OwnedFd>`, which we can
        // maybe convert into an `OwnedFile`.
        // 擁有一個 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.
        // 借用這個 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 了
    let mut file = File::from_fd(fd);
    writeln!(&mut file, "greetings, y'all")?;

    // We can borrow a `BorrowedFd` from a `File`.
    unsafe {
        // 借用 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();

    // 不是必須的,會自動析構 fd 
    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(())
}

理由與替代方案

關於  “unsafe 是爲了內存安全” 的說法

Rust 在歷史上劃定了一條界線,指出 unsafe 僅僅是用於 內存安全相關。比較知名的例子是 std::mem::forget, 它增加是 unsafe 的,後來改爲了 safe。

聲明 unsafe 只用於內存安全的結論表明,unsafe 不應該用於 其他非內存安全類的 API ,比如 標示某個 API 是應該避免使用的之類。

內存安全優先級高於其他缺陷,因爲它不僅僅是爲了避免非預期行爲,而是爲了避免無法約束一段代碼可能做的事情的情況。

I/O 安全也是屬於這類情況,理由有二:

  1. I/O安全錯誤會導致內存安全錯誤,在mmap周圍的安全包裝器存在的情況下(在具有操作系統特定 API 的平臺上,允許它們是安全的)。

  2. I/O安全錯誤也意味着一段代碼可以讀取、寫入或刪除程序中其他部分使用的數據,而不需要命名它們或給它們一個引用。如果不知道鏈接到程序中的所有其他crate的實現細節,就不可能約束一個crate可以做的事情的集合。

原始句柄很像進入獨立地址空間的原始指針;它們可以懸空或以虛假的方式進行計算。I/O安全與內存安全類似;兩者都是爲了防止詭異的遠隔作用,而且在兩者中,所有權是健壯抽象的主要基礎,所以使用類似的安全概念是很自然的。

相關

參考資料

[1] RFC : https://github.com/rust-lang/rfcs/blob/master/text/3128-io-safety.md

[2] 反模式: https://zh.wikipedia.org/wiki / 反模式

[3] 指令: https://zh.wikipedia.org/wiki / 指令

[4] io-lifetimes: https://github.com/sunfishcode/io-lifetimes

[5] https://github.com/smiller123/bento: https://github.com/smiller123/bento

[6] https://github.com/bytecodealliance/rsix: https://github.com/bytecodealliance/rsix

[7] RFC #3128 IO Safety: https://github.com/rust-lang/rfcs/blob/master/text/3128-io-safety.md

[8] nrc 的 RFC 索引列表: https://www.ncameron.org/rfcs/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/C7khFKUm9n7s6hXKcP-wug