Merge #222
222: Add the Allowed type to libtock_platform. r=hudson-ayers a=jrvanwhy
Allowed is the type that will be returned by `Platform::allow()` in the successful case. It represents a memory buffer shared with the kernel. It uses raw pointers and volatile writes to perform read/write accesses to the memory to avoid encountering undefined behavior.
This is the first step towards fixing #129 and #143.
Co-authored-by: Johnathan Van Why <jrvanwhy@google.com>
diff --git a/core/platform/src/allows/allow_readable.rs b/core/platform/src/allows/allow_readable.rs
new file mode 100644
index 0000000..c35701b
--- /dev/null
+++ b/core/platform/src/allows/allow_readable.rs
@@ -0,0 +1,18 @@
+/// Because the kernel may modify shared memory and place any bit pattern into
+/// that memory, we can only read types out of shared memory if every bit
+/// pattern is a valid value of that type. `AllowReadable` types are safe to
+/// read out of a shared buffer.
+pub unsafe trait AllowReadable {}
+
+unsafe impl AllowReadable for i8 {}
+unsafe impl AllowReadable for i16 {}
+unsafe impl AllowReadable for i32 {}
+unsafe impl AllowReadable for i64 {}
+unsafe impl AllowReadable for i128 {}
+unsafe impl AllowReadable for isize {}
+unsafe impl AllowReadable for u8 {}
+unsafe impl AllowReadable for u16 {}
+unsafe impl AllowReadable for u32 {}
+unsafe impl AllowReadable for u64 {}
+unsafe impl AllowReadable for u128 {}
+unsafe impl AllowReadable for usize {}
diff --git a/core/platform/src/allows/allowed.rs b/core/platform/src/allows/allowed.rs
new file mode 100644
index 0000000..4d66c2b
--- /dev/null
+++ b/core/platform/src/allows/allowed.rs
@@ -0,0 +1,85 @@
+/// An individual value that has been shared with the kernel using the `allow`
+/// system call.
+// Allowed's implementation does not directly use the 'b lifetime. Platform uses
+// 'b to prevent the Allowed from accessing the buffer after the buffer becomes
+// invalid.
+// Allowed requires T to be Copy due to concerns about the semantics of
+// non-copyable types in shared memory as well as concerns about unexpected
+// behavior with Drop types. See the following PR discussion for more
+// information: https://github.com/tock/libtock-rs/pull/222
+pub struct Allowed<'b, T: Copy + 'b> {
+ // Safety properties:
+ // 1. `buffer` remains valid and usable for the lifetime of this Allowed
+ // instance.
+ // 2. Read and write accesses of `buffer`'s pointee must be performed as a
+ // volatile operation, as the kernel may mutate buffer's pointee at any
+ // time.
+ // 3. The value `buffer` points to may have an arbitrary bit pattern in
+ // it, so reading from `buffer` is only safe if the type contained
+ // within is AllowReadable.
+ buffer: core::ptr::NonNull<T>,
+
+ // We need to use the 'b lifetime in Allowed to prevent an "unused lifetime"
+ // compiler error. We use it here. Note that the phantom data must be an
+ // &mut rather than a shared reference in order to make Allowed invariant
+ // with respect to T. Invariance is required because Allowed allows us to
+ // mutate the value in buffer, and invariance is the required property to do
+ // so without allowing callers to produce dangling references. This is
+ // documented at https://doc.rust-lang.org/nomicon/subtyping.html.
+ _phantom: core::marker::PhantomData<&'b mut T>,
+}
+
+// Allowed's API is based on that of core::cell::Cell, but removes some methods
+// that are not safe for use with shared memory.
+//
+// Internally, Allowed performs accesses to the shared memory using volatile
+// reads and writes through raw pointers. We avoid constructing references to
+// shared memory because that leads to undefined behavior (there is some
+// background on this in the following discussion:
+// https://github.com/rust-lang/unsafe-code-guidelines/issues/33). Tock runs on
+// single-threaded platforms, some of which lack atomic instructions, so we only
+// need to be able to deconflict races between the kernel (which will never
+// interrupt an instruction's execution) and this process. Therefore volatile
+// accesses are sufficient to deconflict races.
+impl<'b, T: Copy + 'b> Allowed<'b, T> {
+ // Allowed can only be constructed by the Platform. It is constructed after
+ // the `allow` system call, and as such must accept a raw pointer rather
+ // than a reference. The caller must make sure the following are true:
+ // 1. buffer points to a valid instance of type T
+ // 2. There are no references to buffer's pointee
+ // 3. buffer remains usable until the Allowed's lifetime has ended.
+ #[allow(dead_code)] // TODO: Remove when Platform is implemented
+ pub(crate) unsafe fn new(buffer: core::ptr::NonNull<T>) -> Allowed<'b, T> {
+ Allowed {
+ buffer,
+ _phantom: core::marker::PhantomData,
+ }
+ }
+
+ // Sets the value in the buffer.
+ pub fn set(&self, value: T) {
+ unsafe {
+ core::ptr::write_volatile(self.buffer.as_ptr(), value);
+ }
+ }
+}
+
+impl<'b, T: crate::AllowReadable + Copy + 'b> Allowed<'b, T> {
+ pub fn replace(&self, value: T) -> T {
+ let current = unsafe { core::ptr::read_volatile(self.buffer.as_ptr()) };
+ unsafe {
+ core::ptr::write_volatile(self.buffer.as_ptr(), value);
+ }
+ current
+ }
+
+ pub fn get(&self) -> T {
+ unsafe { core::ptr::read_volatile(self.buffer.as_ptr()) }
+ }
+}
+
+impl<'b, T: crate::AllowReadable + Copy + Default + 'b> Allowed<'b, T> {
+ pub fn take(&self) -> T {
+ self.replace(T::default())
+ }
+}
diff --git a/core/platform/src/allows/allowed_tests.rs b/core/platform/src/allows/allowed_tests.rs
new file mode 100644
index 0000000..f1f0c08
--- /dev/null
+++ b/core/platform/src/allows/allowed_tests.rs
@@ -0,0 +1,124 @@
+use crate::Allowed;
+use core::marker::PhantomData;
+use core::ptr::NonNull;
+
+// How do we simulate accesses to the shared buffer by the kernel?
+//
+// Well, a naive way would be to mutate the `buffer` variable directly. Because
+// Allowed accesses the memory through a *mut pointer, such a test would compile
+// and run fine with the current Rust compiler. As far as I can tell, it would
+// not hit any behavior documented as undefined at
+// https://doc.rust-lang.org/stable/reference/behavior-considered-undefined.html,
+// nor would it cause rustc to generate LLVM bitcode that encounters undefined
+// behavior.
+//
+// However, the naive approach will throw an "undefined behavior" error when run
+// under Miri (e.g. with `cargo miri test`), which uses the stacked borrows
+// model [1]. In particular, accessing the `buffer` variable directly pops the
+// SharedRW off buffer's borrow stack, which prevents Allowed from using its
+// *mut pointer to access `buffer` afterwards. It is likely that Rust will adopt
+// the stacked borrows model as its formal model for borrow validity, and in
+// that case accessing `buffer` in that manner will become undefined behavior.
+// In addition, running these tests under Miri is highly valuable, as this is
+// tricky code to get correct and an unsound API may be hard to fix.
+//
+// Instead, we explicitly refer to buffer through use of a KernelPtr that
+// simulates the pointer that `allow()` would hand to the Tock kernel. As far as
+// the stacked borrows model is concerned, accesses through the KernelPtr
+// variable behave identically to mutations performed by the kernel. This
+// pattern allows us to use `cargo miri test` to not only execute the unit
+// tests, but to test whether Allowed would encounter undefined behavior when
+// interacting with a real Tock kernel.
+//
+// [1] https://plv.mpi-sws.org/rustbelt/stacked-borrows/paper.pdf
+struct KernelPtr<'b, T: Copy + 'b> {
+ ptr: NonNull<T>,
+
+ // We need to consume the 'b lifetime. This is very similar to Allowed's
+ // implementation.
+ _phantom: PhantomData<&'b mut T>,
+}
+
+impl<'b, T: Copy + 'b> KernelPtr<'b, T> {
+ // The constructor for KernelPtr; simulates allow(). Returns both the
+ // Allowed instance the Platform would return and a KernelPtr the test can
+ // use to simulate a kernel.
+ pub fn allow(buffer: &'b mut T) -> (Allowed<'b, T>, KernelPtr<'b, T>) {
+ let ptr = NonNull::new(buffer).unwrap();
+ // Discard buffer *without* creating a reference to it, as would be done
+ // if we called drop().
+ let _ = buffer;
+ // All 3 preconditions of Allowed::new are satisfied by the fact that
+ // `buffer` is directly derived from a &'b mut T.
+ let allowed = unsafe { Allowed::new(ptr) };
+ let kernel_ptr = KernelPtr {
+ ptr,
+ _phantom: PhantomData,
+ };
+ (allowed, kernel_ptr)
+ }
+
+ // Replaces the value in the buffer with a new one.
+ pub fn set(&self, value: T) {
+ unsafe {
+ core::ptr::write(self.ptr.as_ptr(), value);
+ }
+ }
+
+ // Copies the contained value out of the buffer.
+ pub fn get(&self) -> T {
+ unsafe { core::ptr::read(self.ptr.as_ptr()) }
+ }
+}
+
+#[test]
+fn set() {
+ let mut buffer = 1;
+ let (allowed, kernel_ptr) = KernelPtr::allow(&mut buffer);
+ assert_eq!(kernel_ptr.get(), 1);
+
+ // Simulate the kernel replacing the value in buffer.
+ kernel_ptr.set(2);
+ allowed.set(3);
+ assert_eq!(kernel_ptr.get(), 3);
+}
+
+#[test]
+fn replace() {
+ let mut buffer = 1;
+ let (allowed, kernel_ptr) = KernelPtr::allow(&mut buffer);
+ assert_eq!(kernel_ptr.get(), 1);
+
+ // Simulate the kernel replacing the value in buffer.
+ kernel_ptr.set(2);
+ let returned = allowed.replace(3);
+ assert_eq!(returned, 2);
+ assert_eq!(kernel_ptr.get(), 3);
+}
+
+#[test]
+fn get() {
+ let mut buffer = 1;
+ let (allowed, kernel_ptr) = KernelPtr::allow(&mut buffer);
+ assert_eq!(kernel_ptr.get(), 1);
+
+ assert_eq!(allowed.get(), 1);
+ assert_eq!(kernel_ptr.get(), 1);
+
+ kernel_ptr.set(2);
+ assert_eq!(allowed.get(), 2);
+ assert_eq!(kernel_ptr.get(), 2);
+}
+
+#[test]
+fn take() {
+ let mut buffer = 1;
+ let (allowed, kernel_ptr) = KernelPtr::allow(&mut buffer);
+ assert_eq!(kernel_ptr.get(), 1);
+
+ // Simulate the kernel replacing the value in buffer.
+ kernel_ptr.set(2);
+ let returned = allowed.take();
+ assert_eq!(returned, 2);
+ assert_eq!(kernel_ptr.get(), 0);
+}
diff --git a/core/platform/src/allows/mod.rs b/core/platform/src/allows/mod.rs
new file mode 100644
index 0000000..8cb2158
--- /dev/null
+++ b/core/platform/src/allows/mod.rs
@@ -0,0 +1,8 @@
+mod allow_readable;
+mod allowed;
+
+pub use allow_readable::AllowReadable;
+pub use allowed::Allowed;
+
+#[cfg(test)]
+mod allowed_tests;
diff --git a/core/platform/src/lib.rs b/core/platform/src/lib.rs
index 2e3dec6..7237a1d 100644
--- a/core/platform/src/lib.rs
+++ b/core/platform/src/lib.rs
@@ -8,8 +8,10 @@
// 3. A system call trait so that Platform works in both real Tock apps and
// unit test environments. [DONE]
+mod allows;
mod error_code;
mod syscalls;
+pub use allows::{AllowReadable, Allowed};
pub use error_code::ErrorCode;
pub use syscalls::{MemopNoArg, MemopWithArg, Syscalls};