tock / libtock-rs

Rust userland library for Tock

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

`Pin<>`-based Allow and Subscribe API design

jrvanwhy opened this issue · comments

The problem with RAII

An obvious, but unsound, API for Read-Write Allow would look as follows:

struct AllowHandle<'b> {
    buffer: *mut [u8],
    _phantom: core::marker::PhantomData<&'b mut [u8]>,
    _pin: core::marker::PhantomPinned,
}

impl<'b> Drop for AllowHandle {
    fn drop(&mut self) {
        // Un-allow the buffer, and panic if it fails.
    }
}

fn allow_rw(buffer: &mut [u8]) -> Result<AllowHandle> {
    // Use Allow to share the buffer with the kernel, and create an AllowHandle to clean it up.
}

This API is unsound because the following use case allows the kernel to mutate memory after it has been reallocated for another purpose, causing undefined behavior:

fn main() {
    let mut buffer = [0; 4];
    let allow_handle = allow_rw(&mut buffer).unwrap();
    core::mem::forget(allow_handle);  // Prevent the un-allow from running.
    drop(buffer);  // Get rid of buffer, allow rustc to re-allocate it.
    // The kernel can still mutate the memory buffer was in here!
}

If you have not seen this issue before, I suggest reading through RFC 1066.

Pin<> to the rescue!

Fortunately, there is a case in which we can rely on destructors running! The core::pin::Pin type has a drop-execution guarantee. Creating a pinned type in safe code is tricky, but not impossible, and the pin_utils::pin_mut macro provides a way to pin data on the stack.

Although we will not need to create self-referential data structures, we can make use of Pin's Drop guarantee to make sure our destructor runs:

#[derive(Default)]
struct RwAllowBuffer<'b> {
    buffer: *mut [u8],
    _phantom: core::marker::PhantomData<&'b mut [u8]>,
    _pin: core::marker::PhantomPinned,
}

impl<'b> Drop for RwAllowBuffer<'b> {
    fn drop(&mut self) {
        if self.buffer.is_null() { return; }
        // Un-allow the buffer here, panicing on error.
    }
}

impl<'b> RwAllowBuffer<'b> {
    // Because we take self as a Pin, we know that Drop will be called before RwAllowBuffer's
    // memory is repurposed. This is guaranteed to happen before the 'b lifetime
    // expires, which is the safety property we need.
    pub allow(self: Pin<&mut Self>, buffer: &'b mut [u8]) -> Result<()> {
        // Do the allow here, setting buffer if it was successful.
    }
}

You would use this API as follows:

fn main() {
    let mut buffer = [0; 4];
    let allow_buffer = Default::default();
    pin_utils::pin_mut!(allow_buffer);
    allow_buffer.allow(&mut buffer);
    // Buffer is now shared. The buffer will be un-shared at the end of the
    // scope, when allow_buffer is dropped.
}

In later comments, I'll revise the design of this API, and expand it to Subscribe.

This seems an interesting solution, but it took me a while to fully understand the implications. As a less experienced Rust user, this would be my understanding:

If allow returns a handle, there is always the danger that users can call the core::mem::forget function that puts the kernel into an unsafe position. To avoid this, we want an allow function that consumes the allow buffer wrapper and makes sure that the drop function is called before the wrapper goes out of scope. This prevents users from calling core::mem::forget on the wrapper, as the wrapper was moved within the allow function.

Without the use of Pin<>, the wrapper would be dropped as soon as the allow function finishes, as the allow wrapper's memory would be moved into the allow function.

  pub fn allow(mut self, buffer: &'b mut [u8]) -> Result<(), ()> {
        // Do the allow here, setting buffer if it was successful.
        // allow_buffer will be dropped here
    }

fn use_allow () {
    let mut buffer = [0; 4];
    let allow_buffer = RwAllowBuffer::default();
    allow_buffer.allow(&mut buffer);
    // the kernel does not have any allowed buffer here, as it was unallowed (dropped) at the end of `allow`
}

Using Pin<> achieves the goal of moving ownership of the allowed wrapper so that users cannot call drop on it, but the memory location of the wrapper's reference is still pinned to the calling function's stack (in this example the function use_allow). This will make the compiler drop the allow wrapper at the end of the calling function.

  pub fn allow_pin(mut self: Pin<&mut Self>, buffer: &'b mut [u8]) -> Result<(), ()> {
        // Do the allow here, setting buffer if it was successful.
        println! ("pin allow_buffer {:p}", &self.buffer);
        self.buffer = buffer.as_mut_ptr();
        Ok(())
    }

fn use_allow () {
    let mut buffer = [0; 4];
    let allow_buffer = RwAllowBuffer::default();
    pin_utils::pin_mut!(allow_buffer);
    allow_buffer.allow(&mut buffer);
    // the kernel has an allowed buffer here
    // allow_buffer will be dropped here
}

Did I understand this correctly?

While your conclusion is correct, some of the explanation is not quite correct. Using Pin<&mut RwAllowBuffer> does not cause an ownership transfer into allow(); allow() is still called by reference. Instead, it makes the calling function (use_allow) promise that it will run the destructor at the correct time.

I have been playing around with this concept a little bit and I have some wire frame examples

An application main that wants to read the console with a timeout using "standard" libtock-rs drivers (which haven't been created yet):

pub fn main() -> TockResult<()> {
    let console = Console {};

    let mut buffer = [0u8; 64];

    let timeout = Timeout::new_ms(5_000);
    pin_mut!(timeout);
    timeout.as_ref().start()?;

    let canceled = console.read_while(&mut buffer, &mut move || !timeout.expired())?;
    if !canceled {
        console.write(&buffer)?;
    }

    Ok(())
}

Console write frame:

struct Console {}

impl Console {
    fn read_while(
        &self,
        buffer: &mut [u8],
        keep_going: &mut dyn FnMut() -> bool,
    ) -> TockResult<bool> {
        const DRIVER_NUM: usize = 0;
        const ALLOW_NUM: usize = 0;
        const SUB_NUM: usize = 0;
        const CMD_NUM: usize = 0;

        let buf_share = AllowRWContext::new(DRIVER_NUM, ALLOW_NUM, buffer);
        pin_mut!(buf_share);
        buf_share.as_ref().share()?;

        let callback = SubscribeContext::new(DRIVER_NUM, SUB_NUM);
        pin_mut!(callback);
        callback.as_ref().subscribe()?;

        syscalls::command_ignore_val(DRIVER_NUM, CMD_NUM, 0, 0)?;

        return loop {
            if callback.complete() {
                break Ok(true);
            } else if !keep_going() {
                break Ok(false);
            }
            unsafe { syscalls::raw::yieldk() };
        };
    }
}

Timeout wire frame

struct Timeout {
    timeout_ms: usize,
    subscribe_context: SubscribeContext,
}

impl Timeout {
    fn new_ms(timeout_ms: usize) -> Self {
        const DRIVER_NUM: usize = 0;
        const ALLOW_NUM: usize = 0;

        Self {
            timeout_ms,
            subscribe_context: SubscribeContext::new(DRIVER_NUM, ALLOW_NUM),
        }
    }

    fn start(self: Pin<&Self>) -> TockResult<()> {
        const DRIVER_NUM: usize = 0;
        const ALLOW_NUM: usize = 0;
        const SUB_NUM: usize = 0;
        const CMD_NUM: usize = 0;

        // Safe since subscribe_context field is pinned when self is pinned
        unsafe { self.map_unchecked(|s| &s.subscribe_context) }.subscribe()?;

        syscalls::command_ignore_val(DRIVER_NUM, CMD_NUM, self.timeout_ms, 0)?;
        Ok(())
    }

    fn expired(&self) -> bool {
        self.subscribe_context.complete()
    }
}

The SubscribeContext and AllowRWContext are basically the RAII the first comment mentions. They are very similar, and I have included one for brevity.

struct SubscribeContext {
    driver_num: usize,
    sub_num: usize,
    subscribed: Cell<bool>,
    called: UnsafeCell<bool>,
    _pin: PhantomPinned, // disallow unpin. Must use unsafe code to create pin (e.g. pin_mut!)
}

impl SubscribeContext {
    fn new(driver_num: usize, sub_num: usize) -> Self {
        Self {
            driver_num,
            sub_num,
            subscribed: Cell::new(false),
            called: UnsafeCell::new(false),
            _pin: PhantomPinned,
        }
    }

    fn subscribe(self: Pin<&Self>) -> TockResult<()> {
        extern "C" fn callback(_arg1: usize, _arg2: usize, _arg3: usize, called: usize) {
            unsafe { core::ptr::write_volatile(called as *mut _, true) };
        }

        syscalls::subscribe_fn(
            self.driver_num,
            self.sub_num,
            callback,
            self.called.get() as usize,
        )?;
        self.subscribed.set(true);

        Ok(())
    }

    fn complete(&self) -> bool {
        // Safe since pointer is well aligned and a bit-wise copiable 
        unsafe { self.called.get().read() }
    }
}

The "standard" driver for Timeout did have to use unsafe to get a Pinned version of the SubscribeContext member field. Any driver that only exposes synchronous APIs (which Timeout is not), wouldn't have to use unsafe.

I have the rest of the code locally if anyone is interested, but I was trying to keep it brief :)

Overall that looks reasonable. My main concern is this:

The "standard" driver for Timeout did have to use unsafe to get a Pinned version of the SubscribeContext member field. Any driver that only exposes synchronous APIs (which Timeout is not), wouldn't have to use unsafe.

Most drivers will be asynchronous, so I'd prefer a design that doesn't require asynchronous drivers to use unsafe.

However, I think this is solvable by making the caller provide the SubscribeContext.

While your conclusion is correct, some of the explanation is not quite correct. Using Pin<&mut RwAllowBuffer> does not cause an ownership transfer into allow(); allow() is still called by reference. Instead, it makes the calling function (use_allow) promise that it will run the destructor at the correct time.

I was referring to the moving of Pin<&mut RwAllowBuffer>.

Instead of this design, I've implemented the design from #341 in libtock-rs. Closing, as I don't think we have a need to iterate on this design any further.