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 的標準庫也有一些高級類型,如File
和TcpStream
,它們是這些原始句柄的封裝器,提供了操作系統 API 的高級接口。
這些高級類型也實現了Unix-like
平臺上的FromRawFd
和Windows
上的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 解決方案
OwnedFd
和 BorrowedFd<'fd>
這兩種類型用於替代 RawFd
,對句柄值賦予所有權語義,代表句柄值的 擁有和借用。
OwnedFd
擁有一個 fd
,會在析構的時候關閉它。BorrowedFd<'fd>
中的生命週期參數表示對這個 fd
的訪問被借用多長時間。
對於 Windows 來說,也有類似的類型,但都是Handle
和Socket
形式。
和其他類型相比,I/O
類型並不區分可變和不可變。操作系統資源可以在Rust
的控制之外以各種方式共享,所以I/O
可以被認爲是使用內部可變性。
AsFd
、Into<OwnedFd>
和From<OwnedFd>
這三個概念是AsRawFd::as_raw_fd
、IntoRawFd::into_raw_fd
和FromRawFd::from_raw_fd
的概念性替代,分別適用於大多數使用情況。它們以OwnedFd
和BorrowedFd
的方式工作,所以它們自動執行其I/O
安全不變性。
pub fn do_some_io<FD: AsFd>(input: &FD) -> io::Result<()> {
some_syscall(input.as_fd())
}
使用這個類型,就會避免之前那個問題。由於AsFd
只針對那些適當擁有或借用其文件描述符的類型實現,這個版本的do_some_io
不必擔心被傳遞假的或懸空的文件描述符。
逐步採用
I/O
安全和新的類型和特性不需要立即被採用,可以逐步採用。
-
首先,
std
爲所有相關的std
類型添加新的類型和特質,並提供impls
。這是一個向後兼容的變化。 -
之後,
crate
可以開始使用新的類型,併爲它們自己的類型實現新的特質。這些變化將是很小的,而且是半兼容的,不需要特別的協調。 -
一旦標準庫和足夠多的流行
crate
實現了新的特質,crate
就可以按照自己的節奏開始使用新的特質作爲接受通用參數時的邊界。這些將是與semver
不兼容的變化,儘管大多數切換到這些新特質的API
的用戶不需要任何改變。
原型實現
該 RFC 內容原型已經實現,參見 io-lifetimes[4] 。
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 / AsSocket
、IntoHandle /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
安全也是屬於這類情況,理由有二:
-
I/O
安全錯誤會導致內存安全錯誤,在mmap
周圍的安全包裝器存在的情況下(在具有操作系統特定 API 的平臺上,允許它們是安全的)。 -
I/O安全
錯誤也意味着一段代碼可以讀取、寫入或刪除程序中其他部分使用的數據,而不需要命名它們或給它們一個引用。如果不知道鏈接到程序中的所有其他crate
的實現細節,就不可能約束一個crate
可以做的事情的集合。
原始句柄很像進入獨立地址空間的原始指針;它們可以懸空或以虛假的方式進行計算。I/O
安全與內存安全類似;兩者都是爲了防止詭異的遠隔作用,而且在兩者中,所有權是健壯抽象的主要基礎,所以使用類似的安全概念是很自然的。
相關
-
https://github.com/smiller123/bento[5]
-
https://github.com/bytecodealliance/rsix[6]
-
RFC #3128 IO Safety[7]
-
nrc 的 RFC 索引列表 [8]
參考資料
[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