Valid uses of addr_of!

The documentation for addr_of! says this:

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

This wording is quite unclear about what "the usual rules" are. The community wants this use of addr_of! to be valid, as a technique to turn an unaligned pointer to a struct into an unaligned pointer to one of its fields:

(Ignore the fact that the struct isn't at some funky alignment so this code isn't quite a perfect example)

struct Misalignment {
    a: u32,
}

fn main() {
    let items: [Misalignment; 2] = [Misalignment { a: 0 }, Misalignment { a: 1 }];
    unsafe {
        let ptr: *const Misalignment = items.as_ptr().cast::<u8>().add(1).cast::<Misalignment>();
        let _ptr = core::ptr::addr_of!((*ptr).a);
    }
}

The same "usual rules" argument that says that disallowing dereference of a null pointer can reasonably lead a user to conclude that addr_of! use like above is not valid because it dereferences a misaligned pointer. But if the above code is not allowed, addr_of! is not very useful.

Other possibly-valid calls

Is this well-defined?

#[repr(packed)] struct Misaligner { _head: u8, tail: u32, } fn main() { let oops = Misaligner { _head: 0u8, tail: 0u32, }; let ptr: *const Misaligner = &oops as *const Misaligner; unsafe { let _ptr: *const u32 = core::ptr::addr_of!((*ptr).tail); } }

Deref coercion

Some users want addr_of! to not go through Deref coercion. The ambigiuty of combining "the usual rules" along with probably permitting the unaligned dereference above makes it unclear if addr_of! is somehow special. Whatever is resolved here should make it clear whether or not Deref coercion applies inside addr_of!.

Operations in order

  • Deref: Value -> Place
    • Checks: Inbounds, alignment
  • Offset: Place -> Place
    • Checks: Off-topic for this meeting, discussed in previous meeting
  • addr_of!: Place -> Value
    • Checks: Nothing

Examples for discussion

#[repr(align(8))] struct Overaligned(u32); let o = [0_u32; 2]; // Assume allocated at alignment 4, consider if size 1 let p = addr_of!(o) as *const Overaligned; let _x = (*p).0; // UB under Ralf's alignment computation, but could be allowed

If people expect the first example to be allowed (alignment violation),
then wouldn't they also expect this to be allowed? Should we allow this?

#[repr(C)] struct Misalignment { x: u32, a: u32, } fn main() { unsafe { let ptr: *const Misalignment = 1usize as *const Misalignment; let _ptr = core::ptr::addr_of!((*ptr).a); } // many people imagine this happens unsafe { let ptr: *const Misalignment = 1usize as *const Misalignment; let offset: usize = offset_of!(Misalignment.a); let out = ptr.wrapping_bytes_add(offset); } // ... but it's actually `bytes_add`, no `wrapping`! }
struct S {
    a: (),
    b: u32,
}
let r: &S = ...;
foo(); // deallocs memory backing `r`
let _x = (*r).a;

We should write down the rules and PR to addr_of/addr_of_mut

- Connor

We should come up with some set of rules for place projections/pointer derefence, and PR them to the docs. They can be conservative or lax, probably more conservative for now since they can be relaxed later.

Should we document relaxation rules:

  • Alignment probably yes?
  • Dereferenceable?
Select a repo