Safety problem writing structs into [u8]

I have seen both [u8] and [MaybeUninit<u8>] used for representing buffers.

I was working on a struct for representing byte streams and I figured I would use [u8] because it's easier to work with and I figured I wouldn't lose any performance because I'm intending to reuse the same buffer over and over again so the cost of zero initializing it once shouldn't be significant.

I intend to repeatedly write structs into the buffer (for streaming out to disk or sockets or shared memory or ...). I thought that as long as the buffer was zero initialized and I was only writing already constructed structs into the buffer I would be free from having to think about uninitialized memory. I have methods that expose &[u8] so user code is allowed to expect there are no uninitialized bytes.

However, I have since learned that padding bytes count as uninitialized memory, and the compiler is allowed to decide on a case by case basis whether or not to include padding bytes in a struct copy, so it's possible that writing a struct into the buffer will cause some of the bytes to become uninitialized even though they were zeroed out before!

So the first idea I had was to try to forbid structs that have padding bytes. So I could have a derive macro implementing an unsafe trait that would assert that the size of the struct is the same as the sum of the sizes of its fields, which should prove that there is no padding.

However, what if the user tries to copy a struct into the buffer that has no padding bytes but has a field that is a MaybeUninit<T>? Then I would still end up with uninitialized bytes in my buffer.

So it seems like I am forced to have a derive macro that implements an unsafe trait that asserts that every field is not an instance of MaybeUninit<T>. But Rust only lets you do type checks against specific types, AFAIK there is no way to check "Is this type any instantiation of MaybeUninit<T>?" If there were specialization I could have a method that's implemented differently for MaybeUninit<T> and static asserts, or if there were negation I could have a trait that's implemented for every type except MaybeUninit<T>. But without those features I'm assuming my only option is to explicitly whitelist a set of types by manually implementing the unsafe trait for them? Or resign myself to use [MaybeUninit<u8>].

TL;DR slowly discovering why bytemuck::Pod and bytemuck::ArbitraryBitPattern are defined the way they are

I would suggest this one, because 1. you won't be able to avoid unsafe anyway, and 2. this seems to capture the problem exactly.

(It's too bad that most of the useful array/slice methods on [MaybeUninit] are still unstable, but they can be replicated with regular pointer casting since the layout of MaybeUninit is guaranteed, and you couldn't use them in the case where you have padding, anyway.)

3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.