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?