Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

phil-levis
Copy link
Contributor

@phil-levis phil-levis commented Jun 8, 2021

Pull Request Overview

This pull request grew out of #2590.

It does a bunch of cleanup on TRD104. The most notable change is a clearer definition of the expectations on allowed buffers. The prior text said that userspace cannot access a shared buffer. These changes update that text to be clearer and also state the assumptions that the kernel can make. Specifically:

  1. A userspace API MUST NOT depend on or assume userspace accessing an allowed buffer.
  2. The kernel MUST NOT assume that userspace does not access a buffer.

The first wording captures the intent. We cannot state that userspace MUST NOT access the buffer because there is no reasonable way to check or enforce this either through software or code review: a userspace bug could cause it to inadvertently access a buffer. The point is not that userspace APIs should not be designed such that they expect or use concurrent access. Therefore, if we in the future use an MPU to protect an allowed buffer we won't break userspace APIs.

The second wording captures that buffers can disappear unexpectedly. While userspace shouldn't be touching shared buffers, those buffers can change at any time. Userspace might revoke the buffer through another allow, or the process might exit (removing access to the buffer).

After a lot of discussion back and forth about concurrent kernel/user access to allowed buffers, we concluded this should be a different system call, which we will specify in a separate document.

Testing Strategy

This pull request was tested by a huge number of discussions and debates.

TODO or Help Wanted

This pull request still needs lots more careful reads.

Documentation Updated

  • Updated the relevant files in /docs, or no updates are required.

Formatting

  • Ran make prepush.

@phil-levis phil-levis requested a review from lschuermann June 8, 2021 17:55
@hudson-ayers hudson-ayers mentioned this pull request Jun 8, 2021
2 tasks
alistair23
alistair23 previously approved these changes Jun 9, 2021
Copy link
Member

@lschuermann lschuermann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these changes look good! One minor question though. Never mind, turns out it makes sense! 😄

lschuermann
lschuermann previously approved these changes Jun 9, 2021
hudson-ayers
hudson-ayers previously approved these changes Jun 9, 2021
Copy link
Contributor

@hudson-ayers hudson-ayers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, I have one (optional) suggestion

@jrvanwhy
Copy link
Contributor

Leon asked me a question that made me realize that I have not fully thought through libtock-rs' Allow API. To date, I have spent most of my time thinking about Read-Write Allow, and have assumed that Read-Only Allow is strictly easier. Unfortunately, I now realize that Read-Only Allow has its own challenge.

The Challenge

To start, consider what a naive Allow API would look like in libtock-rs:

fn allow_ro_static(driver_id: u32, buffer_id: u32, new_buffer: &'static [u8])
    -> Result<&'static [u8], (ErrorCode, &'static [u8])>;

fn allow_rw_static(driver_id: u32, buffer_id: u32, new_buffer: &'static mut [u8])
    -> Result<&'static mut [u8], (ErrorCode, &'static mut [u8])>;

allow_rw_static looks good. It takes a &mut reference, which guarantees that there is no other reference to the buffer. It returns a &mut reference so that the code that owns the buffer can re-use it. That code would use a type similar to TakeCell to store the buffer, checking availability at runtime.

allow_ro_static is problematic, however. It is supposed to support buffers that are written by the app and read by the kernel -- but you can't build a TakeCell-like tool to track mutable and immutable access. If you try, you end up with something unsound:

impl TakeCell {
    // Retrieves a mutable reference if no other reference exists.
    fn take_mut(&self) -> Option<&'static mut [u8]>;

    // Returns the mutable reference.
    fn return_mut(&self, val: &'static mut [u8]);

    // Retrieves a shared reference if no other reference exists.
    fn take_ref(&self) -> Option<&'static [u8]>;

    // Returns the shared reference
    fn return_ref(&self, val: &'static [u8]);
}

fn main() {
    let mut variable = [0];
    let take_cell = TakeCell::new(&mut variable);
    let ref1: &[u8] = take_cell.take_ref().unwrap();
    let ref2: &[u8] = ref1; // Copies the reference.
    take_cell.return_ref(ref1);
    let mut_ref: &mut [u8] = take_cell.take_mut().unwrap();
    // mut_ref and ref2 are both alive here, which is undefined behavior
}

The problem is that & references are Copy, so there's no way for RoTakeCell to determine that every shared reference has been returned.

One apparent solution is to have allow_ro_static take a &mut reference instead for uniqueness:

fn allow_ro_static(driver_id: u32, buffer_id: u32, new_buffer: &'static mut [u8])
    -> Result<&'static mut [u8], (ErrorCode, &'static mut [u8])>;

fn allow_rw_static(driver_id: u32, buffer_id: u32, new_buffer: &'static mut [u8])
    -> Result<&'static mut [u8], (ErrorCode, &'static mut [u8])>;

but now allow_ro_static doesn't work with references that are pointing to read-only memory (i.e. truly immutable variables). The same result happens if we try to use &[Cell<u8>] instead: we can't create a [Cell<u8>] in readonly memory, forcing everything into RAM. One of the primary motivations for Read-Only Allow was to keep read-only buffers in immutable storage, so this is a deal-breaker.

Idea: Move-Only Immutable Reference

Instead of using & references for Read-Only Allow, we can introduce our own reference type that is equivalent to & except it is non-copyable:

struct AllowRef { slice: core::ptr::NonNull<[u8]> }

impl std::ops::Deref for AllowRef {
    type Target = [u8];
    fn deref(&self) -> &[u8] { /* omitted */ }
}

Then we make Read-Only Allow use AllowRef:

fn allow_ro_static(driver_id: u32, buffer_id: u32, new_buffer: AllowRef)
    -> Result<AllowRef, (ErrorCode, AllowRef)>;

fn allow_rw_static(driver_id: u32, buffer_id: u32, new_buffer: &'static mut [u8])
    -> Result<&'static mut [u8], (ErrorCode, &'static mut [u8])>;

Then we need two TakeCell-like types: one for buffers in read-only memory, and a second for buffers in read-write memory. The read-only version can return multiple AllowRefs, while the read-write one can return at most one:

struct RoAllowBuffer<const LEN: usize> { buffer: [u8; LEN] }

impl<const LEN: usize> RoAllowBuffer<LEN> {
    // Does not need to perform runtime checks, as there can't be unique references to the buffer
    pub fn get(&self) -> AllowRef { /* omitted */ }
}

struct RwAllowBuffer<const LEN: usize> {
    borrowed: Cell<bool>,
    buffer: UnsafeCell<[u8; LEN]>,
}

impl<const LEN: usize> RwAllowBuffer<LEN> {
    // Returns a reference if not already borrowed.
    pub fn get_mut(&self) -> Option<&'static mut [u8]> { /* omitted */ }

    // Undoes the borrow. Must check that buffer == self.buffer at runtime.
    pub fn return_mut(&self, buffer: &'static mut [u8]) { /* omitted */ }

    // Returns a read-only reference if not already borrowed.
    pub fn get(&self) -> Option<AllowRef> { /* omitted */ }

    // Undoes the borrow. Must check that buffer == self.buffer at runtime.
    pub fn return_ref(&self, buffer: AllowRef) { /* omitted */ }
}

As an alternative, we could modify RwAllowBuffer to support multiple
simultaneous immutable borrows by having it track the number of AllowRefs
present.

Other options?

Every other sound Allow API I can think of has considerably more overhead, so I think this is what libtock-rs' Allow API will need to look like.

For @phil-levis and @lschuermann: I do not think this impacts the TRD. :-)

@phil-levis phil-levis dismissed stale reviews from hudson-ayers, lschuermann, and alistair23 via 3a40f00 June 14, 2021 23:02
multiple times and it's up to the kernel to make sure this
doesn't cause a problem.
@phil-levis
Copy link
Contributor Author

OK, I have rewritten the text so that it does not require the kernel to return INVALID on an overlapping Read-Write Allow call, and that the kernel has be safe if/when this happens.

@phil-levis phil-levis requested a review from lschuermann June 16, 2021 00:15
Copy link
Member

@lschuermann lschuermann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me.

@lschuermann lschuermann added the last-call Final review period for a pull request. label Jun 16, 2021
@phil-levis
Copy link
Contributor Author

bors r+

@bors
Copy link
Contributor

bors bot commented Jun 16, 2021

@bors bors bot merged commit 8f66d98 into master Jun 16, 2021
@bors bors bot deleted the trd104-buffer-reading branch June 16, 2021 18:04
@lschuermann
Copy link
Member

Awesome! As documented in the TRD, the kernel's AppSlice infrastructure as of now does not match the specification. I'll create a PR proposal with the slice-of-cells based mechanism @hudson-ayers and I worked on, as proposed on the tock-dev ML-thread.

bradjc added a commit that referenced this pull request Jul 9, 2021
As of 8f66d98 ("Merge #2617"), it is explicitly permitted for
Tock applications to Allow overlapping memory regions to multiple
Allow slots in both the same and different capsules
simultaneously. This has the implication of making the current
interface for accessing process memory (`AppSlice`) unsound.

This commit rewrites the interface for interacting with process
memory, such that it is valid for the kernel to have multiple process
buffers which can be both writeable and overlapping. This is achieved
by developing an interface around `&[Cell<u8>]` (slice of Cells),
which does permit interior mutability due to the usage of
`UnsafeCell<u8>`.

It further adapts a consistent naming scheme:

- App -> Process, as potentially multiple processes of the same
  application can run simultaneously.

- AppSlice -> ProcessBuf, as this is not really a slice, but rather
  represents a bounded memory region (buffer) within some process
  memory.

- introduction of new `ProcessSlice`s, which are
  `#[repr(transparent)]` wrappers around slices of Cells, providing
  convenient APIs and, in the case of read-only Allowed memory,
  limiting to read-only accesses.

- introduction of a `ReadableProcessByte`, which is a
  `#[repr(transparent)]` wrapper around a `Cell<u8>`, limiting to
  read-only accesses.

In general, this manages to provide a convenient interface for
accessing userspace memory, while directly operating on Rust slices
with their inherent aliasing restrictions.

However, the `ProcessSlices` obtained are incompatible with regular
Rust slices, which sometimes makes copying over an intermediate buffer
necessary.

Usually, Rust slices only feature a `copy_from_slice` method, taking a
mutable `&mut self` reference. However, given that a Rust slice cannot
copy from a `ProcessSlice`, these feature an inverse method
`copy_to_slice`, which can write the contents to a regular Rust slice.

Signed-off-by: Leon Schuermann <[email protected]>
Co-authored-by: Pat Pannuto <[email protected]>
Co-authored-by: Brad Campbell <[email protected]>
lschuermann added a commit to lschuermann/tock that referenced this pull request Jul 13, 2021
As of 8f66d98 ("Merge tock#2617"), it is explicitly permitted for
Tock applications to Allow overlapping memory regions to multiple
Allow slots in both the same and different capsules
simultaneously. This has the implication of making the current
interface for accessing process memory (`AppSlice`) unsound.

This commit rewrites the interface for interacting with process
memory, such that it is valid for the kernel to have multiple process
buffers which can be both writeable and overlapping. This is achieved
by developing an interface around `&[Cell<u8>]` (slice of Cells),
which does permit interior mutability due to the usage of
`UnsafeCell<u8>`.

It further adapts a consistent naming scheme:

- App -> Process, as potentially multiple processes of the same
  application can run simultaneously.

- AppSlice -> ProcessBuffer, as this is not really a slice, but rather
  represents a bounded memory region (buffer) within some process
  memory.

- introduction of new `ProcessSlice`s, which are
  `#[repr(transparent)]` wrappers around slices of Cells, providing
  convenient APIs and, in the case of read-only Allowed memory,
  limiting to read-only accesses.

- introduction of a `ReadableProcessByte`, which is a
  `#[repr(transparent)]` wrapper around a `Cell<u8>`, limiting to
  read-only accesses.

In general, this manages to provide a convenient interface for
accessing userspace memory, while directly operating on Rust slices
with their inherent aliasing restrictions.

However, the `ProcessSlices` obtained are incompatible with regular
Rust slices, which sometimes makes copying over an intermediate buffer
necessary.

Usually, Rust slices only feature a `copy_from_slice` method, taking a
mutable `&mut self` reference. However, given that a Rust slice cannot
copy from a `ProcessSlice`, these feature an inverse method
`copy_to_slice`, which can write the contents to a regular Rust slice.

Signed-off-by: Leon Schuermann <[email protected]>
Co-authored-by: Pat Pannuto <[email protected]>
Co-authored-by: Brad Campbell <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation last-call Final review period for a pull request.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants