Does the indirection of a pointer immediately create a reference?

Consider this example:

struct Wrapper<T> {
    live: bool,
    data: T,
}
fn main() {
    let ptr = Box::into_raw(Box::new(Wrapper {
        live: true,
        data: 0i32,
    }));
    unsafe {
        std::ptr::drop_in_place(&mut (*ptr).data); // #0
        _ = *ptr; // #1
       // _ = & mut *ptr; // #2
    }
}

Does #1 create a reference? Another question is: does #2 cause UB? If yes, where is the relevant rule? I know a relevant rule is

A reference or Box<T> must be aligned and non-null, it cannot be dangling, and it must point to a valid value (in case of dynamically sized types, using the actual dynamic type of the pointee as determined by the metadata). Note that the last point (about pointing to a valid value) remains a subject of some debate.

However, I don't find the rule that says data has invalid value after #0.

Not only it does not create a reference, it does not even constitute a read. Reading into a wildcard is NOP from opsem perspective (Miri won't flag it).

If you read not into a wildcard (e.g. let _foo = *ptr), then the pointer must be valid for reads (i.e. non-null, aligned, points at an allocated object and with correct provenance) but a reference is still not created, which has implications for the aliasing model.

I don't know if this is documented (probably yes) and where, but this is pretty much agreed upon.

You probably want to read What is a place expression?

How about the second question, and this one?

However, I don't find the rule that says data has an invalid value after #0.

Whatever’s left after dropping a value of type T might not be a valid value of type T. In particular, one of the requirements for Box<_> values to be valid is that they must be dereferenceable, but dropping a box deallocates its backing memory (so it might not be dereferenceable anymore). This isn’t a rule, just… a fact or property of Rust, I suppose.

One of the rules for creating a reference to a value is that the value must be valid for its type, as you noted. Therefore, #2 is currently considered UB.

To my understanding, it’s quite possible that #2 will eventually be considered to violate the safety invariant of references (so exposing a reference to an invalid value to arbitrary safe Rust code would be unsound and could cause UB) but not the language-level validity invariant of references (so it would not instantly cause UB, and very careful unsafe code could do it). This is not currently guaranteed, it’s just a future possibility: Should validity of a reference depend on the *contents* of memory in any way? · Issue #414 · rust-lang/unsafe-code-guidelines · GitHub

One of the rules for creating a reference to a value is that the value must be valid for its type, as you noted. Therefore, #2 is currently considered UB.

The question is, I don't find any rule that says that (*ptr).data has an invalid value after #0.

First, this is undecided.

Now: if T is not Copy, this has a very real possibility to be defined to be UB (assuming you do perform a read, and not _ = (*ptr).data), see MIR move elimination by Amanieu · Pull Request #3943 · rust-lang/rfcs · GitHub. If T is Copy, like here, then moving from it does not invalidate it, but drop_in_place() might.

Do you mean it is undecided whether #2 is UB or not in the current Rust?

That’s what I answered in the first paragraph, though? That fact isn’t a “rule”, but it’s still a fact of Rust.

let _x = *ptr

does create a reference a dereference it. Very unsafe.

let _ = *ptr

This is a weird special case in Rust that has no practical use for pointers. Unlike literally any other variable name, this isn't assignment of an expression, it's a match of a pattern that doesn't do anything and the expression is unused. This being a magical no-op used to even accidentally allow unsafe code in safe Rust, because the code was ignored and did nothing (but those bugs have been fixed).

It doesn't make *ptr any safer, but rather it's a useless and unused and whether that has consequences is unclear (if a ptr is dereferenced in a forest and nobody hears it, is it sound?)

My inner - albeit insufferably pedantic - side couldn't help but wish you'd phrase it as:

if a ptr is dereferenced in a forest and nobody ever reads from it, is it sound?

"Hearing" a pointer being dereferenced can be somewhat .. challenging to imagine.


In addition to everything mentioned before, don't forget about the scope:

fn main() {
    let addr: usize = 0;
    let ptr = addr as *const i32;
    // no UB
    unsafe { 
        _ = *ptr; 
        let _ = *ptr; 
    }
    // definite UB
    let _ = unsafe { *ptr };
}

Does this really create a reference? IMO, this only results in a read?

I agree that it doesn’t actually create a reference, though my instinct is that it’s equivalent to creating an extremely short-lived immutable/shared reference (and reading from that)…

eh I don’t know.

It’s definitely a different operation in the Abstract Machine than creating a reference. I’m slightly suspicious of the possibility that creating a reference invalidates other pointers/references that otherwise wouldn’t be. I checked the Tree Borrows paper just now, and I’m fairly sure it’s equivalent. I’m also fairly confident that it’s equivalent in Stacked Borrows.

Not every use of *ptr makes a reference, and not every use of this syntax is a read. It can be a place that is used without dereferencing it, e.g. addr_of! and &raw are special.

Reading a pointer via ptr::read is also a special operation that copies bytes without treating it as a reference. It's still unsafe and requires the pointer to be valid, but doesn't have all the extra requirements of safe Rust references around immutability and exclusivity.

However, if you get a value by dereferencing a pointer, it is equivalent of turning the pointer into a reference and using that reference.

Ok, I don't find this statement in the Rust Reference; this may need to be documented.

I'm pretty sure that's not correct. This is also not lowered into the same MIR. However I'm unable to construct an example where not creating a reference has implications on aliasing (i.e. where the aliasing model will disallow creating a reference but not reading).

Isn’t ptr::read(ptr) just special w.r.t. null pointers, compared to reading *ptr?

Aside from volatile or atomic reads, of course.

It's not special wrt. null pointers. Both disallow them.

It's only special in that ptr::read() allows you to read non-Copy types, and essentially duplicates them.

No, ptr::read allows null pointers for ZSTs. I’m not sure about *ptr… dunno where that’s documented.