Skip to content

Commit b13550b

Browse files
bors[bot]jrvanwhy
andauthored
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 <[email protected]>
2 parents 704272c + 17513eb commit b13550b

File tree

5 files changed

+237
-0
lines changed

5 files changed

+237
-0
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/// Because the kernel may modify shared memory and place any bit pattern into
2+
/// that memory, we can only read types out of shared memory if every bit
3+
/// pattern is a valid value of that type. `AllowReadable` types are safe to
4+
/// read out of a shared buffer.
5+
pub unsafe trait AllowReadable {}
6+
7+
unsafe impl AllowReadable for i8 {}
8+
unsafe impl AllowReadable for i16 {}
9+
unsafe impl AllowReadable for i32 {}
10+
unsafe impl AllowReadable for i64 {}
11+
unsafe impl AllowReadable for i128 {}
12+
unsafe impl AllowReadable for isize {}
13+
unsafe impl AllowReadable for u8 {}
14+
unsafe impl AllowReadable for u16 {}
15+
unsafe impl AllowReadable for u32 {}
16+
unsafe impl AllowReadable for u64 {}
17+
unsafe impl AllowReadable for u128 {}
18+
unsafe impl AllowReadable for usize {}

core/platform/src/allows/allowed.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/// An individual value that has been shared with the kernel using the `allow`
2+
/// system call.
3+
// Allowed's implementation does not directly use the 'b lifetime. Platform uses
4+
// 'b to prevent the Allowed from accessing the buffer after the buffer becomes
5+
// invalid.
6+
// Allowed requires T to be Copy due to concerns about the semantics of
7+
// non-copyable types in shared memory as well as concerns about unexpected
8+
// behavior with Drop types. See the following PR discussion for more
9+
// information: https://github.com/tock/libtock-rs/pull/222
10+
pub struct Allowed<'b, T: Copy + 'b> {
11+
// Safety properties:
12+
// 1. `buffer` remains valid and usable for the lifetime of this Allowed
13+
// instance.
14+
// 2. Read and write accesses of `buffer`'s pointee must be performed as a
15+
// volatile operation, as the kernel may mutate buffer's pointee at any
16+
// time.
17+
// 3. The value `buffer` points to may have an arbitrary bit pattern in
18+
// it, so reading from `buffer` is only safe if the type contained
19+
// within is AllowReadable.
20+
buffer: core::ptr::NonNull<T>,
21+
22+
// We need to use the 'b lifetime in Allowed to prevent an "unused lifetime"
23+
// compiler error. We use it here. Note that the phantom data must be an
24+
// &mut rather than a shared reference in order to make Allowed invariant
25+
// with respect to T. Invariance is required because Allowed allows us to
26+
// mutate the value in buffer, and invariance is the required property to do
27+
// so without allowing callers to produce dangling references. This is
28+
// documented at https://doc.rust-lang.org/nomicon/subtyping.html.
29+
_phantom: core::marker::PhantomData<&'b mut T>,
30+
}
31+
32+
// Allowed's API is based on that of core::cell::Cell, but removes some methods
33+
// that are not safe for use with shared memory.
34+
//
35+
// Internally, Allowed performs accesses to the shared memory using volatile
36+
// reads and writes through raw pointers. We avoid constructing references to
37+
// shared memory because that leads to undefined behavior (there is some
38+
// background on this in the following discussion:
39+
// https://github.com/rust-lang/unsafe-code-guidelines/issues/33). Tock runs on
40+
// single-threaded platforms, some of which lack atomic instructions, so we only
41+
// need to be able to deconflict races between the kernel (which will never
42+
// interrupt an instruction's execution) and this process. Therefore volatile
43+
// accesses are sufficient to deconflict races.
44+
impl<'b, T: Copy + 'b> Allowed<'b, T> {
45+
// Allowed can only be constructed by the Platform. It is constructed after
46+
// the `allow` system call, and as such must accept a raw pointer rather
47+
// than a reference. The caller must make sure the following are true:
48+
// 1. buffer points to a valid instance of type T
49+
// 2. There are no references to buffer's pointee
50+
// 3. buffer remains usable until the Allowed's lifetime has ended.
51+
#[allow(dead_code)] // TODO: Remove when Platform is implemented
52+
pub(crate) unsafe fn new(buffer: core::ptr::NonNull<T>) -> Allowed<'b, T> {
53+
Allowed {
54+
buffer,
55+
_phantom: core::marker::PhantomData,
56+
}
57+
}
58+
59+
// Sets the value in the buffer.
60+
pub fn set(&self, value: T) {
61+
unsafe {
62+
core::ptr::write_volatile(self.buffer.as_ptr(), value);
63+
}
64+
}
65+
}
66+
67+
impl<'b, T: crate::AllowReadable + Copy + 'b> Allowed<'b, T> {
68+
pub fn replace(&self, value: T) -> T {
69+
let current = unsafe { core::ptr::read_volatile(self.buffer.as_ptr()) };
70+
unsafe {
71+
core::ptr::write_volatile(self.buffer.as_ptr(), value);
72+
}
73+
current
74+
}
75+
76+
pub fn get(&self) -> T {
77+
unsafe { core::ptr::read_volatile(self.buffer.as_ptr()) }
78+
}
79+
}
80+
81+
impl<'b, T: crate::AllowReadable + Copy + Default + 'b> Allowed<'b, T> {
82+
pub fn take(&self) -> T {
83+
self.replace(T::default())
84+
}
85+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
use crate::Allowed;
2+
use core::marker::PhantomData;
3+
use core::ptr::NonNull;
4+
5+
// How do we simulate accesses to the shared buffer by the kernel?
6+
//
7+
// Well, a naive way would be to mutate the `buffer` variable directly. Because
8+
// Allowed accesses the memory through a *mut pointer, such a test would compile
9+
// and run fine with the current Rust compiler. As far as I can tell, it would
10+
// not hit any behavior documented as undefined at
11+
// https://doc.rust-lang.org/stable/reference/behavior-considered-undefined.html,
12+
// nor would it cause rustc to generate LLVM bitcode that encounters undefined
13+
// behavior.
14+
//
15+
// However, the naive approach will throw an "undefined behavior" error when run
16+
// under Miri (e.g. with `cargo miri test`), which uses the stacked borrows
17+
// model [1]. In particular, accessing the `buffer` variable directly pops the
18+
// SharedRW off buffer's borrow stack, which prevents Allowed from using its
19+
// *mut pointer to access `buffer` afterwards. It is likely that Rust will adopt
20+
// the stacked borrows model as its formal model for borrow validity, and in
21+
// that case accessing `buffer` in that manner will become undefined behavior.
22+
// In addition, running these tests under Miri is highly valuable, as this is
23+
// tricky code to get correct and an unsound API may be hard to fix.
24+
//
25+
// Instead, we explicitly refer to buffer through use of a KernelPtr that
26+
// simulates the pointer that `allow()` would hand to the Tock kernel. As far as
27+
// the stacked borrows model is concerned, accesses through the KernelPtr
28+
// variable behave identically to mutations performed by the kernel. This
29+
// pattern allows us to use `cargo miri test` to not only execute the unit
30+
// tests, but to test whether Allowed would encounter undefined behavior when
31+
// interacting with a real Tock kernel.
32+
//
33+
// [1] https://plv.mpi-sws.org/rustbelt/stacked-borrows/paper.pdf
34+
struct KernelPtr<'b, T: Copy + 'b> {
35+
ptr: NonNull<T>,
36+
37+
// We need to consume the 'b lifetime. This is very similar to Allowed's
38+
// implementation.
39+
_phantom: PhantomData<&'b mut T>,
40+
}
41+
42+
impl<'b, T: Copy + 'b> KernelPtr<'b, T> {
43+
// The constructor for KernelPtr; simulates allow(). Returns both the
44+
// Allowed instance the Platform would return and a KernelPtr the test can
45+
// use to simulate a kernel.
46+
pub fn allow(buffer: &'b mut T) -> (Allowed<'b, T>, KernelPtr<'b, T>) {
47+
let ptr = NonNull::new(buffer).unwrap();
48+
// Discard buffer *without* creating a reference to it, as would be done
49+
// if we called drop().
50+
let _ = buffer;
51+
// All 3 preconditions of Allowed::new are satisfied by the fact that
52+
// `buffer` is directly derived from a &'b mut T.
53+
let allowed = unsafe { Allowed::new(ptr) };
54+
let kernel_ptr = KernelPtr {
55+
ptr,
56+
_phantom: PhantomData,
57+
};
58+
(allowed, kernel_ptr)
59+
}
60+
61+
// Replaces the value in the buffer with a new one.
62+
pub fn set(&self, value: T) {
63+
unsafe {
64+
core::ptr::write(self.ptr.as_ptr(), value);
65+
}
66+
}
67+
68+
// Copies the contained value out of the buffer.
69+
pub fn get(&self) -> T {
70+
unsafe { core::ptr::read(self.ptr.as_ptr()) }
71+
}
72+
}
73+
74+
#[test]
75+
fn set() {
76+
let mut buffer = 1;
77+
let (allowed, kernel_ptr) = KernelPtr::allow(&mut buffer);
78+
assert_eq!(kernel_ptr.get(), 1);
79+
80+
// Simulate the kernel replacing the value in buffer.
81+
kernel_ptr.set(2);
82+
allowed.set(3);
83+
assert_eq!(kernel_ptr.get(), 3);
84+
}
85+
86+
#[test]
87+
fn replace() {
88+
let mut buffer = 1;
89+
let (allowed, kernel_ptr) = KernelPtr::allow(&mut buffer);
90+
assert_eq!(kernel_ptr.get(), 1);
91+
92+
// Simulate the kernel replacing the value in buffer.
93+
kernel_ptr.set(2);
94+
let returned = allowed.replace(3);
95+
assert_eq!(returned, 2);
96+
assert_eq!(kernel_ptr.get(), 3);
97+
}
98+
99+
#[test]
100+
fn get() {
101+
let mut buffer = 1;
102+
let (allowed, kernel_ptr) = KernelPtr::allow(&mut buffer);
103+
assert_eq!(kernel_ptr.get(), 1);
104+
105+
assert_eq!(allowed.get(), 1);
106+
assert_eq!(kernel_ptr.get(), 1);
107+
108+
kernel_ptr.set(2);
109+
assert_eq!(allowed.get(), 2);
110+
assert_eq!(kernel_ptr.get(), 2);
111+
}
112+
113+
#[test]
114+
fn take() {
115+
let mut buffer = 1;
116+
let (allowed, kernel_ptr) = KernelPtr::allow(&mut buffer);
117+
assert_eq!(kernel_ptr.get(), 1);
118+
119+
// Simulate the kernel replacing the value in buffer.
120+
kernel_ptr.set(2);
121+
let returned = allowed.take();
122+
assert_eq!(returned, 2);
123+
assert_eq!(kernel_ptr.get(), 0);
124+
}

core/platform/src/allows/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
mod allow_readable;
2+
mod allowed;
3+
4+
pub use allow_readable::AllowReadable;
5+
pub use allowed::Allowed;
6+
7+
#[cfg(test)]
8+
mod allowed_tests;

core/platform/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
// 3. A system call trait so that Platform works in both real Tock apps and
99
// unit test environments. [DONE]
1010

11+
mod allows;
1112
mod error_code;
1213
mod syscalls;
1314

15+
pub use allows::{AllowReadable, Allowed};
1416
pub use error_code::ErrorCode;
1517
pub use syscalls::{MemopNoArg, MemopWithArg, Syscalls};

0 commit comments

Comments
 (0)