Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add more explicit I/O safety documentation #114780

Merged
merged 11 commits into from
Sep 22, 2023
45 changes: 44 additions & 1 deletion library/std/src/io/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//! the [`Read`] and [`Write`] traits, which provide the
//! most general interface for reading and writing input and output.
//!
//! # Read and Write
//! ## Read and Write
//!
//! Because they are traits, [`Read`] and [`Write`] are implemented by a number
//! of other types, and you can implement them for your types too. As such,
Expand Down Expand Up @@ -238,13 +238,56 @@
//! contract. The implementation of many of these functions are subject to change over
//! time and may call fewer or more syscalls/library functions.
//!
//! ## I/O Safety
//!
//! Rust follows an I/O safety discipline that is comparable to its memory safety discipline. This
//! means that file descriptors can be *exclusively owned*. (Here, "file descriptor" is meant to
//! subsume similar concepts that exist across a wide range of operating systems even if they might
//! use a different name, such as "handle".) An exclusively owned file descriptor is one that no
//! other code is allowed to access in any way, but the owner is allowed to a access and even close
RalfJung marked this conversation as resolved.
Show resolved Hide resolved
//! it any time. A type that owns its file descriptor should usually close it in its `drop`
//! function. Types like [`File`] generally own their file descriptor. Similarly, file descriptors
RalfJung marked this conversation as resolved.
Show resolved Hide resolved
//! can be *borrowed*, granting the temporary right to perform operations on this file descriptor.
//! This indicates that the file descriptor will not be closed for the lifetime of the borrow, but
//! it does *not* imply any right to close this file descriptor, since it will likely be owned by
//! someone else.
//!
//! The platform-specific parts of the Rust standard library expose types that reflect these
//! concepts, see [`os::unix`] and [`os::windows`].
//!
//! To uphold I/O safety, it is crucial that no code acts on file descriptors it does not own. In
RalfJung marked this conversation as resolved.
Show resolved Hide resolved
//! other words, a safe function that takes a regular integer, treats it as a file descriptor, and
//! acts on it, is *unsound*.
//!
//! Not upholding I/O safety and acting on a file descriptor without proof of ownership can lead to
//! misbehavior and even Undefined Behavior in code that relies on ownership of its file
//! descriptors: a closed file descriptor could be re-allocated, so the original owner of that file
//! descriptor is now working on the wrong file. Some code might even rely on fully encapsulating
//! its file descriptors with no operations being performed by any other part of the program.
//!
//! Note that exclusive ownership of a file descriptor does *not* imply exclusive ownership of the
//! underlying kernel object that the file descriptor references (also called "file description" on
//! some operating systems). File descriptors basically work like [`Arc`]: when you receive an owned
//! file descriptor, you cannot know whether there are any other file descriptors that reference the
//! same kernel object. However, when you create a new kernel object, you know that you are holding
//! the only reference to it. Just be careful not to borrow it to anyone, since they can obtain a
RalfJung marked this conversation as resolved.
Show resolved Hide resolved
//! clone and then you can no longer know what the reference count is! In that sense, [`OwnedFd`] is
//! like `Arc` and [`BorrowedFd<'a>`] is like `&'a Arc` (and similar for the Windows types). There
//! is no equivalent to `Box` for file descriptors in the standard library (that would be a type
//! that guarantees that the reference count is `1`).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding to the Arc analogy, a sentence could be added to justify why it is unsound to use dup2 to replace a BorrowedFd with a different file descriptor.

Copy link
Member Author

@RalfJung RalfJung Aug 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Talking about dup2 here is hard since that is so platform-specific. But really the bad thing dup2 does is close the other FD, so I think this would be an example explaining why you cannot close a BorrowedFd. I could add that (basically: given an &Arc, you also don't get to decrement the refcount).

//!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: can we also list some of the risks of violating IO safety?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ideally, exhaustively if possible

roughly: if you are accessing an fd after it has been closed, it might have been reallocated to belong to someone else, which might be the allocator, or memory mapping libraries, or something else.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a paragraph. However, without full file description encapsulation, I am not sure if there even is an example where this can truly lead to UB?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
//!
//!
//! Note that functions like `pidfd_getfd` or debugging interfaces are out of scope.
//!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably say a bit about what that means, too, similar to https://doc.rust-lang.org/nightly/std/os/unix/io/index.html#procselfmem-and-similar-os-features? Basically we are saying Rust programs must not abuse these APIs, so "out of scope" is not a free pass to use them to circumvent everything. They can be used for debugging/tracing only.

//! [`File`]: crate::fs::File
//! [`TcpStream`]: crate::net::TcpStream
//! [`io::stdout`]: stdout
//! [`io::Result`]: self::Result
//! [`?` operator]: ../../book/appendix-02-operators.html
//! [`Result`]: crate::result::Result
//! [`.unwrap()`]: crate::result::Result::unwrap
//! [`os::unix`]: ../os/unix/io/index.html
//! [`os::windows`]: ../os/windows/io/index.html
//! [`OwnedFd`]: ../os/fd/struct.OwnedFd.html
//! [`BorrowedFd<'a>`]: ../os/fd/struct.BorrowedFd.html
//! [`Arc`]: crate::sync::Arc

#![stable(feature = "rust1", since = "1.0.0")]

Expand Down
12 changes: 8 additions & 4 deletions library/std/src/os/fd/owned.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ use crate::sys_common::{AsInner, FromInner, IntoInner};

/// A borrowed file descriptor.
///
/// This has a lifetime parameter to tie it to the lifetime of something that
/// owns the file descriptor.
/// This has a lifetime parameter to tie it to the lifetime of something that owns the file
/// descriptor. For the duration of that lifetime, it is guaranteed that nobody will close the file
/// descriptor.
///
/// This uses `repr(transparent)` and has the representation of a host file
/// descriptor, so it can be used in FFI in places where a file descriptor is
Expand All @@ -42,7 +43,8 @@ pub struct BorrowedFd<'fd> {

/// An owned file descriptor.
///
/// This closes the file descriptor on drop.
/// This closes the file descriptor on drop. It is guaranteed that nobody else will close the file
/// descriptor.
///
/// This uses `repr(transparent)` and has the representation of a host file
/// descriptor, so it can be used in FFI in places where a file descriptor is
Expand Down Expand Up @@ -155,7 +157,9 @@ impl FromRawFd for OwnedFd {
/// # Safety
///
/// The resource pointed to by `fd` must be open and suitable for assuming
/// ownership. The resource must not require any cleanup other than `close`.
/// [ownership][io-safety]. The resource must not require any cleanup other than `close`.
///
/// [io-safety]: io#io-safety
#[inline]
unsafe fn from_raw_fd(fd: RawFd) -> Self {
assert_ne!(fd, u32::MAX as RawFd);
Expand Down
5 changes: 4 additions & 1 deletion library/std/src/os/fd/raw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@ pub trait FromRawFd {
///
/// # Safety
///
/// The `fd` passed in must be a valid and open file descriptor.
/// The `fd` passed in must be an [owned file descriptor][io-safety];
/// in particular, it must be open.
///
/// [io-safety]: io#io-safety
///
/// # Example
///
Expand Down
23 changes: 15 additions & 8 deletions library/std/src/os/fortanix_sgx/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,22 @@ pub trait FromRawFd {
/// Constructs a new instance of `Self` from the given raw file
/// descriptor and metadata.
///
/// This function **consumes ownership** of the specified file
/// descriptor. The returned object will take responsibility for closing
/// it when the object goes out of scope.
/// This function is typically used to **consume ownership** of the
/// specified file descriptor. When used in this way, the returned object
/// will take responsibility for closing it when the object goes out of
/// scope.
///
/// This function is also unsafe as the primitives currently returned
/// have the contract that they are the sole owner of the file
/// descriptor they are wrapping. Usage of this function could
/// accidentally allow violating this contract which can cause memory
/// unsafety in code that relies on it being true.
/// However, consuming ownership is not strictly required. Use a
/// [`From<OwnedFd>::from`] implementation for an API which strictly
/// consumes ownership.
///
/// # Safety
///
/// The `fd` passed in must be an [owned file descriptor][io-safety];
/// in particular, it must be open.
// FIXME: say something about `metadata`.
///
/// [io-safety]: io#io-safety
#[unstable(feature = "sgx_platform", issue = "56975")]
unsafe fn from_raw_fd(fd: RawFd, metadata: Self::Metadata) -> Self;
}
Expand Down
22 changes: 14 additions & 8 deletions library/std/src/os/solid/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,21 @@ pub trait FromRawFd {
/// Constructs a new instance of `Self` from the given raw file
/// descriptor.
///
/// This function **consumes ownership** of the specified file
/// descriptor. The returned object will take responsibility for closing
/// it when the object goes out of scope.
/// This function is typically used to **consume ownership** of the
/// specified file descriptor. When used in this way, the returned object
/// will take responsibility for closing it when the object goes out of
/// scope.
///
/// This function is also unsafe as the primitives currently returned
/// have the contract that they are the sole owner of the file
/// descriptor they are wrapping. Usage of this function could
/// accidentally allow violating this contract which can cause memory
/// unsafety in code that relies on it being true.
/// However, consuming ownership is not strictly required. Use a
/// [`From<OwnedFd>::from`] implementation for an API which strictly
/// consumes ownership.
///
/// # Safety
///
/// The `fd` passed in must be an [owned file descriptor][io-safety];
/// in particular, it must be open.
///
/// [io-safety]: io#io-safety
unsafe fn from_raw_fd(fd: RawFd) -> Self;
}

Expand Down
9 changes: 5 additions & 4 deletions library/std/src/os/unix/io/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//!
//! This module provides three types for representing file descriptors,
//! with different ownership properties: raw, borrowed, and owned, which are
//! analogous to types used for representing pointers:
//! analogous to types used for representing pointers. These types reflect the Unix version of [I/O safety].
//!
//! | Type | Analogous to |
//! | ------------------ | ------------ |
Expand Down Expand Up @@ -65,15 +65,16 @@
//! to be opened and read from or written must be `unsafe`. Rust's safety guarantees
//! only cover what the program itself can do, and not what entities outside
//! the program can do to it. `/proc/self/mem` is considered to be such an
//! external entity, along with debugging interfaces, and people with physical access to
//! the hardware. This is true even in cases where the program is controlling
//! the external entity.
//! external entity, along with `/proc/self/fd/*`, debugging interfaces, and people with physical
//! access to the hardware. This is true even in cases where the program is controlling the external
//! entity.
//!
//! If you desire to comprehensively prevent programs from reaching out and
//! causing external entities to reach back in and violate memory safety, it's
//! necessary to use *sandboxing*, which is outside the scope of `std`.
//!
//! [`BorrowedFd<'a>`]: crate::os::unix::io::BorrowedFd
//! [I/O safety]: crate::io#io-safety
#![stable(feature = "rust1", since = "1.0.0")]

Expand Down
3 changes: 2 additions & 1 deletion library/std/src/os/windows/io/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//!
//! This module provides three types for representing raw handles and sockets
//! with different ownership properties: raw, borrowed, and owned, which are
//! analogous to types used for representing pointers:
//! analogous to types used for representing pointers. These types reflect the Windows version of [I/O safety].
//!
//! | Type | Analogous to |
//! | ---------------------- | ------------ |
Expand Down Expand Up @@ -47,6 +47,7 @@
//!
//! [`BorrowedHandle<'a>`]: crate::os::windows::io::BorrowedHandle
//! [`BorrowedSocket<'a>`]: crate::os::windows::io::BorrowedSocket
//! [I/O safety]: crate::io#io-safety

#![stable(feature = "rust1", since = "1.0.0")]

Expand Down
6 changes: 4 additions & 2 deletions library/std/src/os/windows/io/raw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ pub trait FromRawHandle {
/// # Safety
///
/// The `handle` passed in must:
/// - be a valid an open handle,
/// - be an [owned handle][io-safety]; in particular, it must be open.
/// - be a handle for a resource that may be freed via [`CloseHandle`]
/// (as opposed to `RegCloseKey` or other close functions).
///
Expand All @@ -71,6 +71,7 @@ pub trait FromRawHandle {
///
/// [`CloseHandle`]: https://docs.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-closehandle
/// [here]: https://devblogs.microsoft.com/oldnewthing/20040302-00/?p=40443
/// [io-safety]: io#io-safety
#[stable(feature = "from_raw_os", since = "1.1.0")]
unsafe fn from_raw_handle(handle: RawHandle) -> Self;
}
Expand Down Expand Up @@ -207,10 +208,11 @@ pub trait FromRawSocket {
/// # Safety
///
/// The `socket` passed in must:
/// - be a valid an open socket,
/// - be an [owned socket][io-safety]; in particular, it must be open.
/// - be a socket that may be freed via [`closesocket`].
///
/// [`closesocket`]: https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-closesocket
/// [io-safety]: io#io-safety
#[stable(feature = "from_raw_os", since = "1.1.0")]
unsafe fn from_raw_socket(sock: RawSocket) -> Self;
}
Expand Down