Stream: t-lang/wg-unsafe-code-guidelines

Topic: Uniqueness of mutable references


RalfJ (Apr 17 2019 at 08:47, on Zulip):

How do y'all feel about code like this?

fn test(x: &mut (i32, i32)) -> &i32 {
    let x2 = &mut *x;
    let xraw = x2 as *mut (i32, i32);
    let _val = x.1; // we just read, but this still invalidates x2 and hence xraw
    let _val = *xraw; // UB?
}

I am contemplating whether this should be UB or not. I feel the model gets much nicer if this is UB: when x2 gets invalidated, we should also invalidate what got derived from x2. However, then the following is currently also UB:

fn test(x: &mut (i32, i32)) -> &i32 {
    let xraw = x as *mut (i32, i32); // implicitly a (&mut *x)
    let _val = x.1; // we just read, but this still invalidates x2 and hence xraw
    let _val = *xraw; // UB?
}

Cc @Matthew Jasper

RalfJ (Apr 17 2019 at 08:52, on Zulip):

I would guess that also in LLVM, when a pointer can no longer be used because its noalias scope has ended, pointers derived from it cannot be used any more either? Cc @Ariel Ben-Yehuda

RalfJ (Apr 17 2019 at 09:32, on Zulip):

dang, this pattern appears in libstd... not explicitly but through the desugaring of x as *mut _ as (&mut *x) as *mut _. so it has to be allowed. :/

RalfJ (Apr 17 2019 at 09:47, on Zulip):

I could try to do some syntactic hacks to make x as *mut _ not have retagging from the implicit reborrow, but maybe we dont want to be quite so sensitive to reborrows anyway? No idea what that'll mean for LLVM compatibility though.

RalfJ (Apr 17 2019 at 10:04, on Zulip):

but this does mean that if we actually tracked raw pointer identity like LLVM does, you could not cast the same mutable ref to a raw pointer twice and then use both of them. the implicit reborrow of the second cast would certainly have to kill the preexisting raw ptr, after all we do want to know for sure that our newly created mutable reference is unique -- right? do we?

gnzlbg (Apr 17 2019 at 10:11, on Zulip):

@RalfJ do you want different semantics between let _val = x.1 and x.1 = 4; ?

RalfJ (Apr 17 2019 at 10:12, on Zulip):

@gnzlbg yes, that is unavoidable.

let x = &mut 0; let y = &*x;
let _val = *x;
let _val = *y; // can still read y after *reading* x (accepted by borrow checker)
*x = 4;
let _val = *y; // but certainly not after *writing* x
gnzlbg (Apr 17 2019 at 10:13, on Zulip):

does that also apply if I write through the pointer first, and then read through the reference ?

RalfJ (Apr 17 2019 at 10:14, on Zulip):

...?

RalfJ (Apr 17 2019 at 10:15, on Zulip):

please post code, I have no idea what you mean

gnzlbg (Apr 17 2019 at 10:15, on Zulip):
let x = &mut 0; let y = x as *mut i32;
let _val = *x;
let _val = *y; // can still read y after *reading* x (accepted by borrow checker)
*y = 4; // is this ok?
let _val = *x; // if so, is this ok?
RalfJ (Apr 17 2019 at 10:16, on Zulip):

eh, you are asking if you can write to a shared ref? no you cannot^^

gnzlbg (Apr 17 2019 at 10:16, on Zulip):

sorry, wrong type of y

RalfJ (Apr 17 2019 at 10:17, on Zulip):

by symmetry I'd expect that y can be used until x is written through

gnzlbg (Apr 17 2019 at 10:17, on Zulip):

so the two loads of x cannot be hoisted, because one can write through y, even though x is noalias ?

RalfJ (Apr 17 2019 at 10:18, on Zulip):

so yes I think your code should be okay. but the question above is about

let x = &mut 0; let y = &mut *x as *mut i32; // <- mind the extra reborrow
let _val = *x; // kills the reborrow (which is unique) and hence kills y?
let _val = *y;
*y = 4;
let _val = *x;
RalfJ (Apr 17 2019 at 10:18, on Zulip):

x being noalias means it doesnt alias with stuff that was created earlier. but it can alias with new pointers created from x.

gnzlbg (Apr 17 2019 at 10:19, on Zulip):

ah

gnzlbg (Apr 17 2019 at 10:21, on Zulip):

so i have no idea if that should be UB

gnzlbg (Apr 17 2019 at 10:21, on Zulip):

if you have to pop up the stack to find out if you can dereference x, and then you dereference y, then in the current model it would be UB IIUC because y has been popped from the stack

RalfJ (Apr 17 2019 at 10:22, on Zulip):

but with implicit reborrows this means

let x = &mut 0;
let y1 = &mut *x as *mut i32;
let y2 = &mut *x as *mut i32;
// can we still use y1?

is an open question... the &mut *x in the let y2 line asserts that it is a unique pointer (a fresh noalias pointer), which I guess should mean that y1 is not allowed to be used any more

gnzlbg (Apr 17 2019 at 10:23, on Zulip):

if one wanted to not make it UB, one would need to pop stuff from the stack to check for x, but put derived stuff from x on the top of the stack somewhere else (e.g. a second stack), and the algo would be more complicated

RalfJ (Apr 17 2019 at 10:23, on Zulip):

well the current WIP implementation somewhat gives up on the stack thing

gnzlbg (Apr 17 2019 at 10:24, on Zulip):

does making it UB enable any optimizations ?

RalfJ (Apr 17 2019 at 10:24, on Zulip):

(I am working on somewhat of a redesign of stacked borrows)

RalfJ (Apr 17 2019 at 10:24, on Zulip):

and basically removes from the stack what is incompatible with the access -- so the implicit reborrow would get removed but the raw ptr created from that would not

RalfJ (Apr 17 2019 at 10:24, on Zulip):

does making it UB enable any optimizations ?

no idea^^

gnzlbg (Apr 17 2019 at 10:25, on Zulip):

so from a user pov, this appears to be something that I might want to do

gnzlbg (Apr 17 2019 at 10:25, on Zulip):

if this is UB, i'd write more complicated code to achieve the same thing to avoid the UB

gnzlbg (Apr 17 2019 at 10:25, on Zulip):

it would be nice to know if there are any more advantages than just making stacked borrows simpler

gnzlbg (Apr 17 2019 at 10:26, on Zulip):

not that a simple stacked borrows model isn't a goal worth pursuing, but that's at tension with requiring users to write more complex code

RalfJ (Apr 17 2019 at 11:23, on Zulip):

it would be nice to know if there are any more advantages than just making stacked borrows simpler

it's not just about simpler. for the case of creating multiple mutable references, I dont see any way to to do that with the current constraints.

RalfJ (Apr 17 2019 at 11:23, on Zulip):

however, what could work is being less aggressive about retagging: maybe a simple assignment-style reborrow (x = &[mut]*y) should really truly be a NOP, even in stacked borrows

RalfJ (Apr 17 2019 at 11:23, on Zulip):

I think that would solve all these problems

RalfJ (Apr 17 2019 at 11:24, on Zulip):

at the cost of making the logic to emit retagging more complicated

RalfJ (Apr 17 2019 at 11:32, on Zulip):

it would be nice though to at least capture the explicit reborrows the user wrote... hm

Last update: Nov 20 2019 at 13:00UTC