Введение RFC сообщества Rust | Создание безопасного ввода/вывода

задняя часть Rust

мотивация

Недавно 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Безопасность также попадает в эту категорию по двум причинам:

  1. I/OОшибка безопасности может вызвать ошибку безопасности памяти, вmmapПри наличии окружающих оболочек безопасности (на платформах с API-интерфейсами для ОС им разрешено быть безопасными).
  2. I/O安全Ошибки также означают, что фрагмент кода может читать, записывать или удалять данные, используемые другими частями программы, без необходимости называть их или давать им ссылку. Если не в курсе всех остальных ссылок в программеcrateдетали реализации, невозможно ограничитьcrateКоллекция вещей, которые можно сделать.

Необработанные дескрипторы очень похожи на необработанные указатели на отдельные адресные пространства; они могут быть висячими или вычисляться ложным образом.I/OБезопасность аналогична безопасности памяти; обе они предназначены для предотвращения жутких удаленных эффектов, и в обоих случаях владение является основной основой для надежных абстракций, поэтому естественно использовать схожие концепции безопасности.

Связанный