Immutability by shared reference

How long does the immutability from being pointed to by shared references of some bytes last?
Specifically, are the bytes considered immutable, if the shared reference is dropped, but the bytes still immutably borrowed?
Example:

let mut a = [0i32; 2];
let ir: &i32;
{
    // now `a` is immutably borrowed
    ir = &a[0];
    // but the &[i32; 2] is dropped here
}
//// borrow checker kicks in, does not compile
//// meaning `a` is still considered immutably borrowed
//let _ = &mut a;
unsafe {
    // `ir` only points to the first `size_of::<i32>()` bytes
    // owned by `a`, can I modify the other bytes?
    // Undefined Behavior?
    ((&raw const a) as *const _ as *mut i32).offset(1).write(1);
}
println!("{}", ir);

This matters, for example, when making an insert-only linked list. Because members of a linked list live in their own allocated heap memory, it is theoretically safe to make immutable references to already-inserted members that live as long as the list while not impeding the insertion of new members.
I do think we know how to solve this situation without answers, since we can just connect the linked list with OnceCell<Node>s, as mentioned in the documentation for std::sync::OnceLock, but it'd be nice to know for future unknown situations.

Sorry, after more digging, I found the answer on the std::cell::UnsafeCell documentation.
For the entire lifetime of the immutable borrow, the pointee bytes are marked immutable. Quoting that document:

If you create a safe reference with lifetime 'a (either a &T or &mut T reference), then you must not access the data in any way that contradicts that reference for the remainder of 'a.

I guess you've found an answer to your question, but I'll just note that the first set of {} in your code does nothing.

1 Like

I'm a bit confused on what you mean by that.
I think I am not sure where (as in, "in what scope") the dot method call syntax coerces self into &self.
So the &[i32; 2] reference created for the <[i32; 2] as Index>::index call is out of scope regardless of being in the first set of {}?

This is false as a reference derived from it is assigned to a variable declared in the outer scope.

A reference going out of scope does not cause its lifetime/borrows to be alive at that point... or to end at that point either. (Not since NLL landed 7 years ago anyway.)

Generally, there are 2 kinds of answers to questions like this. One is the question of “does my code written like this do UB?” the other question about lifetimes is often “is this API sound?”. The first question can often be easiest to answer by running the code through miri – especially if it’s a simple API or non-generic one, because the main limitation of miri is that you might forget about relevant test cases to run through it. The second can’t really be answered by miri, API soundness with lifetimes (i.e. the 'a marks in the type system) is just not an easy thing at all… but it sounds like your question is generally about the first kind of idea, anyway.

Your concrete example core does fail miri but it looks like, first and foremost, it fails because &raw const a creates a pointer without any write permissions.

1 Like