Ptr_metadata and unsizing null/dangling raw pointers: safe or not?

Hello!

Please excuse me if there is an obvious answer to this -- I have looked for it in the reference and a few RFCs, but I might have missed it and I haven't been following memory model discussions closely: About ptr_metadata (RFC 2580, issue #81513, I can't link them because I'm new), is it clarified anywhere what it means to unsize a null/dangling data pointer, regarding the metadata of the resulting wide pointer? On current nightly, this does not require unsafe:

#![feature(ptr_metadata)]

use std::ptr;

struct Foo;
struct NotFoo;

trait Bar {}

impl Bar for Foo {}
impl Bar for NotFoo {}

fn main() {
    let m1 = ptr::metadata(&Foo as &dyn Bar);
    let m2 = ptr::metadata(&NotFoo as &dyn Bar);
    let m3 = ptr::metadata(ptr::null::<Foo>() as *const dyn Bar);
    println!("metadata(&Foo as _) eq metadata(&NotFoo as _): {:?}", m1.eq(&m2));
    println!("metadata(&Foo as _) eq metadata(null::<Foo>() as _): {:?}", m1.eq(&m3));
    println!("metadata(&NotFoo as _) eq metadata(null::<Foo>() as _): {:?}", m2.eq(&m3));
}

... and outputs:

metadata(&Foo as _) eq metadata(&NotFoo as _): false
metadata(&Foo as _) eq metadata(null::<Foo>() as _): true
metadata(&NotFoo as _) eq metadata(null::<Foo>() as _): false

Is this considered one of the following or something else?

  • Undefined
  • Defined but invalid/unspecified, i.e. it is UB to dereference ptr::from_raw_parts(&foo as *const _, ptr::metadata(ptr::null::<Foo>() as _)) as a &dyn Bar
  • Valid, i.e. it is safe to dereference ptr::from_raw_parts(&foo as *const _, ptr::metadata(ptr::null::<Foo>() as _)) as a &dyn Bar (given foo: Foo and an appropriate lifetime)

The exact requirements for the metadata field in a fat raw pointer is an active topic of discussion, so at this time you should follow the strictest possible interpretation to ensure that your code is valid, with the possibility that the rules are relaxed in the future.

The strictest interpretation that is compatible with your code being safe is that:

  • When creating a raw pointer from raw parts, the vtable field must always point to allocated memory containing an actual vtable for some type implementing that trait. However it isn't required that the raw pointer actually points at a value that the vtable is valid for until you dereference the raw pointer.
  • When obtaining a vtable from a raw pointer, you can only assume that it is a valid vtable for a given type if you created it from a real reference to that type.
3 Likes

In your example there's no raw pointer dereference and all the metadata are constructed from valid source - the concrete type Foo and NotFoo.

1 Like

Thanks for the pointers! I see that this is something I need to keep a closer watch on.

This does seem to be a very reasonable interpretation for now, then. Although from a implementation viewpoint it is most practical to just tack a real vtable upon unsizing without looking at the data pointers at all, i.e.

..., throw optimization into the mix and things can get complicated. I guess it is indeed safest to avoid relying on this behavior until it's defined, or at least verified on multiple platforms for a certain pinned nightly.

Thanks for the help!

That could make maintenance of such code pretty hazardous, since rustc reserves the right to change those details in any compiler released before they are formally specified.

If you can accomplish something in safe Rust on stable, it's not UB (unless there's a bug in the compiler/stdlib or oversight in the language). And if you have something that is UB, it's UB regardless of whether optimization (compilation generally) happens to produce a misbehaving program or not.

(Mentioning this as your example used no unsafe but didn't align with your question.)

Yes, and so I was asking for clarification? The only reason why I'm resorting to guesswork is because the clarification I got was "nobody's sure yet".

How? They seem to align to me. Would you please explain what you meant here?

First, your example uses no unsafe, and if this remains the case once stabilized (e.g. if metadata does not become an unsafe function), than it will not be UB.

Next, your actual question as I interpreted it is:

But your example doesn't do this, it just compares the return values of metadata. Your example doesn't call ptr::from_raw_parts. You don't construct (or dereference) a *dyn Bar much less a &dyn Bar.