Struct vstd::simple_pptr::PPtr

source ·
pub struct PPtr<V>(pub usize, pub PhantomData<V>);
Expand description

PPtr (which stands for “permissioned pointer”) is a wrapper around a raw pointer to a heap-allocated V. This is designed to be simpler to use that Verus’s more general pointer support, but also to serve as a better introductory point. Historically, PPtr was positioned as a “trusted primitive” of Verus, but now, it is implemented and verified from the more general pointer support, which operates on similar principles, but which is much precise to Rust’s pointer semantics.

A PPtr is equvialent to its usize-based address. The type paramter V technically doesn’t matter, and you can freely convert between PPtr<V> and PPtr<W> by casting to and from the usize address. What really matters is the type paramter of the PointsTo<V>.

In order to access (read or write) the value behind the pointer, the user needs a special ghost permission token, PointsTo<V>. This object is tracked, which means that it is “only a proof construct” that does not appear in compiled code, but its uses are checked by the borrow-checker. This ensures memory safety, data-race-freedom, prohibits use-after-free, etc.

PointsTo objects.

The PointsTo object represents both the ability to access (read or write) the data behind the pointer and the ability to free it (return it to the memory allocator).

The perm: PointsTo<V> object tracks two pieces of data:

Your access to the PointsTo object determines what operations you can safely perform with the pointer:

  • You can read from the pointer as long as you have read access to the PointsTo object, e.g., &PointsTo<V>.
  • You can write to the pointer as long as you have mutable access to the PointsTo object, e.g., &mut PointsTo<V>
  • You can call free to deallocate the memory as long as you have full onwership of the PointsTo object (i.e., the ability to move it).

For those familiar with separation logic, the PointsTo object plays a role similar to that of the “points-to” operator, ptrvalue.

Example

fn main() {
    unsafe {
        // ALLOCATE
        // p: PPtr<u64>, points_to: PointsTo<u64>
        let (p, Tracked(mut points_to)) = PPtr::<u64>::empty();

        assert(points_to.mem_contents() === MemContents::Uninit);
        assert(points_to.pptr() == p);

        // unsafe { *p = 5; }
        p.write(Tracked(&mut points_to), 5);

        assert(points_to.mem_contents() === MemContents::Init(5));
        assert(points_to.pptr() == p);

        // let x = unsafe { *p };
        let x = p.read(Tracked(&points_to));

        assert(x == 5);

        // DEALLOCATE
        let y = p.into_inner(Tracked(points_to));

        assert(y == 5);
    }
}

Examples of incorrect usage

The following code has a use-after-free bug, and it is rejected by Verus because it fails to satisfy Rust’s ownership-checker.

fn main() {
    unsafe {
        // ALLOCATE
        // p: PPtr<u64>, points_to: PointsTo<u64>
        let (p, Tracked(mut points_to)) = PPtr::<u64>::empty();

        // unsafe { *p = 5; }
        p.write(Tracked(&mut points_to), 5);

        // let x = unsafe { *p };
        let x = p.read(Tracked(&points_to));

        // DEALLOCATE
        p.free(Tracked(points_to));                 // `points_to` is moved here

        // READ-AFTER-FREE
        let x2 = p.read(Tracked(&mut points_to));   // so it can't be used here
    }
}

The following doesn’t violate Rust’s ownership-checking, but it “mixes up” the PointsTo objects, attempting to use the wrong PointsTo for the given pointer. This violates the precondition on p.read().

fn main() {
    unsafe {
        // ALLOCATE p
        let (p, Tracked(mut perm_p)) = PPtr::<u64>::empty();

        // ALLOCATE q
        let (q, Tracked(mut perm_q)) = PPtr::<u64>::empty();

        // DEALLOCATE p
        p.free(Tracked(perm_p));

        // READ-AFTER-FREE (read from p, try to use q's permission object)
        let x = p.read(Tracked(&mut perm_q));
    }
}

Differences from PCell.

PPtr is similar to cell::PCell, but has a few key differences:

  • In PCell<V>, the type V is placed internally to the PCell, whereas with PPtr, the type V is placed at some location on the heap.
  • Since PPtr is just a pointer (represented by an integer), it can be Copy.
  • The ptr::PointsTo token represents not just the permission to read/write the contents, but also to deallocate.

Simplifications relative to more general pointer API

  • Pointers are only represented by addresses and don’t have a general notion of provenance
  • Pointers are untyped (only PointsTo is typed).
  • The PointsTo also encapsulates the permission to free a pointer.
  • PointsTo tokens are non-fungible. They can’t be broken up or made variable-sized.

Tuple Fields§

§0: usize§1: PhantomData<V>

Implementations§

source§

impl<V> PPtr<V>

source

pub open spec fn spec_addr(p: PPtr<V>) -> usize

{ p.0 }

Use addr() instead

source

pub exec fn addr(self) -> usize

ensures
u == self.addr(),

Cast a pointer to an integer.

source

pub exec fn from_addr(u: usize) -> Self

ensures
u == s.addr(),

Cast an integer to a pointer.

Note that this does not require or ensure that the pointer is valid. Of course, if the user creates an invalid pointer, they would still not be able to create a valid PointsTo token for it, and thus they would never be able to access the data behind the pointer.

This is analogous to normal Rust, where casting to a pointer is always possible, but dereferencing a pointer is an unsafe operation. With PPtr, casting to a pointer is likewise always possible, while dereferencing it is only allowed when the right preconditions are met.

source§

impl<V> PPtr<V>

source

pub exec fn empty() -> (PPtr<V>, Tracked<PointsTo<V>>)

ensures
pt.1@.pptr() == pt.0,
pt.1@.is_uninit(),

Allocates heap memory for type V, leaving it uninitialized.

source

pub exec fn new(v: V) -> (PPtr<V>, Tracked<PointsTo<V>>)

ensures
pt.1@.pptr() == pt.0,
pt.1@.mem_contents() == MemContents::Init(v),

Allocates heap memory for type V, leaving it initialized with the given value v.

source

pub exec fn free(self, verus_tmp_perm: Tracked<PointsTo<V>>)

requires
perm.pptr() == self,
perm.is_uninit(),

Free the memory pointed to be perm. Requires the memory to be uninitialized.

This consumes perm, since it will no longer be safe to access that memory location.

source

pub exec fn into_inner(self, verus_tmp_perm: Tracked<PointsTo<V>>) -> V

requires
perm.pptr() == self,
perm.is_init(),
ensures
v == perm.value(),

Free the memory pointed to be perm and return the value that was previously there. Requires the memory to be initialized. This consumes the PointsTo token, since the user is giving up access to the memory by freeing it.

source

pub exec fn put(self, verus_tmp_perm: Tracked<&mut PointsTo<V>>, v: V)

requires
old(perm).pptr() == self,
old(perm).mem_contents() == MemContents::Uninit::<V>,
ensures
perm.pptr() == old(perm).pptr(),
perm.mem_contents() == MemContents::Init(v),

Moves v into the location pointed to by the pointer self. Requires the memory to be uninitialized, and leaves it initialized.

In the ghost perspective, this updates perm.mem_contents() from MemContents::Uninit to MemContents::Init(v).

source

pub exec fn take(self, verus_tmp_perm: Tracked<&mut PointsTo<V>>) -> V

requires
old(perm).pptr() == self,
old(perm).is_init(),
ensures
perm.pptr() == old(perm).pptr(),
perm.mem_contents() == MemContents::Uninit::<V>,
v == old(perm).value(),

Moves v out of the location pointed to by the pointer self and returns it. Requires the memory to be initialized, and leaves it uninitialized.

In the ghost perspective, this updates perm.value from Some(v) to None, while returning the v as an exec value.

source

pub exec fn replace(self, verus_tmp_perm: Tracked<&mut PointsTo<V>>, in_v: V) -> V

requires
old(perm).pptr() == self,
old(perm).is_init(),
ensures
perm.pptr() == old(perm).pptr(),
perm.mem_contents() == MemContents::Init(in_v),
out_v == old(perm).value(),

Swaps the in_v: V passed in as an argument with the value in memory. Requires the memory to be initialized, and leaves it initialized with the new value.

source

pub exec fn borrow<'a>(self, verus_tmp_perm: Tracked<&'a PointsTo<V>>) -> &'a V

requires
perm.pptr() == self,
perm.is_init(),
ensures
*v === perm.value(),

Given a shared borrow of the PointsTo<V>, obtain a shared borrow of V.

source

pub exec fn write(self, verus_tmp_perm: Tracked<&mut PointsTo<V>>, in_v: V)
where V: Copy,

requires
old(perm).pptr() == self,
ensures
perm.pptr() === old(perm).pptr(),
perm.mem_contents() === MemContents::Init(in_v),
source

pub exec fn read(self, verus_tmp_perm: Tracked<&PointsTo<V>>) -> V
where V: Copy,

requires
perm.pptr() == self,
perm.is_init(),
ensures
out_v == perm.value(),

Trait Implementations§

source§

impl<V> Clone for PPtr<V>

source§

exec fn clone(&self) -> Self

ensures
res == *self,
1.0.0 · source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
source§

impl<V> Copy for PPtr<V>

Auto Trait Implementations§

§

impl<V> RefUnwindSafe for PPtr<V>
where V: RefUnwindSafe,

§

impl<V> Send for PPtr<V>
where V: Send,

§

impl<V> Sync for PPtr<V>
where V: Sync,

§

impl<V> Unpin for PPtr<V>
where V: Unpin,

§

impl<V> UnwindSafe for PPtr<V>
where V: UnwindSafe,

Blanket Implementations§

source§

impl<T> Any for T
where T: 'static + ?Sized,

source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
source§

impl<T> Borrow<T> for T
where T: ?Sized,

source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
source§

impl<T> From<T> for T

source§

fn from(t: T) -> T

Returns the argument unchanged.

source§

impl<T, U> Into<U> for T
where U: From<T>,

source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

source§

impl<T> ToOwned for T
where T: Clone,

§

type Owned = T

The resulting type after obtaining ownership.
source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

§

type Error = Infallible

The type returned in the event of a conversion error.
source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.