8 ms·
Write into uninit'd buffers was one of the pain points of Rust for the creator of the new open source "edit" program for Windows[1]. I wonder what he thinks of
by 90s_dev 1y ago
Write into uninit'd buffers was one of the pain points of Rust for the creator of the new open source "edit" program for Windows[1]. I wonder what he thinks of this article.
> Another thing is the difficulty of using uninitialized data in Rust. I do understand that this involves an attribute in clang which can then perform quite drastic optimizations based on it, but this makes my life as a programmer kind of difficult at times. When it comes to `MaybeUninit`, or the previous `mem::uninit()`, I feel like the complexity of compiler engineering is leaking into the programming language itself and I'd like to be shielded from that if possible. At the end of the day, what I'd love to do is declare an array in Rust, assign it no value, `read()` into it, and magically reading from said array is safe. That's roughly how it works in C, and I know that it's also UB there if you do it wrong, but one thing is different: It doesn't really ever occupy my mind as a problem. In Rust it does.
[https://news.ycombinator.com/item?id=44036021 https://news.ycombinator.com/item?id=44036021]
- electrograv 1y ago> That's roughly how it works in C, and I know that it's also UB there if you do it wrong, but one thing is different: It doesn't really ever occupy my mind as a problem. In Rust it does. UB doesn’t occupy the author’s mind when writing C, when it really should. This kind of lazy attitude to memory safety is precisely why so much C code is notoriously riddled with memory bugs and security vulnerabilities.
- mk12 1y agoThere is an important difference for this case though. It C it’s fine to have pointers into uninitialized memory as you as you don’t read them until after initializing. You can write through those pointers the same way you always do. In Rust it’s UB as soon as you “produce” an invalid value, which includes references to uninitialized memory. Everything uses references in Rust but when dealing with uninitialized memory you have to scrupulously avoid them, and instead write through raw pointers. This means you can’t reuse any code that writes through &mut. Also, the rules change over time. At one point I had unsafe code that had a Vec of uninitialized elements, which was ok because I never produced a reference to any element until after I had written them (through raw pointers). But they later changed the Vec docs to say that’s UB, I guess because they want to reserve the right to use references even if you never call a method that returns a reference.
- Arnavion 1y agoThis stopped being much of a problem when MaybeUninit was stabilized. Now you can stick to using &MaybeUninit<T> / &mut MaybeUninit<T> instead of needing to juggle *T / *mut T and carefully track converting that to &T / &mut T only when it's known to be initialized, and you can't accidentally use a MaybeUninit<T> where you meant to use a T because the types are different. It's not as painless as it could be though, because many of the MaybeUninit<T> -> T conversion fns are unstable. Eg the code in TFA needs `&mut [MaybeUninit<T>] -> &mut [T]` but `[T]::assume_init_mut()` is unstable. But reimplementing them is just a matter of copying the libstd impl, that in turn is usually just a straightforward reinterpret-cast one-liner.
- nemothekid 1y agoBizarre. I think I've been writing broken Rust code for a couple years. If I understand you correctly something like: let mut data = Vec::with_capacity(sz); unsafe { data.set_len(sz) }; buf.copy_to_slice(data.as_mut_slice()); is UB?
- vgatherps 1y agoYes, this is the case that I ran into as well. You have to zero memory before reading and/or have some crazy combination of tracking what’s uninitialized capacity or initialized len, I think the rust stdlib write trait for &mut Vec got butchered over this concern. It’s strictly more complicated and slower than the obvious thing to do and only exists to satisfy the abstract machine.
- Arnavion 1y agoNo. The correct way to write that code is to use .spare_capacity_mut() to get a &mut [MaybeUninit<T>], then write your Ts into that using .write_copy_of_slice(), then .set_len(). And that will not be any slower (though obviously more complicated) than the original incorrect code.
- vgatherps 1y ago
- o11c 1y agoThe reason this particular UB doesn't need mindspace for C programmers is because it's not even meaningful to do anything with the parts of the buffer beyond the written length. Most other UBs related to datums that you think you can do something with.
- usefulcat 1y agoI suspect that the main reason it doesn't really occupy the author's mind is that even though it's possible to misuse read(), it's really not that hard to actually use it safely. It sounds like the more difficult problem here has to do with explaining to the compiler that read() is not being used unsafely.
- lhecker 1y agoWhat I meant is that if I write a UTF8 --> UTF16 conversion function for my editor in C I can write size_t convert(state_t* state, const void* inp, void* out) This function now works with both initialized and uninitialized data in practice. It also is transparent over whether the output buffer is an `u8` (a byte buffer to write it out into a `File`) or `u16` (a buffer for then using the UTF16). I've never had to think about whether this doesn't work (in this particular context; let's ignore any alignment concerns for writes into `out` in this example) and I don't recall running into any issues writing such code in a long long time. If I write the equivalent code in Rust I may write fn convert(&mut self, inp: &[u8], out: &mut [MaybeUninit<u8>]) -> usize The problem is now obvious to me, but at least my intention is clear: "Come here! Give me your uninitialized arrays! I don't care!". But this is not the end of the problem, because writing this code is theoretically unsafe. If you have a `[u8]` slice for `out` you have to convert it to `[MaybeUninit<u8>]`, but then the function could theoretically write uninitialized data and that's UB isn't it? So now I have to think about this problem and write this instead: fn convert(&mut self, inp: &[u8], out: &mut [u8]) -> usize ...and that will also be unsafe, because now I have to convert my actual `[MaybeUninit<u8>]` buffer (for file writes) to `[u8]` for calls to this API. Long story short, this is a problem that occupies my mind when writing in Rust, but not in C. That doesn't mean that C's many unsafeties don't worry me, it just means that this _particular_ problem type described above doesn't come up as an issue in C code that I write. Edit: Also, what usefulcat said.
- ninkendo 1y agoWhy wouldn’t you accept a &mut [MaybeUninit<T>] and return a &mut [u8], hiding the unsafe bits that transmute the underlying reference? Something like: fn convert<'i, 'o>(inp: &'i [u8], buf: &'o mut MaybeUninit<u8>) -> &'o mut [u8] (Honest question, actually… because the above may be impossible to write and I’m on my phone and can’t try it.) Edit: it works: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=29cb6251fbd9626910cf4d7306394562 https://play.rust-lang.org/?version=stable&mode=debug&editio...
- lhecker 1y agoThat's a fair workaround for my specific example. But I believe it's possible to contrive a different example where such a solution would not be possible. Put differently, I only tried to convey the overall idea of what I think is a shortcoming in Rust at the moment. Edit: Also, I believe your code would fail my second section, as the `convert` function would have difficulty accepting a `[u8]` slice. Converting `[u8]` to `[MaybeUninit<u8>]` is not safe per se.
- ii41 1y agoI think this solves his problem. He said he wants a read function that turns the unsafe buffer into a safe buffer, and this API does that. IIRC it's not that hard to convince the compiler to give you a safe buffer from a MaybeUninit. However, this type has really lengthy docs and makes you question everything you do with it. Thinking through all this is painful but it's not like you don't have to it with C.
- jcranmer 1y agoThe basic problem with uninitialized buffers is that they effectively require write-only references to exist, and Rust's type system doesn't have (and doesn't easily support) write-only references, only read-only and read-write. MaybeUninit is a partial solution to the problem, but since it's a library solution and not a language solution, it suffers from a lack of integration with the language, e.g., getting MaybeUninit fields from a MaybeUninit struct is challenging. And the most aggravating part of all of this is that the most common use case for uninitialized memory (the scenario being talked about both in the article here and the discussion you quote) is actually pretty easy to have a reasonable, safe abstraction for, so the fact that the current options requires both use of unsafe code and also potentially faulty duplication of value calculations doesn't make for a fun experience. (Also, the I/O traits predate MaybeUninit, which means the most common place to want to work with uninitialized memory is one where you can't do it properly.)
- 90s_dev 1y agoThen is the solution to have write-only be the default for muts, so that they start out write-only at least, but can also be made to be read-write either (a) in given circumstances of creation, or (b) after certain operations on them, or (c) if created by certain APIs?
- lhecker 1y agoAbstracting away the `assume_init` is a great idea! I think I could use something like that for the editor. The only concern I have is that the `read` function is templated on the parameter type. I'd ideally _really_ prefer it if I didn't need two copies of the same function to switch over `[u8]` and `[MaybeUninit<u8>]` due to different return types. [^1] I guess the approach could be tuned to avoid this? Personally, I also like the simpler approach overall, compared to the `BorrowedBuf` trait, for the same reasons outlined in the article. While this possibly solves parts of pain points that I had, what I meant to write is that in an ideal world I could write Rust while mostly not thinking about this issue much, if at all. Even with this approach, I'd still need to decide whether my API needs to take a `[u8]` or a `Buffer`, just in the mere off-chance that a caller may want to pass an uninitialized array further up in the call chain. This then requires making the call path generic for the buffer parameter which may end up duplicating any of the functions along the path, even though that's not really my intention by marking it as `Buffer`. I think if there was a way to modify Rust so we can boldly state in writing "You may cast a `[MaybeUninit<T>]` into a `[T]` and pass it into a call _if_ you're absolutely certain that nothing reads from the slice", it would already go a long way. It may not make this more comfortable yet, but it would definitely take off a large part of my worries when writing such unsafe casts. That's basically what I meant with "occupy my mind": It's not that I wouldn't think about it at all, rather it just wouldn't be a larger concern for me anymore, for code where I know for sure that this requirement is fulfilled (i.e. similar to how I know it when writing equivalent C code). Edit: jcranmer's suggestion of write-only references would solve this, I think? https://news.ycombinator.com/item?id=44048450 https://news.ycombinator.com/item?id=44048450 [^1]: This is of course not a problem for a simple `read` syscall, but may be an issue for more complex functions, e.g. the UTF8 <> UTF16 converter API I suggested elsewhere in this thread, particularly if it's accelerated, the way simdutf is.