Description
Both of Rust's aliasing models currently allow code like the following:
#[repr(align(4))]
#[derive(Debug)]
struct TwoU16s(u16, u16);
fn main() {
let mut store = TwoU16s(10, 20);
let ptr = &raw mut store;
// create a unique reference to `store`
let all = unsafe { &mut *ptr };
*all = TwoU16s(30, 40);
// create a second unique reference to one field of `store`
let one = unsafe { &mut (*ptr).1 };
// assign through each mutable reference during the lifetime of the other
*one = 50;
all.0 = 60;
*one += 5;
println!("{:?}", store);
}
This code creates two mutable references, all
which refers to the whole of the object store
and one
which refers to a single field of it, and uses both references at the same time (because one
is written through both before and after the write through all
, both mutable references must be live at the same time). This is allowed by both Tree Borrows and Stacked Borrows (because the references access disjoint memory). However, it goes against most Rust programmers' mental models of how mutable references behave (because there are two simultaneously live mutable references to the same memory, neither of which is a reborrow of the other and both of which can be used without ending the lifetime of the other).
I'm primarily concerned about the disconnect between what appears to be currently permitted and what most Rust programmers would expect to be permitted – if the aliasing model deviates too far from what people would expect to be allowed, it may lead to them making unsound assumptions. (For example, on platforms where 32-bit writes are faster than 16-bit writes, a compiler operating on an intuitive idea of how mutable references work might assume that the write to all.0
might be more efficiently implemented by writing to the entirety of all
, which might appear to have a known value (30, 40) at that point, but that would break code like the code above.)
Code like the code above might potentially be useful in practice – it makes it possible to get the correct provenance for writes to one
when using strict provenance, in cases where you can statically prove that the memory behind one
won't be written to (or given as a function argument) by other code, but can't prove that no mutable borrows of store
exist (such a case actually came up for me recently, which is what prompted me to experiment with this sort of code). As such, there seem to be advantages to both intentionally allowing it (because it makes this sort of program easier to express with strict provenance), and intentionally disallowing it (because it allows more compiler optimisations). However, it's currently unclear whether it's been allowed intentionally or unintentionally, especially because it "looks like" a bug (even if it actually isn't).
(Split out from #133 – currently "spurious" writes through a mutable reference are not allowed, but if they were allowed, they would break code like this, and thus it would have to be considered illegal in order to be able to implement optimisations that relied on spurious writes.)