&mut T
and &mut T
results in a compilation error; this is great, it's objectively wrong to borrow mutably twice.
Is *mut T
and*mut T
undefined behaviour or is this a perfectly valid thing to do? That is, is mutable pointer aliasing valid?
What makes it even worse is that &mut T
and *mut T
actually compiles and works as intended, I can modify a value through the reference, the pointer, and then the reference again... but I've seen someone say that it's undefined behaviour. Yeah, "someone said so" is the only information I have.
Here's what I tested:
fn main() {
let mut value: u8 = 42;
let r: &mut u8 = &mut value;
let p: *mut u8 = r as *mut _;
*r += 1;
unsafe { *p += 1; }
*r -= 1;
unsafe { *p -= 1; }
println!("{}", value);
}
and of course, the main point of question:
Note — Thanks to trentcl for pointing out this example actually causes a copy when creating p2
. This can be confirmed by replacing u8
with a non-Copy
type. The compiler then complains about a move. Sadly, this does not get me closer to the answer, only reminds me that I can get unintended behaviour without it being undefined behaviour, simply because of Rust's move semantics.
fn main() {
let mut value: u8 = 42;
let p1: *mut u8 = &mut value as *mut _;
// this part was edited, left in so it's easy to spot
// it's not important how I got this value, what's important is that it points to same variable and allows mutating it
// I did it this way, hoping that trying to access real value then grab new pointer again, would break something, if it was UB to do this
//let p2: *mut u8 = &mut unsafe { *p1 } as *mut _;
let p2: *mut u8 = p1;
unsafe {
*p1 += 1;
*p2 += 1;
*p1 -= 1;
*p2 -= 1;
}
println!("{}", value);
}
Both yield:
42
Does this imply that two mutable pointers pointing to the same location and being dereferenced at different times is not undefined behaviour?
I don't think testing this on compiler is a good idea to begin with, as undefined behaviour could have anything happen, even printing 42
as if nothing is wrong. I mention it anyway as this is one of things I tried, hoping to get an objective answer.
I have no clue how to write a test that could force erratic behaviour that would make it dead obvious that this doesn't work because it's not used as intended, if that's even possible to do so.
I'm aware that this is very likely to be undefined behaviour and break in a multithreaded environment no matter what. I would expect a more detailed answer than that, though, especially if mutable pointer aliasing IS NOT undefined behaviour. (This would in fact be awesome, because while I use Rust for reasons like everyone else - memory safety, to say the least... I expect to still retain a shotgun that I could point anywhere, without it being locked onto my feet. I can have aliased "mutable pointers" without blowing my feet off in C.)
This is a question about whether I can, not about whether I should. I want to dive head-on into unsafe Rust, just to learn about it, but it feels like there's not enough information unlike in "horrible" languages like C about what's undefined behaviour and what's not.
Rust doesn't actually know what push does; all Rust knows is that push takes &mut self , and here that violates the aliasing rule.
Raw pointer are more complicated. Raw pointer arithmetic and casts are "safe", but dereferencing them is not. We can convert raw pointers back to shared and mutable references, and then use them; this will certainly imply the usual reference semantics, and the compiler can optimize accordingly.
Author's note: The following is an intuitive explanation, not a rigorous one. I don't believe there is a rigorous definition of "aliasing" in Rust right now, but you may find it helpful to read the Rustonomicon chapters on references and aliasing.
The rules of references (&T
and &mut T
) are simple:
- At any given time, you can have either one mutable reference or any number of immutable references.
- References must always be valid.
There are no "rules of raw pointers". Raw pointers (*const T
and *mut T
) can alias anything, anywhere, or they can point to nothing at all.
Undefined behavior can happen when you dereference a raw pointer, implicitly or explicitly turning it into a reference. This reference still must obey the rules of references, even when the &
isn't explicit in the source code.
In your first example,
unsafe { *p += 1; }
*p += 1;
takes a &mut
reference to *p
in order to use the +=
operator, as if you had written
unsafe { AddAssign::add_assign(&mut *p, 1); }
(The compiler does not actually use AddAssign
to implement +=
for u8
, but the semantics are the same.)
Because &mut *p
is aliased by another reference, namely r
, the first rule of references is violated, causing undefined behavior.
Your second example (since editing) is different because there is no reference to alias, only another pointer, and there are no aliasing rules that govern pointers. Therefore, this
let p1: *mut u8 = &mut value;
let p2: *mut u8 = p1;
unsafe {
*p1 += 1;
*p2 += 1;
*p1 -= 1;
*p2 -= 1;
}
in the absence of any other references to value
, is perfectly sound.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With