Does addr_of_mut! require MaybeUninit?

Recently in this PR Add old rustc support as feature by Voultapher · Pull Request #33 · Voultapher/self_cell · GitHub I wondered if the macro addr_of_mut! when used with a pointer to a struct requires the members to be MaybeUninit for which a pointer is computed, without creating an intermediary reference to uninitialized data which would be UB.

The std documentation isn't entirely clear to me here:

Referencing addr_of_mut in core::ptr - Rust

Note, however, that the expr in addr_of_mut!(expr) is still subject to all the usual rules. In particular, addr_of_mut!(*ptr::null_mut()) is Undefined Behavior because it dereferences a null pointer.

That sounds like you have to use MaybeUninit as is shown in the second example. However when looking at prior art:

Looking at prior art, it's a mixed bag grep.app | code search

Not even in the std lib implementation its consistent. My questions:

  • Is MaybeUninint required, if yes inner and or outer?
  • If yes, does that mean the implementation of BTree node is UB?
  • If yes, how many more places are there, I suspect I'm not the first to run into this. What can and should we do about existing addr_of_mut UB?
  • If yes, what can we do to improve the documentation?

The place doesn't have to be initialized. See RFC 2582 (which introduced the functionality that the macro uses):

If one wants to avoid creating a reference to uninitialized data (which might or might not become part of the invariant that must be always upheld), it is also currently not possible to create a raw pointer to a field of an uninitialized struct: again, &mut uninit.field as *mut _ would create an intermediate reference to uninitialized data.

[...]

To avoid making too many assumptions by creating a reference, this RFC proposes to introduce a new primitive operation that directly creates a raw pointer to a given place. No intermediate reference exists, so no invariants have to be adhered to: the pointer may be unaligned and/or dangling.

@quinedot The code in question is this one:

core::ptr::addr_of_mut!((*joined_ptr.as_ptr()).dependent)

The reference to uninitialized data is created by *joined_ptr.as_ptr() though. The same pattern can be found in BTreeMap. The section you quoted is pretty clear that this is not allowed.

How does this create a reference? I see a dereference followed by a field expression, which the reference states is a place expression. In fact, it pretty much matches the second example in addr_of_mut.

Edit: I'm assuming no auto-Deref is involved.

So, ptr::addr_of{,_mut}! is a macro used to express &raw {,mut} expressions, which are able to produce a pointer to a valid place without going through / without producing an intermediary Rust reference, since a Rust reference:

  1. not only does it require to refer to a valid place,

  2. but it may also require to refer to a valid expression,

  3. and it does assert the aliasing or lack thereof guarantees that shared and exclusive references comes with:

    • Shared reference case: &x as *const _

      Unless T contains shared mutability (UnsafeCell) somewhere inside it, a shared reference to a T asserts the immutability of the referee, T, from the point in time where it is produced, until the point in time where that reference and any pointer derived from it is last used.

      It also invalidates the &mut aspect of any other outstanding pointers —except for the pointer/ref, if any, from which the shared reference has been derived, since that counts as a reborrow, and thus, after the aforementioned point of last use, such pointer gets its &mut capabilities back.

    • Exclusive reference case: &mut x as *mut _

      An exclusive reference to a T asserts the lack of aliasing to that T, thus fully invalidating any other outstanding pointer or reference to that T —except for the pointer/ref, if any, from which the exclusive reference has been derived, since reborrow yadda yadda…

So usage of ptr::addr_of{,mut}! avoids asserting the invariants from 2. and 3., which makes their usage less prone to Undefined Behavior, which is a big win indeed. It does, however, require to be fed a "valid place" (so as to, for instance, allow some compile backends to perform optimized pointer arithmetic). Note that the wording "valid place" is one I've just come up with, and may not be the most accurate one.

A) The challenges of a "just valid place" / avoiding the requirements 2. and 3. by using MaybeUninit and ptr::addr_of{,mut}

Note that I'm unsure about the rules regarding non-null and yet dangling pointers, or unaligned pointers. I won't talk about the latter given this lack of knowledge, and for the remainder of the post, I'll make the conservative assumption, regarding the former, that dangling pointers, not even non-null, do not point to valid places (which seems to contradict the intention behind @quinedot's quote of the RFC, but since, IIRC, ptr::addr_of…! is allowed to unsugar to LLVM's GEPi, and since that operation is UB with a dangling pointer, I remain skeptical regarding that expectation in the RFC. Take the ::memoffset crate, for instance, the most official / canonical implementation of the offsetof! operation: such crate does not use a NonNull::dangling() to compute the offsets; instead it does use MaybeUninit to get hold of a non-dangling pointer to an instance of the desired type.

Skip this if you already know well the field-by-field initialization of an uninit struct pattern, and why it must be written that way (e.g. the OP)

The key rule for a "valid place" is that a place must refer to the region of memory of (part of) an allocated element/instance/entity. Such can be obtained:

  1. from a valid (and thus initialized (for its type)) entity: let x = 42; ptr::addr_of!(x);

    • Obviously, once the instance's storage is deallocated (e.g., binding goes out of scope), the place it occupied is no longer valid / accessible, and so any pointers pointing to it are then dangling / invalidated.
  2. the pointee of a non-null Allocator-yielded pointer: a "heap" pointer pointing to uninit.

    • Until that pointer is {r,d}eallocated
  3. from the backing storage (init or uninit), of 1.

    • Same as for 1., the place is only accessible until the instance it was derived from is deallocated / goes out of scope.

    This is how one can easily obtain a "stack" pointer pointing to uninit: one starts with a valid (and thus initialized (for its type)) value of the MaybeUninit type: no matter the type T, MaybeUninit<T> has a trivial and cheap valid instance of that type, MaybeUninit::uninit(), which is an instance occupying the same size as an instance of type T would have occupied, but without caring about the value of any of its backing bytes, hence featuring uninit bytes.

    use ::core::mem::{MaybeUninit, MaybeUninit as MU};
    
    let mut value: MU<T> = MaybeUninit::uninit();
    

    We can then take a pointer to that place thanks to 1.:

    let p_uninit: *mut MU<T> = &mut value as _; // asserts lack of aliasing! It's OK.
    // or:
    let p_uninit: *mut MU<T> = ptr::addr_of_mut!(value); // place not derived from a pointer, so it also asserts lack of aliasing (to get `mut` rights).
    

    And now that we have a raw pointer, we are free to toy with / change the type of the pointee, provided we remain in the "raw pointer realm" (no Rust references!):

    let p: *mut T = <*mut MU<T>>::cast::<T>(p_uninit);
    
    • FWIW, this is such a pervasive pattern that MaybeUninit<T> offers a convenience method for this: let p: *mut T = value.as_mut_ptr();

    And now let's consider the place *p refers to. *p is of type T, so the "validity requirements" for that place are that at least part of some real instance (of whatever type) at least as big as T have been allocated there already. And we do meet that requirement, thanks to value, whose address is where p points to. So *p is a valid place, and thanks to the mutable provenance that the pointer is imbued with (whether it originated from &mut value, or ptr::addr_of_mut!(value), or value.as_mut_ptr()), it's even a place that will, on its own, yield a pointer with mutable provenance / capabilities as well (an example where this wouldn't have hold is if p_uninit had been constructed using &value or ptr::addr_of!(value)).

    Thus, ptr::addr_of_mut!(*p) is yielding a pointer identical to p: same address (guaranteed!), and same mut capabilities / provenance.

While doing addr_of_mut!(*p) is not very interesting on its own (it's literally the "inverse" of that * operator), things become more interesting, for the "initialize field-by-field" pattern (be it for a pointer to uninit memory in the heap, or in the stack), when T is, say, a struct or a tuple. Say T = (A, B);, and say we want to initialize that very pointee T since we do have a value of type A and of type B:

/// # Safety (invariants / requirements for callers to uphold):
///
///   - `out` must be a pointer with mutable capabilities over its pointee;
///
///   - well-aligned? (if we used `.write_unaligned()` below then I'm not sure about this one being mandatory, but when in doubt, it's better to be conservative and require it)
///
///   - no parallel mutation of `*out` during this call.
unsafe
fn init<A, B> (
    out: *mut (A, B),
    a: A,
    b: B,
)
{
    unsafe {
        // Safety (requirements that us, the callee, do uphold for this to be OK).
        //   - From the call-site required invariants, we are allowed to compute
        //     this `out_a` pointer, and it will have mutable capabilites over
        //     the whole `*out` range, so, *a fortiori*, over `(*out).0`.
        let out_a = ptr::addr_of_mut!( (*out).0 );
        //   - well-aligned (since `out` was), and no parallel mutation
        out_a.write(a);
    }
    unsafe {
        // Symmetrical safety comments.
        let out_b = ptr::addr_of_mut!( (*out).1 );
        out_b.write(a);
    }
}

For those not fully aware of this, in the more general case (arbitrary types A, and B), it is impossible to write a field-by-field initialization of a repr(Rust) struct (the body of init, here), without access to ptr::addr_of_mut!.

Granted, many cases do not need field-by-field init, and in this instance, doing out.write((a, b)); would have been a way simpler implementation.

But consider the case where B = &A, and so where we would like to write out_b.write(at_a); In that case, to obtain such a at_a reference without using either of ptr::addr_of{,_mut}! would have been difficult and error-prone at best, and impossible in the general case (the only way that comes to mind would be to abuse having a distinct "dummy" (but valid!) &A reference somewhere, so as to do out.write((a, dummy_ref)); so that we'd then be able to use &(*out).0 and &mut (*out).1 without triggering UB).

B) Avoiding the requirements of 3. (aliasing or lack thereof) only

In this instance, since we don't mind asserting validity (of expr) invariants, MaybeUninit may not be necessary.

But that doesn't mean that we shouldn't be mindful of aliasing (or lack thereof) invariants.

Indeed, let's consider:

struct DoublyLinkedNode {
    prev: *mut Self, // `Option<ptr::NonNull<Self>>` is better, but skipped here to keep the example overly simple.
    next: *mut Self,
}

impl DoublyLinkedNode {
    unsafe // Safety: `node`'s `next` and `prev` pointers,
           // if non-null, are well-aligned pointers to other
           // `DoublyLinkedNode` instances, with mutable capabilities (SharedReadWrite)
           // and with no mutation happening in parallel of this call.
    fn unlink (cur_node: &Cell<DoublyLinkedNode>)
    {
        let prev = cell_project!(DoublyLinkedNode, cur_node.prev).get();
        if prev.is_null().not() {
            ptr::addr_of_mut![ (*prev).next ]
                .write(ptr::null_mut())
            ;
        }
        // Same for `next`
    }
}

In this snippet, the usage of ptr::addr_of_mut!() is one way to avoid the lack-of-aliasing-violation-induced unsoundness that we'd have triggered if we had written:

// Unsound!
if prev.is_null().not() {
    (*prev).next = ptr::null_mut();
}

Indeed, the .next = … assignment to a field uses a &mut under the hood, thus asserting lack of aliasing w.r.t. that next field of the previous node, hence invalidating any other outstanding pointers to that node!

  • Such as prev_prev.next (where prev_prev would be the node at position .prev.prev w.r.t. cur_node)). So, for instance, if we were to follow up this unregistering of cur_node with prev_prev checking to see how many nodes would be available along its .next chain, it would try to go and check if (*prev_prev.next).next is null, thus performing a read across an invalidated pointer, and thus UB.

So there are two ways main ways of fixing that unsoundness:

if prev.is_null().not() {
-   (*prev).next = ptr::null_mut();
+   let prev: &Cell<DoublyLinkedNode> = &*prev.cast();
+   cell_project!(DoublyLinkedNode, prev.next).set(ptr::null_mut());

or what I did in the example:

if prev.is_null().not() {
-   (*prev).next = ptr::null_mut();
+   ptr::addr_of_mut![ (*prev).next ].write(ptr::null_mut());
  • EDIT: fixed typo, thanks @fee1-dead for reporting it!

FWIW, I initially thought that:

was gonna involve this, but it just turns out that they do use uninit memory there as well; they're just using Box::new_uninit() for that.

5 Likes

The implications in the RFC that you can &raw (*uninit).field are, apparently, misinterpretations. (But at least I'm in good company, and it is considered a goal for others.)

I think an example of how not to do it would help the documentation.

1 Like

Aren't they talking about null and dangling pointers there? I haven't seen any indication that, assuming uninit is a valid, non-null, non-dangling pointer to uninitialized data, that &raw (*uninit).field would be UB.

2 Likes

I think what threw me off is that creating a reference with &mut x is only valid on initialized memory, dereferencing with *x is valid on uninitialized memory but only aligned memory and only if the offset calculation that might be involved there points to the same allocation. So core::ptr::addr_of_mut!((*joined_ptr.as_ptr()).dependent) is valid for as long as the memory that as_ptr() points to is aligned and properly allocated.

I think the docs need to be a lot more clear about this because it's quite confusing.

How not to use ptr::addr_of{,_mut}

All of the following code snippets do not feature well-defined behavior (even if some of those restrictions may be lifted in the future).

We will assume having:

use ::core::{mem::{self, MaybeUninit, MaybeUninit as MU}, ptr};

struct S {
    foo: Foo,
    bar: Bar,
}

I'll also write as addr_of{,_mut} things that hold for both addr_of! and addr_of_mut!:

  1. Null pointer

    let p = ptr::null_mut::<S>();
    let at_foo = ptr::addr_of{,_mut}!( (*p).foo ); // UB, dereference of null pointer
    
  2. Dangling pointers

    let p = ptr::NonNull::<S>::dangling().as_ptr();
    let at_foo = ptr::addr_of{,_mut}!( (*p).foo ); // UB, dereference of dangling pointer
    

    nor

    let mut value = MaybeUninit::<S>::uninit();
    
    let p: *mut S = ptr::addr_of_mut!(value).cast();
    // or
    let p: *mut S = <*mut _>::cast(&mut value);
    // or
    let p: *mut S = value.as_mut_ptr();
    
    drop(value);
    
    let at_foo = ptr::addr_of{,_mut}!( (*p).foo ); // UB, dereference of dangling (dellocated pointee) ptr
    
  3. Unaligned pointer

    The following one is a bit unclear, although I do suspect it to be UB:

    const_assert!( mem::align_of::<S>() > 1 ); // assume this for this snippet
    
    let mut value: MU<[u8; mem::size_of::<S>()]> = MaybeUninit::uninit();
    
    let p_unaligned = <*mut _>::cast::<S>(&mut value);
    
    let at_foo = ptr::addr_of{,_mut}!( (*p_unaligned).foo ); // UB, dereference of unaligned pointer
    
    • But the following one is well-defined (p is well-aligned, it's the resulting pointer which is unaligned but it is never *-dereferenced):

      #[repr(C, packed)]
      struct Packed(u8, u16);
      
      let mut value = Packed(42, 27);
      
      let p: &mut Packed = &mut value;
      
      let at_u16_unaligned = ptr::addr_of{,_mut}!( (*p).1 );
      
      at_u16_unaligned.write_unaligned(0);
      

      This is another legit use case of ptr::addr_of{,_mut}!, where no uninit memory is involved.

  4. Aliasing rules / provenance violation

    1. let mut value = MaybeUninit::<S>::uninit();
      let p: *mut S = ptr::addr_of_mut!(value).cast(); // or any other
      
      value.write(S { foo: Foo::new(), bar: Bar::new() });
      
      let init: &mut S = &mut *p; // assume init: OK, aliasing: UB — the write to `value` asserted lack of other pointers, invalidating `p`.
      
    2. let mut value = (42, 27);
      let p: *mut (i32, i32) = ptr::addr_of_mut!(value);
      assert_eq!(value, (42, 27));
      let init: &mut S = &mut *p; // Aliasing UB: since this _exclusive_ reference
                                  // originates from `p`, it asserts lack of aliasing from
                                  // that point until here, which was violated by the shared
                                  // reference produced in the `assert_eq!` comparison.
      
    3. The :foot: :gun:

      TBD: incorrectly written container_of! patterns

3 Likes

Do you intend opening a PR for this? Otherwise if that's ok for you I would do so, I think that would be beneficial.

@Yandros as followup questions, I looked at self_cell/lib.rs at 0759fe785e2c0a8276569b2581264f2e6f85b8ab · Voultapher/self_cell · GitHub again, I wonder if it violates the aliasing rules:

let mut joined_ptr = core::mem::transmute::<NonNull<u8>, NonNull<JoinedCell>>(
    joined_void_ptr
);

Here I have a mutable valid aligned and allocated pointer to the JoinedCell. Which is passed to drop_guard

// Drop guard that cleans up should building the dependent panic.

let drop_guard =
    $crate::unsafe_self_cell::OwnerAndCellDropGuard::new(joined_ptr);

However here:

dependent_ptr.write(dependent_builder(&*owner_ptr));

I take a const reference to the owner, wouldn't that imply that there ought not be any mutable pointers to owner. Thus invalidating the joined_ptr, and making the access in OwnerAndCellDropGuard and or subsequent member functions of the SelfCell UB. Technically joined_ptr does not point at owner_ptr, but owner_ptr is derived from joined_ptr, not sure if that makes a difference.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.