Hi folks,
I am trying to get my head around reference aliasing, stacked borrows and UB.
Consider this code which creates an immutable reference to buffer under Vec but does not touch the vec
until drop.
struct MyStruct<'a> {
vec: &'a mut Vec<u8>,
// aliases vec's buffer but we promise not to touch vec until drop
buf: &'a [u8]
}
impl<'a> MyStruct<'a> {
fn new(vec: &'a mut Vec<u8>) -> Self {
let buf = unsafe { std::slice::from_raw_parts(vec.as_ptr(), vec.len()) };
Self {
vec,
buf
}
}
// hand of ref, people can do something
fn get_ref(&self) -> &[u8] {
self.buf
}
}
impl<'a> Drop for MyStruct<'a> {
fn drop(&mut self) {
// do something with vec content
self.vec[0] = 47;
}
}
fn main() {
let v = vec![0];
let s = MyStruct::new(&mut v);
let myref = s.get_ref();
let read = *myref;
drop(s)
}
For now consider that 'a
lifetime does not escape from the struct (e.g. by having fn get_ref(&self) -> &'a [u8]
as then it is obvious UB as you can hand out 'a
immutable lifetime and then mutate data under it when dropping MyStruct
).
My first question is - Is creation of immutable alias immediate UB in MyStruct::new()
? Rust says that this shouldn't be allowed. But then, we have stacked borrows and this is clearly a borrow from vec so the aliasing should be allowed.
If this isn't immediate UB, then I believe the stacked borrow says that if we don't access buf
reference after drop
, it should be sound to modify vec
in drop
(in stacked-borrow model accessing vec
walks the stack-borrow stack and invalidates buf
but because we never access buf
afterwards everything is shiny).
Now, the real question - if these two above are not UB, then what will prevent optimizer from assuming that there are two independent borrows and moving potential read
of &s.buf
after write to .vec
in drop
. Reading a (valid) reference should be non-sideeffect which in terms could be re-ordered with side-effect of writing to "another" memory location. Now this is obviously unsound so at least one of the three things must be unsound.