Skip to content

Commit ac872ec

Browse files
committed
Remove IoSafe, add OwnedFd, BorrowedFd, and friends.
1 parent 2e82b83 commit ac872ec

File tree

1 file changed

+96
-162
lines changed

1 file changed

+96
-162
lines changed

text/0000-io-safety.md

Lines changed: 96 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88

99
Close a hole in encapsulation boundaries in Rust by providing users of
1010
`AsRawFd` and related traits guarantees about their raw resource handles, by
11-
introducing a concept of *I/O safety* and a new `IoSafe` trait. Build on, and
12-
provide an explanation for, the `from_raw_fd` function being unsafe.
11+
introducing a concept of *I/O safety* and a new set of types and traits.
1312

1413
# Motivation
1514
[motivation]: #motivation
@@ -50,7 +49,7 @@ This RFC introduces a path to gradually closing this loophole by introducing:
5049

5150
- A new concept, I/O safety, to be documented in the standard library
5251
documentation.
53-
- A new trait, `std::io::IoSafe`.
52+
- A new set of types and traits.
5453
- New documentation for
5554
[`from_raw_fd`]/[`from_raw_handle`]/[`from_raw_socket`] explaining why
5655
they're unsafe in terms of I/O safety, addressing a question that has
@@ -129,106 +128,71 @@ lifetime the OS associates with the handle.
129128

130129
I/O safety is new as an explicit concept, but it reflects common practices.
131130
Rust's `std` will require no changes to stable interfaces, beyond the
132-
introduction of a new trait and new impls for it. Initially, not all of the
133-
Rust ecosystem will support I/O safety though; adoption will be gradual.
131+
introduction of some new types and traits and new impls for them. Initially,
132+
not all of the Rust ecosystem will support I/O safety though; adoption will
133+
be gradual.
134134

135-
## The `IoSafe` trait
135+
## `OwnedFd` and `BorrowedFd<'owned>`
136136

137-
These high-level types also implement the traits [`AsRawFd`]/[`IntoRawFd`] on
138-
Unix-like platforms and
139-
[`AsRawHandle`]/[`AsRawSocket`]/[`IntoRawHandle`]/[`IntoRawSocket`] on Windows,
140-
providing ways to obtain the low-level value contained in a high-level value.
141-
APIs use these to accept any type containing a raw handle, such as in the
142-
`do_some_io` example in the [motivation].
137+
These two types are conceptual replacements for `RawFd`, and represent owned
138+
and borrowed handle values. `OwnedFd` owns a file descriptor, including closing
139+
it when it's dropped. `BorrowedFd`'s lifetime parameter ties it to the lifetime
140+
of something that owns a file descriptor. These types enforce all of their I/O
141+
safety invariants automatically.
143142

144-
`AsRaw*` and `IntoRaw*` don't make any guarantees, so to add I/O safety, types
145-
will implement a new trait, `IoSafe`:
143+
For Windows, similar types, but in `Handle` and `Socket` forms.
146144

147-
```rust
148-
pub unsafe trait IoSafe {}
149-
```
150-
151-
There are no required functions, so implementing it just takes one line, plus
152-
comments:
145+
## `AsFd`, `IntoFd`, and `FromFd`
153146

154-
```rust
155-
/// # Safety
156-
///
157-
/// `MyType` wraps a `std::fs::File` which handles the low-level details, and
158-
/// doesn't have a way to reassign or independently drop it.
159-
unsafe impl IoSafe for MyType {}
160-
```
147+
These three traits are conceptual replacements for `AsRawFd`, `IntoRawFd`, and
148+
`FromRawFd` for most use cases. They work in terms of `OwnedFd` and
149+
`BorrowedFd`, so they automatically enforce their I/O safety invariants.
161150

162-
It requires `unsafe`, to require the code to explicitly commit to upholding I/O
163-
safety. With `IoSafe`, the `do_some_io` example should simply add a
164-
`+ IoSafe` to provide I/O safety:
151+
Using these traits, the `do_some_io` example in the [motivation] can avoid
152+
the original problems. Since `AsFd` is only implemented for types which
153+
properly own their file descriptors, this version of `do_some_io` doesn't
154+
have to worry about being passed bogus or dangling file descriptors:
165155

166156
```rust
167-
pub fn do_some_io<FD: AsRawFd + IoSafe>(input: &FD) -> io::Result<()> {
168-
some_syscall(input.as_raw_fd())
157+
pub fn do_some_io<FD: AsFd>(input: &FD) -> io::Result<()> {
158+
some_syscall(input.as_fd())
169159
}
170160
```
171161

172-
Some types have the ability to dynamically drop their resources, and
173-
these types require special consideration when implementing `IoSafe`. For
174-
example, a class representing a dynamically reassignable output source might
175-
have code like this:
176-
177-
```rust
178-
struct VirtualStdout {
179-
current: RefCell<std::fs::File>
180-
}
181-
182-
impl VirtualStdout {
183-
/// Assign a new output destination.
184-
///
185-
/// This function ends the lifetime of the resource that `as_raw_fd`
186-
/// returns a handle to.
187-
pub fn set_output(&self, new: std::fs::File) {
188-
*self.current.borrow_mut() = new;
189-
}
190-
}
162+
For Windows, similar traits, but in `Handle` and `Socket` forms.
191163

192-
impl AsRawFd for VirtualStdout {
193-
fn as_raw_fd(&self) -> RawFd {
194-
self.current.borrow().as_raw_fd()
195-
}
196-
}
197-
```
164+
## Portability for simple use cases
198165

199-
If a user of this type were to hold a `RawFd` value over a call to `set_file`,
200-
the `RawFd` value would become dangling, even though its within the lifetime of
201-
the `&self` reference passed to `as_raw_fd`:
166+
Portability in this space isn't easy, since Windows has two different handle
167+
types while Unix has one. However, some use cases can treat `AsFd` and
168+
`AsHandle` similarly, while some other uses can treat `AsFd` and `AsSocket`
169+
similarly. In these two cases, trivial `Filelike` and `Socketlike` abstractions
170+
allow code which works in this way to be generic over Unix and Windows.
202171

203-
```rust
204-
fn foo(output: &VirtualStdout) -> io::Result<()> {
205-
let raw_fd = output.as_raw_fd();
206-
output.set_file(File::open("/some/other/file")?);
207-
use(raw_fd)?; // Use of dangling file descriptor!
208-
Ok(())
209-
}
210-
```
172+
On Unix, `AsFilelike` and `AsSocketlike` have blanket implementations for
173+
any type that implements `AsFd`. On Windows, `AsFilelike` has a blanket
174+
implementation for any type that implements `AsHandle`, and `AsSocketlike`
175+
has a blanket implementation for any type that implements `AsSocket`.
211176

212-
The `IoSafe` trait requires types capable of dynamically dropping their
213-
resources within the lifetime of the `&self` passed to `as_raw_fd` must
214-
document the conditions under which this can occur, as the documentation
215-
comment above does.
177+
Similar portability abstractions apply to the `From*` and `Into*` traits.
216178

217179
## Gradual adoption
218180

219-
I/O safety and `IoSafe` wouldn't need to be adopted immediately, adoption
220-
could be gradual:
181+
I/O safety and the new types and traits wouldn't need to be adopted
182+
immediately; adoption could be gradual:
221183

222-
- First, `std` adds `IoSafe` with impls for all the relevant `std` types.
223-
This is a backwards-compatible change.
184+
- First, `std` adds the new types and traits with impls for all the relevant
185+
`std` types. This is a backwards-compatible change.
224186

225-
- After that, crates could implement `IoSafe` for their own types. These
226-
changes would be small and semver-compatible, without special coordination.
187+
- After that, crates could begin to use the new types and implement the new
188+
traits for their own types. These changes would be small and semver-compatible,
189+
without special coordination.
227190

228-
- Once the standard library and enough popular crates utilize `IoSafe`,
229-
crates could start to add `+ IoSafe` bounds (or adding `unsafe`), at their
230-
own pace. These would be semver-incompatible changes, though most users of
231-
APIs adding `+ IoSafe` wouldn't need any changes.
191+
- Once the standard library and enough popular crates implement the new
192+
traits, crates could start to switch to using the new traits as bounds when
193+
accepting generic arguments, at their own pace. These would be
194+
semver-incompatible changes, though most users of APIs switching to these
195+
new traits wouldn't need any changes.
232196

233197
# Reference-level explanation
234198
[reference-level-explanation]: #reference-level-explanation
@@ -250,44 +214,52 @@ Functions accepting arbitrary raw I/O handle values ([`RawFd`], [`RawHandle`],
250214
or [`RawSocket`]) should be `unsafe` if they can lead to any I/O being
251215
performed on those handles through safe APIs.
252216

253-
Functions accepting types implementing
254-
[`AsRawFd`]/[`IntoRawFd`]/[`AsRawHandle`]/[`AsRawSocket`]/[`IntoRawHandle`]/[`IntoRawSocket`]
255-
should add a `+ IoSafe` bound if they do I/O with the returned raw handle.
217+
## `OwnedFd` and `BorrowedFd<'owned>`
218+
219+
`OwnedFd` and `BorrowedFd` are both `repr(transparent)` with a `RawFd` value
220+
on the inside, and both can use niche optimizations so that `Option<OwnedFd>`
221+
and `Option<BorrowedFd<'_>>` are the same size, and can be used in FFI
222+
declarations for functions like `open`, `read`, `write`, `close`, and so on.
223+
When used this way, they ensure I/O safety all the way out to the FFI boundary.
224+
225+
These types also implement the existing `AsRawFd`, `IntoRawFd`, and `FromRawFd`
226+
traits, so they can interoperate with existing code that works with `RawFd`
227+
types.
228+
229+
## `AsFd`, `IntoFd`, and `FromFd`
256230

257-
## The `IoSafe` trait
231+
These types provide `as_fd`, `into_fd`, and `from_fd` functions similar to
232+
their `Raw` counterparts, but with the benefit of a safe interface, it's safe
233+
to provide a few simple conveniences which make the API much more flexible:
258234

259-
Types implementing `IoSafe` guarantee that they uphold I/O safety. They must
260-
not make it possible to write a safe function which can perform invalid I/O
261-
operations, and:
235+
- A `from_into_fd` function which takes a `IntoFd` and converts it into a
236+
`FromFd`, allowing users to perform this common sequence in a single
237+
step, and without having to use `unsafe`.
262238

263-
- A type implementing `AsRaw* + IoSafe` means its `as_raw_*` function returns
264-
a handle which is valid to use for the duration of the `&self` reference.
265-
If such types have methods to close or reassign the handle without
266-
dropping the whole object, they must document the conditions under which
267-
existing raw handle values remain valid to use.
239+
- A `as_filelike_view::<T>()` function returns a `View`, which contains a
240+
temporary `ManuallyDrop` instance of T constructed from the contained
241+
file descriptor, allowing users to "view" a raw file descriptor as a
242+
`File`, `TcpStream`, and so on.
268243

269-
- A type implementing `IntoRaw* + IoSafe` means its `into_raw_*` function
270-
returns a handle which is valid to use at the point of the return from
271-
the call.
244+
## Prototype implementation
272245

273-
All standard library types implementing `AsRawFd` implement `IoSafe`, except
274-
`RawFd`.
246+
All of the above is prototyped here:
275247

276-
Note that, despite the naming similarity, the `IoSafe` trait's requirements are not
277-
identical to the I/O safety requirements. The return value of `as_raw_*` is
278-
valid only for the duration of the `&self` argument passed in.
248+
<https://github.com/sunfishcode/io-experiment>
249+
250+
The README.md has links to documentation, examples, and a survey of existing
251+
crates providing similar features.
279252

280253
# Drawbacks
281254
[drawbacks]: #drawbacks
282255

283256
Crates with APIs that use file descriptors, such as [`nix`] and [`mio`], would
284-
need to migrate to types implementing `AsRawFd + IoSafe`, use crates providing
285-
equivalent mechanisms such as [`unsafe-io`], or change such functions to be
257+
need to migrate to types implementing `AsFd`, or change such functions to be
286258
unsafe.
287259

288260
Crates using `AsRawFd` or `IntoRawFd` to accept "any file-like type" or "any
289261
socket-like type", such as [`socket2`]'s [`SockRef::from`], would need to
290-
either add a `+ IoSafe` bound or make these functions unsafe.
262+
either switch to `AsFd` or `IntoFd`, or make these functions unsafe.
291263

292264
# Rationale and alternatives
293265
[rationale-and-alternatives]: #rationale-and-alternatives
@@ -347,52 +319,19 @@ I/O safety approach will require changes to Rust code in crates such as
347319
[`RawFd`], though the changes can be made gradually across the ecosystem rather
348320
than all at once.
349321

350-
## I/O safety but not `IoSafe`
351-
352-
The I/O safety concept doesn't depend on `IoSafe` being in `std`. Crates could
353-
continue to use [`unsafe_io::OwnsRaw`], though that does involve adding a
354-
dependency.
355-
356-
## Define `IoSafe` in terms of the object, not the reference
357-
358-
The [reference-level-explanation] explains `IoSafe + AsRawFd` as returning a
359-
handle valid to use for "the duration of the `&self` reference". This makes it
360-
similar to borrowing a reference to the handle, though it still uses a raw
361-
type which doesn't enforce the borrowing rules.
362-
363-
An alternative would be to define it in terms of the underlying object. Since
364-
it returns raw types, arguably it would be better to make it work more like
365-
`slice::as_ptr` and other functions which return raw pointers that aren't
366-
connected to reference lifetimes. If the concept of borrowing is desired, new
367-
types could be introduced, with better ergonomics, in a separate proposal.
368-
369-
## New types and traits
370-
371-
New types and traits could provide a much cleaner API, along the lines of:
372-
373-
```rust
374-
pub struct BorrowedFd<'owned> { ... }
375-
pub struct OwnedFd { ... }
376-
377-
pub trait AsFd { ... }
378-
pub trait IntoFd { ... }
379-
pub trait FromFd { ... }
380-
```
322+
## The `IoSafe` trait (and `OwnsRaw` before it)
381323

382-
An initial prototype of this here:
383-
384-
<https://github.com/sunfishcode/io-experiment>
324+
Earlier versions of this RFC proposed an `IoSafe` trait, which was meant as a
325+
minimally intrusive fix. Feedback from the RFC process led to the development
326+
of a new set of types and traits. This has a much larger API surface area,
327+
which will take more work to design and review. And it and will require more
328+
extensive changes in the crates ecosystem over time. However, early indications
329+
are that the new types and traits are easier to understand, and easier and
330+
safer to use, and so are a better foundation for the long term.
385331

386-
The details are mostly obvious, though one notable aspect of this design is
387-
the use of `repr(transparent)` to define types that can participate in FFI
388-
directly, leading to FFI usage patterns that don't interact with raw types
389-
at all. An example of this is here:
390-
391-
<https://github.com/sunfishcode/io-experiment/blob/main/examples/hello.rs>
392-
393-
This provides a cleaner API than `*Raw*` + `IoSafe`. The main obvious downside
394-
is that a lot of code will likely need to continue to support `*Raw*` for a
395-
long time, so this would increase the amount of code they have to maintain.
332+
Earlier versions of `IoSafe` were called `OwnsRaw`. It was difficult to find a
333+
name for this trait which described exactly what it does, and arguably this is
334+
one of the signs that it wasn't the right trait.
396335

397336
# Prior art
398337
[prior-art]: #prior-art
@@ -403,9 +342,15 @@ such as in [C#], [Java], and others. Making it `unsafe` to perform I/O through
403342
a given raw handle would let safe Rust have the same guarantees as those
404343
effectively provided by such languages.
405344

406-
The `std::io::IoSafe` trait comes from [`unsafe_io::OwnsRaw`], and experience
407-
with this trait, including in some production use cases, has shaped this RFC.
345+
There are several crates on crates.io providing owning and borrowing file
346+
descriptor wrappers. The [io-experiment README.md's Prior Art section]
347+
describes these and details how io-experiment's similarities and differences
348+
with these existing crates in detail. At a high level, these existing crates
349+
share the same basic concepts that io-experiment uses. All are built around
350+
Rust's lifetime and ownership concepts, and confirm that these concepts
351+
are a good fit for this problem.
408352

353+
[io-experiment README.md's Prior Art section]: https://github.com/sunfishcode/io-experiment#prior-art
409354
[C#]: https://docs.microsoft.com/en-us/dotnet/api/system.io.file?view=net-5.0
410355
[Java]: https://docs.oracle.com/javase/7/docs/api/java/io/File.html?is-external=true
411356

@@ -432,17 +377,6 @@ needs, but it could be explored in the future.
432377

433378
Some possible future ideas that could build on this RFC include:
434379

435-
- New wrapper types around `RawFd`/`RawHandle`/`RawSocket`, to improve the
436-
ergonomics of some common use cases. Such types may also provide portability
437-
features as well, abstracting over some of the `Fd`/`Handle`/`Socket`
438-
differences between platforms.
439-
440-
- Higher-level abstractions built on `IoSafe`. Features like
441-
[`from_filelike`] and others in [`unsafe-io`] eliminate the need for
442-
`unsafe` in user code in some common use cases. [`posish`] uses this to
443-
provide safe interfaces for POSIX-like functionality without having `unsafe`
444-
in user code, such as in [this wrapper around `posix_fadvise`].
445-
446380
- Clippy lints warning about common I/O-unsafe patterns.
447381

448382
- A formal model of ownership for raw handles. One could even imagine

0 commit comments

Comments
 (0)