мотивация
Недавно Rust официально объединил RFC , путем введения концепции безопасности ввода-вывода и нового набора типов и характеристик дляAsRawFd
А пользователи родственных трейтов предоставляют гарантии относительно своего исходного дескриптора ресурса, тем самым закрывая дыру в границах инкапсуляции в Rust.
Стандартная библиотека Rust обеспечивает безопасность ввода-вывода, гарантируя, что программа имеет собственный необработанный дескриптор, к которому никакие другие части не могут получить доступ. ноFromRawFd::from_raw_fd
небезопасно, поэтому это невозможно сделать в Safe RustFile::from_raw(7)
Такого рода вещи. в этом файловом дескрипторе I/O
операции, и этот файловый дескриптор может находиться в частном владении других частей программы.
Однако многие API выполняют ввод-вывод, принимая необработанные дескрипторы:
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 the Distance).
Дистанционный эффект(Action at a distance) это программированиеантишаблон, что означает, что поведение одной части программы сильно зависит от других частей программы.инструкция, а найти инструкции, влияющие на другие программы, сложно или невозможно.
В некоторых особых случаях нарушение безопасности ввода / вывода может даже привести к безопасности памяти.
Знакомство с концепцией безопасности ввода/вывода
В стандартной библиотеке есть несколько типов и трейтов:RawFd(Unix) / RawHandle/RawSocket(Windows)
, которые представляют необработанные дескрипторы ресурсов операционной системы. Эти типы сами по себе не обеспечивают никакого поведения, а просто представляют собой идентификаторы, которые можно передать базовому API операционной системы.
Эти необработанные дескрипторы можно рассматривать как необработанные указатели с аналогичными опасностями. Хотя получить необработанный указатель безопасно, разыменование необработанного указателя может привести к неопределенному поведению, если необработанный указатель не является допустимым указателем или превышает время жизни памяти, на которую он указывает.
Аналогично, поAsRawFd::as_raw_fd
и подобные способы получения необработанного дескриптора безопасны, но если он не является допустимым дескриптором или используется после того, как его ресурсы закрыты, используйте его для выполненияI/O
Может привести к повреждению выходных данных, потере или утечке входных данных или нарушению границ инкапсуляции. В обоих случаях воздействие может быть нелокальным и затрагивать другие несвязанные части программы. Защита от опасности необработанных указателей называется безопасностью памяти, поэтомуЗащита от опасности необработанных ручек называетсяI/O
Безопасность.
Стандартная библиотека Rust также имеет некоторые расширенные типы, такие какFile
иTcpStream
, которые являются оболочками для этих необработанных дескрипторов, предоставляя высокоуровневый интерфейс для API операционной системы.
Эти расширенные типы также реализуютUnix-like
на платформеFromRawFd
иWindows
ВверхFromRawHandle/FromRawSocket
Функции, которые предоставляют функции, которые оборачивают низкоуровневые значения для получения высокоуровневых значений. Эти функции небезопасны, потому что они не гарантируются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 Safe Rust Solutions
OwnedFd
иBorrowedFd<'fd>
Эти два типа используются для заменыRawFd
, который присваивает семантику владения значению дескриптора, представляя право собственности и заимствование значения дескриптора.
OwnedFd
Имеетсяfd
, который будет закрыт, когда он разрушается.BorrowedFd<'fd>
Параметр жизненного цикла вfd
На какой срок был заимствован доступ.
Для Windows существуют похожие типы, но обаHandle
иSocket
форма.
тип | похожий на |
---|---|
OwnedFd |
Box<_> |
BorrowedFd<'a> |
&'a _ |
RawFd |
*const _ |
По сравнению с другими видами,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.
Raw API |
This experimental API |
---|---|
Raw* |
Borrowed* and Owned*
|
AsRaw* |
As* |
IntoRaw* |
Into* |
FromRaw* |
From* |
реализация трейта
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, библиотека также содержит соответствующие трейты для платформы Windows: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");
}
}
}
Поддержка безопасного ввода-вывода для стандартных и других библиотек экосистемы
После создания кроссплатформенных абстрактных типов,ffi / async_std/ fs_err/ mio/ os_pipe/ socket2/ tokio / std
для поддержки безопасной абстракции ввода/вывода.
Случаи применения
// 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(())
}
Причины и альтернативы
Поговорка «небезопасно для безопасности памяти»
Rust исторически подвел черту, заявив, что unsafe используется только в целях безопасности памяти. Известным примером являетсяstd::mem::forget
, он был добавлен как небезопасный, а позже изменен на безопасный.
Вывод о том, что unsafe используется только для обеспечения безопасности памяти, указывает на то, что unsafe не следует использовать для других небезопасных для памяти API, например, указывает на то, что API следует избегать.
Безопасность памяти имеет приоритет над другими недостатками, потому что это не только для того, чтобы избежать неожиданного поведения, но и для того, чтобы избежать ситуаций, когда невозможно ограничить то, что может делать часть кода.
I/O
Безопасность также попадает в эту категорию по двум причинам:
-
I/O
Ошибка безопасности может вызвать ошибку безопасности памяти, вmmap
При наличии окружающих оболочек безопасности (на платформах с API-интерфейсами для ОС им разрешено быть безопасными). -
I/O安全
Ошибки также означают, что фрагмент кода может читать, записывать или удалять данные, используемые другими частями программы, без необходимости называть их или давать им ссылку. Если не в курсе всех остальных ссылок в программеcrate
детали реализации, невозможно ограничитьcrate
Коллекция вещей, которые можно сделать.
Необработанные дескрипторы очень похожи на необработанные указатели на отдельные адресные пространства; они могут быть висячими или вычисляться ложным образом.I/O
Безопасность аналогична безопасности памяти; обе они предназначены для предотвращения жутких удаленных эффектов, и в обоих случаях владение является основной основой для надежных абстракций, поэтому естественно использовать схожие концепции безопасности.