I am investigating a specific case of Undefined Behavior (UB) under the Stacked Borrows memory model. I would like to confirm the physical mechanism of memory access when a reference’s tag is no longer present in the borrow stack.
The Scenario
Suppose we have a structured type and a reference ref_a: &T (or &mut T) pointing to it. Due to a subsequent write operation via a raw pointer or a different unique reference, the Tag associated with ref_a is physically popped from the borrow stack of the underlying memory.
If the code subsequently attempts to access a nested field:
Rustlet x = ref_a.field_a.field_b;
Is it an UB? Miri does not report such an error:
Miri does report an error. (That is as far as I got.)
In your example code, you only write to list_ref.h.prev in particular via the raw pointer. I think each byte of memory gets its own stack of tags, unless I'm mistaken, so using the reference to access list_ref.h.next is not UB (list_ref's tags over the bytes of memory comprising list_ref.h.next remain unpopped).
Some details about field projections work are presumably relevant as well; for obvious reasons, evaluating &list_ref.h.next (which println! internally does) "materializes" a reference at least to the next field of list_ref.h in particular, triggering a retag to assert permission to read at least that field. However, retags aren't eagerly inserted at each step. &list_ref.h.next (absent Deref coercions and whatnot, which don't occur in your code) should just perform pointer arithmetic, adding an appropriate offset to a &List to yield a reference to the field you want, which doesn't require reading list_ref or even just asserting that it could be read or retagged.
For the line at the end, but not the previous line.
if access list_ref.h is an physical UB, then list_ref.h.next in binary code should be considered as
- load list_ref.h as h
- load h.next
e.g. It is a form of indirect addressing
Then an UB must exists in list_ref.h.next;
I mean: if it is UB, then it should an UB in physical world, not only in SB.
So, if list_ref.h.next is not an UB, then list_ref.h should not be taken as an UB
I really dislike the syntax of field projections TBH. Precisely because of this confusion, and because the finer details of field projections are so relevant for unsafe code. And the instant you add other coercions, like Deref coercion, something akin to what you're thinking happens would indeed happen, and it'd be UB. But no, evaluating &list_ref.h.next does not load from list_ref or from list_ref.h. It just does pointer arithmetic. Then, reading from &list_ref.h.next only loads that one specific field.
1 Like
This page seems to have some useful information about projections: Place in rustc_middle::mir - Rust
The only ways for a place computation can cause UB are:
- On a Deref projection, we do an actual load of the inner place, with all the usual consequences (the inner place must be based on an aligned pointer, it must point to allocated memory, the aliasig model must allow reads, this must not be a data race).
- For the projections that perform pointer arithmetic, the offset must in-bounds of an allocation (i.e., the preconditions of ptr::offset must be met).
Note that place[0] only counts as some sort of Index projection (which seems to fall in the "just doing pointer arithmetic, not retagging the reference to all of place" category) if place is a place of type [T] or [T; N]. Otherwise, it's just syntax sugar for a method call, not a "true" form of projection. Yayyyyyy, so there can be subtle differences in UB-relevant semantics between user-defined indexable types and the primitive slice and array types, just like Deref coercions messing with field projections.
2 Likes