The special warning against transmute(&T)-> &mut T


#1

The situation:

src/jostletree.rs:22:45: 22:54 error: mutating transmuted &mut T from &T may cause undefined behavior,consider instead using an UnsafeCell, #[deny(mutable_transmutes)] on by default

src/jostletree.rs:22 unsafe fn warp_into_mut<T>(v:&T)-> &mut T { transmute(v) }

My issue:

I believed I understood all of the reasons warp_into_mut could be problematic. I know about the litany of problems one can encounter with aliased mutable pointers in multithreaded contexts. I know about the difference between copy and copy_nonoverlapping. None of that is happening in my use case. (dunno if this sums it up well but basically all I’m doing is wrapping a fn(&T)->&T as a fn(&mut T)-> &mut T)

Now, rustc comes along and warns me me that doing that is not just unsafe, but possibly undefined behavior (and “may cause undefined behavior” dangerously close to “undefined behavior”). That’s a whole other barrel of tentacles. Then it tells me about this UnsafeCell thing, which seems to be basically a *mut, but then the folks on IRC tell me it is bestowed with magic that *muts know not.

Nowhere does it seem to be explained why this conversion should be considered such a special kind of dangerous black magic.

So I thought it’d be worth asking here.


#2

Per the Rustonomicon:

  • Transmuting an & to &mut is UB
    • Transmuting an & to &mut is always UB
    • No you can’t do it
    • No you’re not special

Sadly, I can’t find anywhere that explains why this is. By my understanding, the reason is that &mut T doesn’t mean “mutable”, it means “unaliased”. You can’t guarantee that when transmuting from &T (and if you could, then you could have just used &mut T in the first place, and you wouldn’t need a transmute). UnsafeCell is special because the compiler knows it might be aliased, so it generates different code when you access one.

As a general note, if you are writing code in an unsafe block and you don’t fully understand what it’s doing, maybe don’t do it; you’re quite possibly preparing to shoot your leg off.


#3

Yes, this is correct. Rust will pass noalias attributes for all &mut T, and readonly for all &T to LLVM, unless they contain an UnsafeCell. The compiler has knowledge of UnsafeCell thanks to this lang item.

LLVM will move stores/loads of a noalias pointer for optimization purposes. If the pointer actually is aliased, by part of it being able to transmute into a &mut T, then that violates the noalias contract, which could be re-ordered. This likely won’t cause issues if the function only has loads, so it may cause undefined behavior.


#4

You can find a semi-ongoing discussion of what exactly is undefined behavior here:

TL;DR: Nobody really knows exactly where to draw the line between “what Rust guarantees” and “what Rust’s current implementation guarantees”.


#5

It actually uses noalias for everything but &mut T (due to some LLVM bugs related to exception handling).

@makoConstruct Rust guarantees that a &T points to memory that cannot be written to by anyone, save for the regions covered by UnsafeCell.

In the case of LLVM, the attributes for &i32 are noalias readonly dereferenceable(4), so you could expect that, a function’s writes through such a pointer can be ignored, because it can never write there.
Example on playpen - look at the LLVM IR in Release mode, test will ret i32 1.

The other place where it shows up in the current implementation is statics: a static with a type that doesn’t include UnsafeCell will be placed in read-only memory and writes to it will cause segfaults.

With the advent of MIR, it may become possible to track origin of raw pointers across functions and error in the obvious UB cases instead of letting the optimizer take advantage of them.