Skip to content

Should &mut-derived pointers be permanently "separate" from their siblings? #450

Open
@RalfJung

Description

@RalfJung

The following code is fine according to LLVM noalias, but rejected by both Stacked Borrows and Tree Borrows:

fn main() { unsafe {
    let mut array = [0i32; 8];
    let ptr1 = array.as_mut_ptr();
    let ptr2 = array.as_mut_ptr();
    ptr1.write(0);
    ptr2.write(0);
} }

The reason (in Tree Borrows terms) is that when ptr2 is created, it is considered as a "sibling" pointer to ptr1, and even though their noalias scope is over, the model still remembers that these pointers are not identical and actions on one can disable the other. Put differently, ptr1 and ptr2 are derived from mutable references that were created to call as_mut_ptr, and those references are considered to be live as long as any pointer derived from them is live.

This is probably going to be surprising. Is it a problem? Tree Borrows is deliberately stricter than LLVM noalias; we want to model Rust concepts, not just the LLVM attribute, and we are hoping to get benefits even inside functions where LLVM's function-scoped noalias cannot be used.

We could attempt to allow code like the above by somehow having a notion of "end of scope" for a mutable reference, and having its no-alias requirements end completely at that point. Is that worth it? And how exactly should that work? Each reference remembers the function it was created in, and when that function ends, it ceases to impose aliasing requirements? For functions with a signature like fn(&mut T) -> &mut U, we certainly want to keep the no-alias requirement for the return reference around even after the function returned -- but maybe that can rely entirely on the caller doing a retag. The bigger concern is that this makes the function boundary very special (even more so than protectors), and the following code would still be UB:

fn main() { unsafe {
    let mut array = [0i32; 8];
    let ptr1 = &mut array as *mut _ as *mut i32;
    let ptr2 = &mut array as *mut _ as *mut i32;
    ptr1.write(0);
    ptr2.write(0);
} }

And... that seems okay? In fact I think we want that code to be UB even if (ptr1, ptr2) is returned out of the function and used in the caller (example below). That code creates two overlapping &mut, I see no good reason to allow this code. But if we reject this code, then why would we accept the variant that uses as_mut_ptr? Is it only because the &mut is now implicit, an auto-ref? Should auto-refs have weaker semantics than explicit &mut? That is insufficient, we want noalias dereferenceable for &mut self arguments. A combination of "auto-ref is somehow very weak" and "function-entry retags have an 'end of scope'" would be sufficient, though the details on what auto-refs do are fuzzy. Maybe they just don't retag at all. (Function-entry retags also don't correspond to an &mut in the source, so it could be justified that they are somehow weaker.) However I think there are plenty of cases where we want aliasing assumptions even when there was no explicit &mut, like on the references returned by get_mut-style functions.
Or should we devise something specific to methods like as_mut_ptr that suppresses the implicit reborrows (and also suppresses the noalias dereferenceable, which we really don't need for these methods)? That seems rather ad-hoc though.

// Example mentioned in the text above.
// If `&mut` loses its no-alias power at the end of the function it appears in,
// this code would be allowed.
fn helper(array: &mut [i32; 8]) -> (*mut i32, *mut i32) { unsafe {
    let ptr1 = &mut *array as *mut _ as *mut i32;
    let ptr2 = &mut *array as *mut _ as *mut i32;
    (ptr1, ptr2)
} }

fn main() { unsafe {
    let mut array = [0i32; 8];
    let (ptr1, ptr2) = helper(&mut array);
    ptr1.write(0);
    ptr2.write(0);
} }

I wonder what others think, in particular in terms of which of these examples should be allowed (if any) and which not.

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-aliasing-modelTopic: Related to the aliasing model (e.g. Stacked/Tree Borrows)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions