Here's an example of the sort of code I'd like to be able to write for an unsafe Rust program I'm working on:
use core::cell::UnsafeCell;
fn main() {
// create an UnsafeCell holding two `u64`s
let c = UnsafeCell::<(u64, u64)>::new((10, 20));
// create a unique reference to the contents
let a = unsafe { &mut *c.get() };
// project that to a unique reference to the first `u64`
let b = &mut a.0;
*b = 30;
// starting from the UnsafeCell, create a mutable reference to the second `u64`
let d = unsafe { &mut (*c.get()).1 }; // (*)
// assign through both mutable references
*d = 40;
*b += 2;
println!("{:?}", unsafe { *c.get() });
}
My question is about the line that I've marked with (*). In order to get access to the second element inside the UnsafeCell
, it does *c.get()
, i.e. it is dereferencing the the entire UnsafeCell
to produce a place. However, the place in question is only used to do pointer arithmetic in order to produce another place – but the provenance of that pointer contains the entire "inside" of the UnsafeCell
. (I'm aware that the "inside" and "outside" of an UnsafeCell
have different provenances and are effectively entirely different pointers from Rust's point of view; a pointer to the outside of the UnsafeCell
would clearly be fine.)
I know that you can't do any of the following with the place produced by the second *c.get()
(without ending the lifetime of the conflicting mutable reference b
), and Miri agrees:
However, Miri seems OK with the code I've written above (both under Stacked Borrows and Tree Borrows), where the place in question is narrowed to a place that's no longer in the range of the mutable reference b
before any attempt is made to read it, write it, or create a reference.
Rust's documentation for when pointer-to-reference conversion is safe says "When creating a mutable reference, then while this reference exists, the memory it points to must not get accessed (read or written) through any other pointer or reference not derived from this reference.", which would seem to allow this – but I don't think this piece of documentation is correct, so it seems unwise to rely on it. (The documentation doesn't disallow things like creating a mutable reference to a place, creating a shared reference to the same place and not doing anything with it, then writing through the original mutable reference; but I think it is generally agreed, both by humans and by Miri, that this is undefined behaviour, even though LLVM allows it and the pointer aliasing rules in the Rust reference are mostly defined in terms of links to the LLVM documentation.)
As such, it makes me suspicious of writing code like the code I wrote above; even though it seems clear that it works on the current Rust implementation, I'm not confident as to whether or not it's guaranteed to work in the future. So my question is: is it guaranteed to be sound (even in the future) to perform a place projection on a raw pointer whose provenance overlaps a unique reference, even though there's hardly anything else you can soundly do with such a place?
After some discussion on the Rust unsafe-code-guidelines repository, the conclusion seems to be "if you read or write to bytes that overlap the range of a mutable reference, via a pointer that was not produced from that reference or a reborrow of it, it violates a safety/library invariant on that reference" (specifically, the invariant that mutable references can always read and write the bytes that they reference, except while they are reborrowed).
Safety invariants are different from validity/language invariants:
For language invariants, violating the invariant causes immediate undefined behaviour;
For library invariants, violating the invariant does not immediately cause undefined behaviour, but safe code is "allowed" to assume that the invariant holds, and thus the broken invariant can subsequently lead to undefined behaviour when the value in question is operated on by safe code.
Library invariants can be broken soundly as long as you control what code is able to observe the value with the broken invariants, and ensure that it doesn't do anything with it that would be undefined behaviour. Miri does not complain about violations of library invariants, partly for this reason (and partly because doing so would be difficult to implement).
As such, the answer to the question I asked is a bit subtle: "the place projection itself is sound; and reading or writing through the resulting pointer can be sound, as long as you don't subsequently read or write the same bytes through the mutable reference". In cases where you control the overlapped mutable reference and can ensure that it isn't used to read or write the same bytes, you can do this sort of thing soundly. In cases where you don't control the overlapped mutable reference (e.g. you're writing a library and the mutable reference is something that potentially exists in the code that calls into your library), doing this is unsound; there might not be immediate undefined behaviour, but it makes it impossible for your library to present a safe API (because the calling code is allowed to assume that its mutable reference can read or write its entire range, something that is no longer true).
In my original program, place-projecting from a
to &mut a.1
would be sound (as is demonstrated in @orlp's answer), because no mutable references are overlapped apart from a
itself. However, place-projecting from c
to &mut (*c.get()).1
breaks a library invariant on a
, making some potential uses of a
undefined behaviour even though they can be written in safe code.
As such, a literal answer "yes" to the question, despite being correct, is misleading: I asked it under the assumption that because the accesses to d
were allowed, there would be no undefined behaviour, and had not considered the possibility that future accesses to a
, after d
's lifetime was over, might be undefined behaviour. So a more useful answer is "it's not unsound yet, but you need to be very careful with what happens later on in the code, as it's easy to create undefined behaviour after doing this using operations which would otherwise be safe".