blob: 1859dd1e2e0873ea8c43d0700c221644687fa4fa [file] [log] [blame]
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 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);
}