Learn Rust With Entirely Too Many Linked Lists
32

Stacked Borrows

Stacked borrows is the operational semantics behind Rust's aliasing rules. Conceptually similar to TBAA-meets-restrict: every reference and raw pointer carries a tag, every memory location maintains a stack of currently-valid tags, and any access by tag X pops everything above X. If your tag has been popped, your access is UB. Tree borrows is a refinement that organizes tags as a tree rather than a stack — it permits some patterns stacked borrows rejects and rejects some it permits.

  1. Make a &mut, take its address as *mut, write through the raw pointer, then write through the &mut again — that last write pops the raw pointer's tag. Subsequent use of the raw pointer is UB. The fix is to use addr_of_mut! (or &raw mut) to get a raw pointer that doesn't go through a reference at all, so its tag has no parent reference to invalidate it.
    // stacked borrows mental model:
    // stack at addr(x): [root]                <- nothing borrowed yet
    // let r = &mut x;
    // stack at addr(x): [root, r_tag]         <- r tag pushed
    // let p = r as *mut i32;
    // stack at addr(x): [root, r_tag, p_tag]  <- p derived from r
    // *r = 5;                                   <- writes via r_tag, pops p_tag
    // stack at addr(x): [root, r_tag]
    // *p = 6;                                   <- p_tag is gone -> UB
  2. The discipline this implies: pick one path to the memory and stick with it for the duration. If you need a raw pointer for the long haul, build it with addr_of_mut! so it doesn't sit on top of a reference whose lifetime you'll outlive.
    use std::ptr::addr_of_mut;
    
    let mut x = 0i32;
    let p: *mut i32 = addr_of_mut!(x);   // no reference created
    unsafe {
        *p = 1;
        *p = 2;
    }
    // no UB — there was never a competing reference