3
votes

I have struct that has unsafe code and raw mutable pointers to another type of struct. The unsafe struct should only be used during the lifetime of the other struct but you can not specify a lifetime for pointers. I discovered std::marker::PhantomData could be used for this unused lifetime issues but I am having issues getting it to work. I'm not sure if this is an invalid use case or I'm doing something wrong.

Simplified Example:

use std::marker::PhantomData;

pub struct Test {
    value: u32,
}

impl Test {
    pub fn value(&self) {
        println!("{}", self.value)
    }

    pub fn set_value(&mut self, value: u32) {
        self.value = value;
    }
}

// I want compiler to complain about the lifetime of test
// so that UnsafeStruct is not used after test is dropped
pub struct UnsafeStruct<'a> {
    test: *mut Test,
    phantom: PhantomData<&'a mut Test>,
}

impl<'a> UnsafeStruct<'a> {
    pub fn new(test: &'a mut Test) -> UnsafeStruct<'a> {
        UnsafeStruct {
            test: test,
            phantom: PhantomData,
        }
    }

    pub fn test_value(&self) {
        unsafe { println!("{}", (*self.test).value) }
    }

    pub fn set_test_value(&mut self, value: u32) {
        unsafe {
            (*self.test).set_value(value);
        }
    }
}

fn main() {
    // No borrow checker errors
    // but the compiler does not complain about lifetime of test
    let mut unsafe_struct: UnsafeStruct;
    {
        let mut test = Test { value: 0 };
        unsafe_struct = UnsafeStruct {
            test: &mut test,
            phantom: PhantomData,
        };

        unsafe_struct.set_test_value(1);
        test.value();

        test.set_value(2);
        unsafe_struct.test_value();
    }
    unsafe_struct.set_test_value(3);
    unsafe_struct.test_value();

    // Lifetime errors caught
    // but there will be borrow checker errors if you fix
    let mut unsafe_struct: UnsafeStruct;
    {
        let mut test = Test { value: 0 };
        unsafe_struct = UnsafeStruct::new(&mut test);

        unsafe_struct.set_test_value(1);
        test.value();

        test.set_value(2);
        unsafe_struct.test_value();
    }
    unsafe_struct.set_test_value(3);
    unsafe_struct.test_value();

    // Borrow checker errors when you fix lifetime error
    {
        let mut test = Test { value: 0 };
        let mut unsafe_struct: UnsafeStruct;
        unsafe_struct = UnsafeStruct::new(&mut test);

        unsafe_struct.set_test_value(1);
        test.value();

        test.set_value(2);
        unsafe_struct.test_value();
    }
}

If I create the UnsafeStruct directly the compiler does not catch the lifetime errors and I would like to use a constructor function anyway. If I use the constructor function then I have borrow checker errors. Is it possible to fix this code, such that the compiler will error when attempting to use a UnsafeStruct outside of the lifetime of the corresponding Test, but will not have the borrow checking errors shown in the example?

3
Why not just use a mutable reference?orlp
If you mean a mutable reference in the UnsafeStruct that would not work because I need unsafe behavior.Jake
Can you clarify what you mean with "I need unsafe behavior"?orlp
I need raw pointer functionality. I need to manipulate a struct directly outside borrow checking rules.Jake
You can store a mutable reference in the struct, and cast it to a raw pointer in your unsafe code when needed.Sven Marnach

3 Answers

3
votes

I am answering my own question. The problem I was trying to solve was using std::marker::PhantomData to achieve adding lifetimes to a struct with raw pointers to prevent use after free errors. You can not achieve this with PhantomData. There is a use case for handling unhandled lifetimes, but that is different than what I was trying accomplish, and was the source of my confusion / question.

I was already aware and have handled the fact that you must handle use after free and other errors when using unsafe code. I just thought I might be able to handle this type of use after free error at compile time instead of runtime.

1
votes

TL;DR What you're doing violates the exclusivity requirement of mutable references, but you can use shared references and internal mutability to make an API that works.

A &mut T reference represents exclusive access to a T. When you borrow an object with &mut, that object must not be accessed (mutably or immutably), through any other reference, for the lifetime of the &mut borrow. In this example:

let mut test = Test { value: 0 };
let mut unsafe_struct: UnsafeStruct;
unsafe_struct = UnsafeStruct::new(&mut test);

unsafe_struct.set_test_value(1);
test.value();

test.set_value(2);
unsafe_struct.test_value();

unsafe_struct keeps the &mut borrow of test alive. It doesn't matter that internally it contains a raw pointer; it could contain nothing. The 'a in UnsafeStruct<'a> extends the lifetime of the borrow, making it undefined behavior to access test directly, until after unsafe_struct is used for the last time.

The example suggests that you actually want shared access to a resource (that is, shared between test and unsafe_struct). Rust has a shared reference type; it's &T. If you want the original T to still be accessible while a borrow is live, that borrow has to be shared (&), not exclusive (&mut).

How do you mutate something if all you have is a shared reference? Using internal mutability.

use std::cell::Cell;

pub struct Test {
    value: Cell<u32>,
}

impl Test {
    pub fn value(&self) {
        println!("{}", self.value.get())
    }

    pub fn set_value(&self, value: u32) {
        self.value.set(value);
    }
}

pub struct SafeStruct<'a> {
    test: &'a Test,
}

impl<'a> SafeStruct<'a> {
    pub fn new(test: &'a Test) -> SafeStruct<'a> {
        SafeStruct { test }
    }

    pub fn test_value(&self) {
        println!("{}", self.test.value.get())
    }

    pub fn set_test_value(&self, value: u32) {
        self.test.set_value(value);
    }
}

There's no unsafe code left -- Cell is a safe abstraction. You could also use AtomicU32 instead of Cell<u32>, for thread-safety, or if the real content of Test is more complicated, RefCell, RwLock, or Mutex. These are all abstractions that provide shared ("internal") mutability, but they differ in usage. Read the documentation and the links below for more detail.

As a final resort, if you need shared mutable access to an object with no overhead, and take full responsibility for guaranteeing its correctness on your own shoulders, you can use UnsafeCell. This does require using unsafe code, but you can write any API you want. Note that all the safe abstractions I just mentioned are built using UnsafeCell internally. You cannot have shared mutability without it.

Links

0
votes

I saw the question has an accepted answer, but I would like to add more explanation to the question.

Note: You can still protect yourself from use-after-free bugs when working with unsafe code. It depends on what you want to achieve. For example, the std Vec is written using unsafe code, but use after free on Vec is prohibited.

Following is a detailed explanation of the question.

For example 1

    // No borrow checker errors
    // but the compiler does not complain about lifetime of test
    let mut unsafe_struct: UnsafeStruct;
    {
        let mut test = Test { value: 0 };
        unsafe_struct = UnsafeStruct {
            test: &mut test,
            phantom: PhantomData,
        };

        unsafe_struct.set_test_value(1);
        test.value();

        test.set_value(2);
        unsafe_struct.test_value();
    }
    unsafe_struct.set_test_value(3); // line uaf
    unsafe_struct.test_value();      //line uaf

I assume you are asking why lines marked with line uaf are accepted by the rust compiler. The answer is that you are directly manipulating the pointer. Rust lifetime can only work with reference.

For your second example

       Lifetime errors caught
    but there will be borrow checker errors if you fix
    let mut unsafe_struct: UnsafeStruct;
    {
        let mut test = Test { value: 0 };
        unsafe_struct = UnsafeStruct::new(&mut test);

        unsafe_struct.set_test_value(1);
        test.value();

        test.set_value(2);
        unsafe_struct.test_value();
    }
    unsafe_struct.set_test_value(3);
    unsafe_struct.test_value();

The borrow checker error is easy to understand as in Rust, you cannot have more than one mutable reference or mutable reference and immutable reference at the same time. And for the lifetime error, it is because when you create the UnsafeStruct with ::new, you are using the function pub fn new(test: &'a mut Test) -> UnsafeStruct<'a> . This function is asserting that the input reference test will be valid as long as 'a which is the lifetime of the structure. But in the code above (example 2), the input lifetime of the new function is shorter than the lifetime of the struct. Let me annotate them as follows

    let mut unsafe_struct: UnsafeStruct; 
    {
        let mut test = Test { value: 0 };
        unsafe_struct = UnsafeStruct::new(&'test mut test); // the reference of test has the lifetime 'test which is clear that it is shorter than the lifetime 'struct.

        unsafe_struct.set_test_value(1);
        test.value();

        test.set_value(2);
        unsafe_struct.test_value();
    }
    unsafe_struct.set_test_value(3);
    unsafe_struct.test_value(); // the lifetime of unsafe_struct ends here.

With the above explanation, example 3 should be clear.