Adding PhantomData still compiles the code and has a UAF

After reading the explanation in PhantomData and dropck confusion - #2 by Yandros
I come out with the following code and I think it will fail to compile as there is a UAF but actually it compiles. Why the code compiles but the examples given in the link do not.
I thought the PhantomData<&'a T> will require the &'a T strictly outlive World, so the compiler will reject the code.


use std::ptr;
use std::marker::PhantomData;

use std::fmt::Debug;
impl<'a, T:Debug> Drop for World<'a, T> {
    fn drop(&mut self) {
       unsafe{
        println!("I was only {:?} days from retirement!", ptr::read(self.ptr));
       }
    }
}

struct World<'a, T:Debug> {
    ptr: *mut T,
    _marker: PhantomData<&'a mut T>,
}
fn main() {

    let mut v = 8usize;
    let mut world = World {
        ptr: &mut v as *mut usize,
        _marker: PhantomData
    };
    let mut v = 99usize;
    world.ptr = &mut v as *mut usize;
}


By writing the construction of World like that, you are not tying the lifetime inside the type of world to the lifetime of the borrow of v:

    let mut world = World /* ::<'unconstrained> */ {
        ptr: &mut v as /* discarded lifetime info */ *mut usize,
        _marker: PhantomData // any lifetime
    };

You need to use a function boundary with explicit lifetimes to "artificially" bind together the lifetime of the borrow of v and the one inside the _marker:

impl<'borrow_of_v, T> World<'borrow_of_v, T> {
    fn new (v: &'borrow_of_v mut T)
      -> World<'borrow_of_v, T>
    {
        // up until this point, same issue as before, unconstrained lifetime.
        let world: World /* <'unconstrained, T> */ =
            World { ptr: v /* as _ */, _marker: PhantomData }
        ;
        // BUT by returning `world` from a function with this signature,
        // we successfully artificially constrain the lifetime:
        return world; // : World<'borrow_of_v, T>
    }
}

By doing so, we do get a compilation error preventing the UAF:


Note: if you want to have PhantomData alter the behavior of dropck checks, you need to have used an unsafe impl<#[may_dangle]…> Drop somewhere.

3 Likes

Hi Yandros, thank you for the explanation. I see the code you give will fail to compile, but after I change the main function as follows, it will compile but with UAF. Does the code support not to compile?

    let mut v = 8usize;
    let mut world;
    
    {
        
        world = {
            World
                ::from(&mut v)
                // { ptr: &mut v as *mut usize, _marker: PhantomData }
        };
        let mut v = 9usize;

        world.ptr = &mut v as *mut usize; // why this line can compile. Is it assigning a shorter lifetime to the `ptr`
    }

Raw pointers have no lifetime attached, so Rust will never be able to trigger lifetime-related errors just because they are used: kind of the whole point of using raw pointers is being able to circumvent the borrow checker!

The trick to using raw pointers and still having the borrow checker guarding you, is to use function APIs with lifetimes attached to them, as showcased in my previous comment.

That is, if you inline the usage of raw pointers, then the compiler won't check these things, but if you use a function boundary so as to feature &'lt … references rather than Rust pointers then you will get back the lifetime checks:

- world.ptr = &mut v as *mut usize;
+ world.set(&mut v); /* outline the above line within such function */

A word of warning

While these toy examples to better learn about what the compiler checks and what it doesn't are a very good initiative, I must still warn against using unsafe and raw pointers in production when the mechanics of the borrow checker are not fully grasped :warning:

3 Likes

Great thanks, Yandros. Now I get it. And thanks for the warning too. Appreciate your explanation helps me get to know Rust better.
Following is my understanding (after reading your answers) regarding your previous note about PhantomData<T> and #may_dangle:

  • When struct Foo<T> owns T, the dropck requires that the T strictly outlive Foo<T>.
  • But sometimes the coder knows that it is OK to have T outlive Foo<T> (without "strictly"), so the coder introduces #may_dangle to ask the dropck to be a little relaxed and accept T that outlive Foo<T>.
  • Another but, the #may_dangle will accept T that has an explicit drop function and an explicit drop function will possibly do something bad as it is very powerful. So the PhantomData<T> jumps into the game and ask the dropck to reject any T with an explicit drop while still accept any T outlive Foo<T>.

Would you mind to help check my understanding?

Yeah, that's the idea. To clarify the two last bullets: sometimes we may have a type (constructor) Foo generic over T, and we know the only thing Foo does,

  • in its explicit impl Drop,

  • and w.r.t. the instances of type T it may contain,

is to call the drop glue of these instances (if any!!), e.g., by calling drop_in_place(): otherwise it has no reason to interact in any other way with the instances of type T in the explicit impl Drop.

In that case, and in that case only, one may relax the dropck requirement to allow

  • instances of type T to dangle when an instance of type Foo<T> may be dropped,

  • "Foo<T> to outlive T",

when, and only when, T is known to have no drop glue whatsoever.

  1. Relaxing the dropck requirement is done through the #[may_dangle] usage:

    unsafe
    impl<#[may_dangle] T> Drop for Foo<T> {
    
  2. But since the above, alone, is too relaxed (e.g., it will disable dropck even when T has drop glue), then we make sure Foo<T> expresses a "symbolical ownership of type T" to "disable this relaxation" by adding a PhantomData<T> field to Foo.

In any other case, PhantomData<T> is unnecessary (except for the requirement to have a type mention a type parameter at least once, to allow Rust to infer the appropriate variance, but in that regard PhantomData<T> will act the same as PhantomData<fn() -> T>, whereas the interaction with dropck will not be triggered by the latter).

  • But it can't hurt either: when in doubt, put a PhantomData<T> field, and if also in doubt w.r.t. variance, incanting a _vade_retro_ub: (PhantomData<T>, PhantomData<fn(&T)>) will yield the almighty rune of protection that shall conjure out most of the nasal demon that may try to sneak out through the dark rifts of unsafe

Some examples:

  • Foo<T : Debug> debug-prints its instance on Drop

    • (This is the usual Inspector example.)

    In this case, we are not "just dropping the instance of type T in the explicit Drop" but we are also directly interacting with part of its non-drop API, so using #[may_dangle] T there is incorrect and unsound, even with a PhantomData.

  • Foo<T> has no explicit Drop impl

    In that case there is no #[may_dangle], and no need to tell dropck anything about T.

  • Foo<T> has an explicit Drop impl that does nothing at all whatsoever w.r.t. the instances of type T it may contain

    • For instance, it could be a #[pin_project]-generated Drop impl made to ensure soundness of the Pin-projecting API by preventing the user from adding their own Drop impl. Such a struct could contain a field of type &'s str, for instance:

      struct Foo<'s> { s: &'s str }
      impl<'s> Drop for Foo<'s> {
          fn drop (self: &'_ mut Foo<'s>)
          {}
      }
      

      In that case, dropck will prevent us to drop a Foo when the str reference it contains dangles, i.e., after 's ends:

          let _f: Foo<'_>; {
              let ref s = String::from("…");
              _f = Foo { s };
          }; // <------------------ …there --------------------+
          …                                                 // |
      } // <-- Error, `_f` dropped here but `*s` is dropped… --+
      

      This is a case where writing, instead,

      unsafe
      impl<#[may_dangle] 's> Drop for Foo<'s> {
          fn drop (self: &'_ mut Foo<'s>)
          {}
      }
      

      would be warranted. And no need for PhantomData, since no drop glue whatsoever could possibly be involved (but again, it doesn't hurt).

1 Like

Hi Yandros, :100:. Your explanation is very informative and helpful! I get the idea now. And I definitely will try to write some code. As another poster said, your answers are a missing chapter in the Nomicon! Hope other people who have the same confusion will see your answers.

1 Like

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.