Why does RFC not apply to borrowing to the result of dereference to pointers?

fn main() {
    let mut i: i32 = 0;
    let ptr = &mut i as *mut _;
    unsafe {
        let rf = &mut *ptr;  // #1
        let rf2 = &mut *ptr;  // #2
        rf;
    }
}

This code can be compiled, however, according to the concept of NLL, #1 and #2 produce the loans ('a, mut, *ptr) and ('b, mut, *ptr), respectively, and they are relevant borrowing that would be conflict

For deep accesses to the path lvalue, we consider borrows relevant if they meet one of the following criteria:

  • there is a loan for the path lvalue;

such two borrowings should cause an error. However, this code is compiled. If change *ptr to a static item, this code can still be compiled. I think the latter is specified by the subtle definition of Lvalue in NLL

LV = x       // local variable
   | LV.f    // field access
   | *LV     // deref

A static item is not a local variable. However, isn't ptr a local variable? The dereference to ptr is *LV.

Where does NLL say the borrowing checking does not apply to the *LV where LV is a pointer?

1 Like

#2 is a reborrow stacked on top of #1. they are not in conflict. see stacked borrows.

EDIT:

the re-borrow at #2 actually invalidate the the (re-)borrow at #1, so this snippet actually has UB when it use rf afterwards.

to make it obey the stacked borrow rules, rf2 must reborrow from *rf (instead of the *ptr). you can check with MIRI.

fn main() {
    let mut i: i32 = 0;
    let ptr = &mut i as *mut _;
    unsafe {
        let rf = &mut *ptr;  // #1
        //let rf2 = &mut *ptr;  // this invalidates `rf`, making the next use of `rf` is UB
        let rf2 = &mut *rf;  // this is proper stacked re-borrow without UB
        rf;
    }
}

Reborrow cannot interpret the reason.

fn main() {
    let mut i = 0;
    let borrow = &mut i;
    let rf = &mut *borrow;
    let rf2 = &mut *borrow;
    rf;
}

In this example, rf and rf2 are also a reborrowing, however, they are in conflict. I just ask how the NLL RFC say the borrowing check does not apply to dereference to pointers.

sorry, I mis-read your example. I updated my previous post.

actually, even your first example compiles, it contains UB because it violates the stacked borrow.

I think the real situation here is that dereferencing a raw pointer is an unsafe operation that does zero borrow checking — the reference that is produced has an unconstrained lifetime that can be whatever you want, and no tracking of the borrowing of the place *ptr is done. Perhaps the formal answer to your question is that the two *ptr expressions are considered unrelated place expressions (lvalues) despite being spelled the same.

However, I don't see why the compiler couldn't check the conflict between uses of *ptr, though it currently does not, other than that adding such a static check now would be backwards-incompatible.

4 Likes

I didn't ask whether this program has UB. I just wonder why the borrowing check does not apply to the first example, where does NLL say such two borrowings that would conflict does not cause error?

As you can see, it works in two steps. First, we enumerate a set of in-scope borrows that are relevant to lvalue – this set is affected by whether this is a “shallow” or “deep” action, as will be described shortly. Then, for each such borrow, we check if it conflicts with the action (i.e.,, if at least one of them is potentially writing), and, if so, we report an error.

because you use raw pointer and unsafe to bypass the borrow checker, but unfortunately the code is UB.

the UB in your unsafe code is exactly what the borrow checker tries to prevent in the safe version.

I just tried to find that formal wording in NLL RFC, however, I didn't find the relevant wording. The subtle definition of Lvalue can be used to explain why the borrowing checking does not apply to static item, however, it still cannot interpret why the dereference to a pointer still pass the borrowing checking

I meant to describe what the compiler does, first imprecisely and then more precisely (but perhaps incorrectly), not to interpret the NLL RFC. (In general, you cannot expect that RFCs exactly match what the de-facto language definition is — they are just one snapshot point in the design-and-implementation process.)

1 Like

Some borrow checking is (at least sometimes) enforced in unsafe Rust in some (AFAIK) underspecified way. But when the lifetimes are arbitrary (e.g. from raw pointers), they're (at least sometimes) not.

Since I don't know any docs about this, I can't give a citation. (And I'm not in a position to figure it out by trail or code archeology at the monoment.)

But I have also long though that unsafe Rust could and should be safe easier by detecting more "dude - that's definitely language UB" patterns.

However a related option is to say "well alright, that's ok in unsafe as long as you clean it up by the time you exit unsafe". And a middle ground is, "we haven't decided yet. Proceed at your own risk. Still your fault of that leads to UB..."

Related:

TL;DR: Things are a lot more murky one you introduce unsafe. Unsafe Rust is (too) hard with (too many) uncertanies.[1]


  1. There are niche areas of easy unsafe, like unchecked indexing say. ↩︎

So, NLL does not cover this example or explain why borrowing checking does not report an error, even though *ptr is the Lvalue in its definition

Yeah I don't think to find a definitive answer in the NLL RFC.

On further consideration, I think adding that check would be feasible; I've asked IRLO about whether I’m right: Why not borrow check raw pointers’ pointee places? - Unsafe Code Guidelines - Rust Internals

2 Likes

Notably, Tree Borrows considers both mutable references to be in the Reserved state for their entire lifetimes, they never become Active / Frozen / Disabled.

Adding any actual use of rf followed by any use of rf2 makes the code actually invalid under all models.

The union example linked by @kpreid is also definitively answered in the NLL RFC, IIUC


#[repr(C)]
union Foo {
    a: (u32,u32),
    b: (u32,u32),
}
fn use_a_ref(ptr: &mut Foo) {
    unsafe {
        let ref1 = &mut ptr.a.0;  // #1
        let ref2 = &mut ptr.b.1;  // #2
        dbg!(*ref1);
    }
}

According to NLL, #1 and #2 are irrelevant borrowings so that they wouldn't conflict. However, the borrowing checker reject this example and report an error

error[E0499]: cannot borrow `*ptr` (via `ptr.b.1`) as mutable more than once at a time
  --> src/main.rs:10:20
   |
9  |         let ref1 = &mut ptr.a.0;
   |                    ------------ first mutable borrow occurs here (via `ptr.a.0`)
10 |         let ref2 = &mut ptr.b.1;
   |                    ^^^^^^^^^^^^ second mutable borrow occurs here (via `ptr.b.1`)
11 |         dbg!(*ref1);
   |              ----- first borrow later used here
   |
   = note: `ptr.b.1` is a field of the union `Foo`, so it overlaps the field `ptr.a.0`

As if the two borrowings are relevant.