While experimenting with unsafe, pointers, stacked borrows, Miri, etc., I stumbled upon an example that I cannot understand. If I have a let mut x = 0, I can think of three way to get a *mut i32 pointing to it:
&mut x as *mut i32
&mut x as *const i32 as *mut i32
&x as *const i32 as *mut i32
I was expecting only option 1 to be correct, while 2 and 3 to lead to undefined behavior if used to write through the pointer.
Now, let's put option 2 aside for a moment. If I do this
fn main() {
let mut x = 0;
{
let a = &mut x as *mut i32;
unsafe { *a = 1 }; // OK
}
{
let b = &x as *const i32 as *mut i32;
unsafe { *b = 1 }; // UB?
}
}
Miri doesn't complain about undefined behavior, whereas if I reverse the order and do this
fn main() {
let mut x = 0;
{
let b = &x as *const i32 as *mut i32;
unsafe { *b = 1 }; // UB?
}
{
let a = &mut x as *mut i32;
unsafe { *a = 1 }; // OK
}
}
it complains (as I expected) with
error: Undefined Behavior: no item granting write access to tag <untagged> at alloc1363 found in borrow stack.
--> src/main.rs:6:18
|
6 | unsafe { *b = 1 }; // UB?
| ^^^^^^ no item granting write access to tag <untagged> at alloc1363 found in borrow stack.
A few questions.
What am I missing about the previous example?
What is the use of casting from *const _ to *mut _ if, even though it is not instant UB, it becomes UB for every nontrivial operation on the resulting pointer that takes advantage of the mutability? Is the only sensible use to make a type invariant, or are there other circumstances where casting *const_ as *mut _ makes sense?
the TL;DR is that *const T and *mut T differ only superficially (and, arguably, we should've had only single * T instead of the two). What matters for stack borrows is the provenance of the pointer -- if you got a pointer from a writable location (&mut T or &UnsafeCell<T>) you can write through it, even if it passed through *const T at some point.
fn main() {
let mut x = 0;
unsafe {
let ptr = &mut x as *const i32 as *mut i32;
*ptr = 0; // UB
}
}
This is because reference as *const _ is actually (&*reference) as *const _, so if you can it's always best to go to *mut _ first. Once you have raw pointers, you can freely cast between them with no consequences.
if you got a pointer from a writable location ( &mut T or &UnsafeCell<T> ) you can write through it, even if it passed through *const T at some point.
What you (RustyYato) are saying, is my current understanding of how it's supposed to work.
This still doesn't explain the difference in my first two examples. Why does Miri treat them differently? Are they really different? Or is it a limitation of Miri?
@matklad's statement is almost correct. Once you have a *mut T, casting it to *const T has no effect, you are still allowed to write to it. playground. But going directly from a reference to a *const T creates an intermediate shared reference, which is usually not what you want. Writing T to any memory currently borrowed by a &T is UB.
Oh I see, it's because &mut x as *const i32 as *mut i32 chooses the path &mut x as &i32 as *const i32 as *mut i32 rather than &mut x as *mut i32 as *const i32 as *mut i32. Makes sense. Good to know.
You're writing to memory through a pointer derived from a shared reference, which is UB. The only case where this isn't UB is going from &UnsafeCell<T> to *mut T and writing to that raw pointer.
edit: your first example looks like a bug in MIRI, please report it
Yes, of course, Miri complaining is what I was expecting. I'm asking why it does not complain here
fn main() {
let mut x = 0;
{
let a = &mut x as *mut i32;
unsafe { *a = 1 }; // OK
}
{
let b = &x as *const i32 as *mut i32;
unsafe { *b = 1 }; // UB?
}
}
fn main() {
let mut x = 0;
{
let b = &x as *const i32 as *mut i32;
unsafe { *b = 2 }; // UB?
}
println!("{}", x);
}
I assume this is because when you reach the write through an immutable reference, the tag from the previous unique access is still on the stack below the Shr tag, so the Shr tag is just popped from the stack, revealing a previous tag that does have write access.
I think it will complain if you pass -Zmiri-track-raw-pointers. Without that flag, "all raw pointers are treated equal", so Miri "confuses" a and b in your example. With that flag, however, int-ptr-casts do not work well. Turns out that pointers are complicated...