Discussion of "Uninitialized Memory: Unsafe Rust is Too Hard"

I just came across this article. It seems quite wrong to me, but I wanted to verify that's the case.

Specifically, everything starting with this quote seems wrong to me:

We're in fact having exactly this happen in our code. For instance (*role).name creates a &mut str behind the scenes which is illegal, even if we can't observe it because the memory where it points to is not initialized.

I expected the second unsafe example to work just fine. Running Miri on the playground seems to agree. Is that true?

The author seems to believe we can't expect repr(Rust) to provide aligned fields. Is that correct?

1 Like

AFAIK, indeed assigning to (the field of) a dereferenced pointer is only problematic e.g. if there’s some deref coercion going on before you get to the pointer (which would involve &mut) or when the type of the field implements Drop (or has any other kind of “drop glue”) (in which case the assignment would try to drop the previous value, which involves passing a mutable reference to some Drop::drop function).

So the code in this playground seems sound to me.


Skimming through the rest of the article, there’s more IMO incorrect assertions, e.g. I do agree your sentiment to question this point:


If any of the fields does implement Drop (or any other kind of “drop glue”), you’ll need to switch to using a pointer’s write method, and creating the pointer to the fields indeed requires addr_of_mut, so the article is not totally off. (The thoughts about alignment also comes with the statement “I'm pretty sure we can depend on things being aligned […] but let's be better safe than sorry”, so I don’t think the author really claims that the additional step of using write_unaligned is necessary.)

3 Likes

I wondered about that, too; specifically,

seems to be based on the assumption that some DerefMut action is going on, which is not true of raw pointers since they don't implement Deref. It might be a problem if role were an Arc or something but then the problem would probably be better localized to wherever the Arc was created. (Box is special, since it implements Deref, but the compiler also knows how to dereference it directly, and it's possible to create partially initialized Boxes in safe code, so that example would be rather ambiguous.)

As far as I know, the language semantics do not require that assigning to the dereference of a raw pointer, even field-at-a-time, must create a reference to the whole thing.

Note that (*role).name = ... can cause other problems if name has drop glue, because the prior value will be dropped in place implicitly by =. In such a case one must use write anyway.

3 Likes

I don't think so.

In the absence of #[repr(packed)] it is perfectly valid to write &role.disabled, and Rust references must always be aligned. Therefore the compiler guarantees repr(Rust) types always have aligned fields.

3 Likes

In fairness to the article, I think there are several takeaways that should not be dismissed, even if some of the examples miss the mark a bit.

"[U]nsafe Rust is harder than C or C++" is entirely correct, at least when "hard" is measured by the difficulty in avoiding UB. Rust has more UB than C (strict aliasing notwithstanding) and, in unsafe, approximately similar tools to help the programmer avoid it. Nobody should be surprised by this claim.

"[T]he rules are so complex now that it's very hard to understand for a casual programmer" might not be totally justified by the examples, but even that fact supports the premise, since not knowing exactly what causes UB is what makes unsafe Rust hard.

"Making unsafe more ergonomic is a hard problem for sure but it might be worth addressing" is hard to disagree with on similar grounds. But before we can improve ergonomics we should at least understand the problem. If (as I believe) (*role).name = "basic"; actually isn't UB, that should be very clear from reading the documentation, and if it is, that still should be very clear from reading the documentation; the article correctly identifies a gap.

14 Likes

But it does create a reference to uninitialized memory, doesn't it? That's not allowed, even if the contents are only written, never read.

1 Like

I don’t think it creates a reference, no. I’m not sure where exactly this assumption that a reference is involved comes from. If it’s about the feeling that “* means Deref”, the use of * operator does not automatically imply the use of Deref trait. Also, addr_of_mut!((*role).name) contains/evaluates the same place expression “(*role).name”, and nobody questions the soundness of that… so if a reference was created then it would need to be created by assignment itself?

2 Likes

Looks like the author has since modified the article to remove the mention of unaligned writes and replaced &'static str with String to show the need for ptr::write. With those changes, it does a better job of illustrating its point.

4 Likes

I thought the article was interesting, but kind of a weird comparison because I don't think it's common to use unsafe that way. That is, the functionality desired can be achieved much more simply and with less unsafe code. I've done several years of professional rust programming including a library that uses FFI and the amount of unsafe code I've had to use is extremely minimal. Am I the outlier?

1 Like

Same here. I also use unsafe for FFI in the overwhelming majority of the time. In fact I think the only time I used unsafe for any serious purpose other than FFI is when I had to work around the lack of stable unsizing coercions for a wrapper type containing [u8]. Otherwise, I find it really hard to justify (even to myself) using it "for performance" or for performing at-best-dubious type system/lifetime tricks, and my experience is that almost all such uses outside the standard library are more often than not simply wrong (i.e., unsound).

Whenever I use unsafe for perfomance reasons, I also leave the safe version in place and have an easy compile-time switch to remove the unsafe blocks. Usually this is limited to things like switching strategic instances of unwrap or unreachable to their unchecked variants.

1 Like

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.