Skip to content

Stacked Borrows and Tree Borrows both currently allow overlapping mutable references to be used, as long as they don't access the same memory #576

Open
@ais523

Description

@ais523

Both of Rust's aliasing models currently allow code like the following:

#[repr(align(4))]
#[derive(Debug)]
struct TwoU16s(u16, u16);

fn main() {
    let mut store = TwoU16s(10, 20);

    let ptr = &raw mut store;
    // create a unique reference to `store`
    let all = unsafe { &mut *ptr };
    *all = TwoU16s(30, 40);
    // create a second unique reference to one field of `store`
    let one = unsafe { &mut (*ptr).1 };
    // assign through each mutable reference during the lifetime of the other
    *one = 50;
    all.0 = 60;
    *one += 5;
    
    println!("{:?}", store);
}

This code creates two mutable references, all which refers to the whole of the object store and one which refers to a single field of it, and uses both references at the same time (because one is written through both before and after the write through all, both mutable references must be live at the same time). This is allowed by both Tree Borrows and Stacked Borrows (because the references access disjoint memory). However, it goes against most Rust programmers' mental models of how mutable references behave (because there are two simultaneously live mutable references to the same memory, neither of which is a reborrow of the other and both of which can be used without ending the lifetime of the other).

I'm primarily concerned about the disconnect between what appears to be currently permitted and what most Rust programmers would expect to be permitted – if the aliasing model deviates too far from what people would expect to be allowed, it may lead to them making unsound assumptions. (For example, on platforms where 32-bit writes are faster than 16-bit writes, a compiler operating on an intuitive idea of how mutable references work might assume that the write to all.0 might be more efficiently implemented by writing to the entirety of all, which might appear to have a known value (30, 40) at that point, but that would break code like the code above.)

Code like the code above might potentially be useful in practice – it makes it possible to get the correct provenance for writes to one when using strict provenance, in cases where you can statically prove that the memory behind one won't be written to (or given as a function argument) by other code, but can't prove that no mutable borrows of store exist (such a case actually came up for me recently, which is what prompted me to experiment with this sort of code). As such, there seem to be advantages to both intentionally allowing it (because it makes this sort of program easier to express with strict provenance), and intentionally disallowing it (because it allows more compiler optimisations). However, it's currently unclear whether it's been allowed intentionally or unintentionally, especially because it "looks like" a bug (even if it actually isn't).

(Split out from #133 – currently "spurious" writes through a mutable reference are not allowed, but if they were allowed, they would break code like this, and thus it would have to be considered illegal in order to be able to implement optimisations that relied on spurious writes.)

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