-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
RFC: Reading into uninitialized buffers #2930
Conversation
Co-authored-by: Taiki Endo <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Has any exploration been done to see how implementing this in the standard library would be able to replace existing similar usages such as those mentioned as prior art?
A big point of the standard library is to have reusable abstractions, so it would be nice to have some confidence that it is an abstraction that would work outside of just the usage of Read
.
Yeah, this should be applicable to AsyncRead without any real issue. |
|
||
The [`tokio::io::AsyncRead`](https://docs.rs/tokio/0.2.21/tokio/io/trait.AsyncRead.html) trait has a somewhat similar | ||
approach, with a `prepare_uninitialized_buffer` method which takes a `&mut [MaybeUninit<u8>]` slice and initializes it | ||
if necessary. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be nice to explicitly state that it's believed that this RFC could replace this usage (even if the crate itself decided not to for whatever reason).
text/0000-read-buf.md
Outdated
pub struct ReadBuf<'a> { | ||
buf: &'a mut [MaybeUninit<u8>], | ||
written: usize, | ||
initialized: usize, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the risk of opening a can of worms, this feels very similar to arrayvec. This makes me wonder a few points:
-
Could this be made generic over
u8
, so it could be used for other similar "partial initialization" cases? -
Could there be an owning variant of this so that we could do something like
let mut buf = ReadBufOwned::<const 8192>();
I'm using unstable const generics here, sadly, but I think the ergonomics are nice.
Because of #2, I'd also like to bikeshed about the name ReadBuf
a little. We have Path
and PathBuf
, where *Buf
is the owned version, so I originally wanted to type ReadBufBuf
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1. Could this be made generic over u8, so it could be used for other similar "partial initialization" cases?
It could be made generic, but I'm not sure the equivalence is really tight enough. The API of ReadBuf is centered entirely around progressive initialization, which doesn't necessarily align with the API for a general purpose vec-style data structure.
2. Could there be an owning variant of this so that we could do something like
That's covered briefly in the future possibilities section.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's covered briefly in the future possibilities section.
Are you referring to this section?
There is probably some kind of abstraction that could be defined to encapsulate that logic.
If so, perhaps that section could be extended a bit; I've re-read it a few times and still can't see a connection to an owning version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I, too, saw some avenues for possible generalizations: currently one is unable to use Vec
in no_std
crates. It's often preferable for unopinionated libraries to let the caller to decide the "backing store" of a buffer. Slices fit the bill for some cases, but in cases where there is input-dependent or otherwise unknown amount of data stored in the buffer, it's generally desirable to have a type that keeps track the "written part" and "uninitialized part", for both ergonomics and correctness (less book-keeping for the user) and performance (skipping initialization). In this sense, Vec
is pretty awesome, but it comes at the cost of an allocation and dependence on allocator.
It's also desirable to have such type as a part of the "common vocabulary" of the ecosystem, but alas, std doesn't have a type that fits the bill at the moment.
Thus, I think it would be valuable to provide a type that is generalized over the contained type, and keeps track the "written"/"uninitialized" split. We could then only allow Read
ing, but Write
ing to it, and consider it as a generic buffer type for some other uses, such as a replacement for Vec
for libraries that need to work in no_std
contexts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A Vec-like abstraction that uses a fixed-size slice of uninitialized memory as backing storage would indeed be a useful abstraction, but requires basically reimplementing the entire Vec if done outside the standard library. I've written at length about this issue here. That writeup may be a bit outdated, but I'm glad to elaborate if you believe this is relevant to this discussion.
One related thing to think about is how well this proposal would mesh with async reads where the kernel is the buffer owner, not the caller. See https://vorner.github.io/2019/11/03/io-uring-mental-experiments.html for some reasoning. |
I believe this is basically orthogonal to an io_uring interface. |
Is there an existing crate to develop |
I think it'd be valuable to specify a feature (even if for internal use only) that allows a trait to specify a list of lists of methods that constitute a minimum implementation, such that an |
@burdges I have some of the code in https://github.com/sfackler/read-buf, but it currently uses an older version of ReadBuf that doesn't track the written cursor internally. |
monomorphization or specialization. | ||
4. It needs to work with both normal and vectored IO (via `read_vectored`). | ||
5. It needs to be composable. Readers are very commonly nested (e.g. `GzipReader<TlsStream<TcpStream>>`), and wrapper | ||
readers should be able to opt-in to fast paths supported by their inner reader. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Every proposal for reading into uninitialized memory that revolved around adding a new method to the Read
trait has been criticized for potentially leaving performance on the table if a wrapper type forgets to forward the new method.
I've never seen it seriously discussed to have a warn-by-default lint for trait methods that are meant to be overridden. This could apply to Iterator::size_hint()
as well, which also leaves performance on the table if not forwarded.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd gladly take the task of implementing such a lint myself, in fact.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my opinion, "potentially leaving performance on the table" is fine - and preferable to overwhelming users new to these interfaces with all of the information they need to implement the absolute perfect interface in every case. We need to worry about information overload and ensure we gradually onboard users to exploiting the full power of these interfaces.
Any lints like this should be opt-in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So a clippy lint, then?
Regarding the unresolved question of whether to include the With the Without the Or is this difference lost in the noise because we're doing I/O? Are there cases where this might not be true? |
It might be nice if there was a safe interface for writing to the uninitialized part of a I did not read the Dropbox Paper document now (and I think I would feel slightly better if the RFC didn't rely on linking to it). Does it cover instead having a |
Seems like you want |
Co-authored-by: Bastian Kauschke <[email protected]>
Co-authored-by: Ralf Jung <[email protected]>
text/0000-read-buf.md
Outdated
/// | ||
/// The caller must ensure that `n` unwritten bytes of the buffer have already been initialized. | ||
#[inline] | ||
pub unsafe fn assume_initialized(&mut self, n: usize) { ... } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
assume_init
would be more consistent with MaybeUninit::assume_init
. But I guess at that point all "initialized" functions here should be called "init" instead. Maybe this is worth putting down as an open question to be decided later?
/// buffer that has been logically written to, a region that has been initialized at some point but not yet logically | ||
/// written to, and a region at the end that is fully uninitialized. The written-to region is guaranteed to be a | ||
/// subset of the initialized region. | ||
pub struct ReadBuf<'a> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe explicitly state that written <= initialized <= buf.len()
is an invariant of this type?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "the written-to region is guaranteed to be a subset of the initialized region" bit is trying to say that. Maybe it should be clarified?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just feel like this is worth repeating in math/Rust notation -- kind of the ultimate clarification. ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It took me a while to understand that, having the mathematical notation would make it easier to understand.
text/0000-read-buf.md
Outdated
/// | ||
/// Panics if `self.remaining()` is less than `n`. | ||
#[inline] | ||
pub fn initialize_unwritten_to(&mut self, n: usize) -> &mut [u8] { ... } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To me, the "to" here sounds like it defines the target of the operation, like "write X to Y". Also the relation with "up to index n
" is a bit weak since this is not index n
in the buffer, it is index n
in the uninitialized part of the buffer (I think).
Maybe initialize_unwritten_n
or initialize_unwritten_len
or initialize_unwritten_with_len
or so?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe a better set of names for the three methods that give access to the unwritten part would be unwritten
, unwritten_len
(both of which return &mut [u8]
), and unwritten_maybe_uninit
(returning &mut [MaybeUninit<u8>]
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do think it's somewhat important for the names of these methods to indicate that they're potentially doing significant amounts of work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, that's fair. OTOH, having the most dangerous method have the shortest name (unwritten_mut
) also doesn't seem great.
Have you considered renaming |
Ooh yeah, |
I would like to propose a different API based on a trait rather than a new type. It allows The trait is following. /// Trait to make easier to work with slices of (uninitialised) bytes.
///
/// This is implemented for common types such as `&mut[u8]` and `Vec<u8>`.
pub trait Bytes {
/// Returns itself as a slice of bytes that may or may not be initialised.
///
/// # Unsafety
///
/// Caller must ensure to only write valid bytes to the returned slice.
unsafe fn as_bytes(&mut self) -> &mut [MaybeUninit<u8>];
/// Update the length of the byte slice.
///
/// # Safety
///
/// The caller must ensure that at least `n` of the bytes returned by
/// [`Bytes::as_bytes`] are initialised.
unsafe fn update_length(&mut self, n: usize);
} It should be used in the following way:
This my implementation for impl TcpStream {
pub fn try_recv<B>(&mut self, mut buf: B) -> io::Result<usize>
where
B: Bytes,
{
let dst = buf.as_bytes();
debug_assert!(
!dst.is_empty(),
"called `TcpStream::try_recv with an empty buffer"
);
syscall!(recv(
self.socket.as_raw_fd(),
dst.as_mut_ptr().cast(),
dst.len(),
0, // Flags.
))
.map(|read| {
let read = read as usize;
// Safety: just read the bytes.
unsafe { buf.update_length(read) }
read
})
}
} Some implementations of For slices, both using impl Bytes for [MaybeUninit<u8>] {
fn as_bytes(&mut self) -> &mut [MaybeUninit<u8>] {
self
}
unsafe fn update_length(&mut self, _: usize) {
// Can't update the length of a slice.
}
}
impl Bytes for [u8] {
fn as_bytes(&mut self) -> &mut [MaybeUninit<u8>] {
// Safety: `MaybeUninit<u8>` is guaranteed to have the same layout as
// `u8` so it same to cast the pointer.
unsafe { &mut *(self as *mut [u8] as *mut [MaybeUninit<u8>]) }
}
unsafe fn update_length(&mut self, _: usize) {
// Can't update the length of a slice.
}
} This results is basically the same usage as impl Bytes for Vec<u8> {
fn as_bytes(&mut self) -> &mut [MaybeUninit<u8>] {
// Safety: `Vec` ensures the pointer is correct for us. The pointer is
// at least valid for start + `Vec::capacity` bytes, a range we stay
// within.
unsafe {
let len = self.len();
let data_ptr = self.as_mut_ptr().add(len).cast();
// NOTE: `Vec` ensure capacity >= len, so this is always >= 0.
let capacity_left = self.capacity() - len;
slice::from_raw_parts_mut(data_ptr, capacity_left)
}
}
unsafe fn update_length(&mut self, n: usize) {
// Safety: caller must ensure that `n` is valid.
self.set_len(self.len() + n);
}
} This allow the following to just work as expected. let mut stream = TcpStream::connect(address)?;
let mut buf = Vec::with_capacity(4 * 1024); // 4 KB.
let n = stream.recv(&mut buf)?;
// `buf` now contains `n` bytes with it length set to `buf.len() + n`.
// Tthe bytes are added to the `Vec`, not overwritten. |
@Thomasdezeeuw please note that your let mut buf = [0u8];
let maybe = unsafe { &mut *((&mut buf) as *mut [u8] as *mut [MaybeUninit<u8>]) };
// After this line `buf` will contain uninitialized value, which is UB
maybe[0] = MaybeUninit::uninit(); See also: rust-lang/rust#66699 |
But note, @Thomasdezeeuw, that your suggestion can be fixed the same way an actual crate with a similar API to your suggestion did: inventing (and using) an It's basically |
@WaffleLapkin, @danielhenrymantilla good points. I didn't think about that case. I could be somewhat easily fixed by making |
|
unsound unsafe code) is to fail to actually write useful data into the buffer. Code using a `BrokenReader` may see bad | ||
data in the buffer, but the bad data at least has defined contents now! | ||
|
||
Note that `read` is still a required method of the `Read` trait. It can be easily written to delegate to `read_buf`: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm personally imagining that I'll very quickly pull out this macro:
macro_rules! default_read_impl {
() => {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let mut buf = std::io::ReadBuf::new(buf);
std::io::Read::read_buf(self, &mut buf)?;
std::result::Result::Ok(buf.filled().len())
}
}
}
impl Read for SomeReader {
default_read_impl!();
fn read_buf(&mut self, buf: &mut ReadBuf<'_>) -> io::Result<()> {
...
}
}
I'm not sure whether that would be worth including in std
. My gut feeling is "no", but that it's going to be a very mild annoyance to copy-paste into every new crate that implements lots of Read
types.
maybe |
That's described in the Future Possibilities section: https://github.com/rust-lang/rfcs/pull/2930/files#diff-ae79fbabbd969ac5cf26c1ec56a7e690310543267ad0c240f20c6f642dbb4eaaR653-R655 |
/// # Safety | ||
/// | ||
/// The caller must not de-initialize portions of the buffer that have already been initialized. | ||
#[inline] | ||
pub unsafe fn unfilled_mut(&mut self) -> &mut [MaybeUninit<u8>] { ... } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to propose that the safety section is expanded to the following:
The caller must not de-initialize portions of the buffer that have already been initialized.
This includes any bytes in the region marked as uninitialized byReadBuf
.
Without a comment like this, it is unclear whether it is sound to put an UninitSlice
from the bytes crate into an ReadBuf
without first initializing the memory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or simply change that to return an Out<[u8]>
to get rid of the unsafe
altogether: even though I like the ReadBuf
abstraction since it tracks the number of initialized elements (which an Out<[u8]>
does not), I still think that an out
-references abstraction is both intuitive and less error-prone than &mut MaybeUninit<...>
s, and something that is very tied to "writing into potentially-uninit stuff", so the &mut MU
part of the ReadBuf
API ought to be replaced with that. Being able to, for instance, feature a non-unsafe
version of this method is just one example of that (and then, if people were to need exactly an &mut [MU<u8>]
, they could call the unsafe
.as_uninit_mut()
method, who has a well-detailed section about how and why writing uninit bytes to the pointee is unsound).
Huzzah! The @rust-lang/libs team decided in September to accept this RFC. I have merged it into the repository. The tracking issue is rust-lang/rust#78485, if you would like to follow along from there. |
Isn't the example in the |
Well, that's the point of that section: it shows how uninit values are not "just random bytes", but that they are treated specially by the compiler. It's a "how bad can uninit-values-based-UB be"? Granted, most people already know that UB is just UB, that there is no more need to dig deeper, but examples of bugs such as the one in that section can show this is not just some theoretical concern, it is a real issue present in current Rust.
Technically not yet, precisely due to the lack of an API that would allow it:
The other option would have been to always require an initialized buffer to perform reads to (i.e., keep the current status quo as is), but this can have a non-negligible performance impact in some performance-sensitive code, which would mean that Rust wouldn't live to the zero-cost abstraction ideal anymore, despite OS primitives that allow reading into uninit buffers and languages in which it can be done (e.g., C, C++). That's where the current code "fails". So this whole RFC is suggesting a way to:
The missing piece of this API, which has been disregarded despite my remarks, is that there is still an |
The problem is that Unsure what you mean "perform checks. AFAIK, there is no check to know whether memory was initialized, but it doesn't matter,
The example does illustrate this.
That's exactly what |
No of course not. If you don't use unsafe, you cannot trigger undefined behavior. If you don't want to be using unsafe blocks, the price is instead being forced to pre-initialize something that gets immediately overwritten afterwards anyway.
Sure, that would be a way to write correct unsafe code that writes to uninitialized memory. The purpose of
The second point is important because the
For Tokio specifically, if you are using the This method also exists for non-poll methods in the form of Edit: Did we all wake up at the same time? |
impl Read for TcpStream { | ||
fn read_buf(&mut self, buf: &mut ReadBuf<'_>) -> io::Result<()> { | ||
unsafe { | ||
// Get access to the filled part of the buffer, without initializing it. This method is unsafe; we are |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sfackler , It seems you meant "unfilled"
Rendered