Merge branch 'master' into rm-allows
diff --git a/core/platform/src/async_traits.rs b/core/platform/src/async_traits.rs new file mode 100644 index 0000000..2853026 --- /dev/null +++ b/core/platform/src/async_traits.rs
@@ -0,0 +1,93 @@ +//! Traits for building lightweight asynchronous APIs. These traits lack the +//! dynamic capabilities of `core::future::Future`, but have much smaller code +//! size and RAM usage costs. +//! +//! Tock kernel developers will be familiar with the 2-phase pattern of +//! operation these traits support. Client code (code using an asynchronous +//! component) calls a function or method provided by the asynchronous component +//! to start an asynchronous operation. When the operation is complete, the +//! asynchronous component calls a callback defined using the `FreeCallback` +//! and/or `MethodCallback` traits. +//! +//! Note that asynchronous callbacks must only be called from within "callback +//! context" -- that is, within a kernel callback (registered using the +//! `subscribe` system call). To enforce that, the callback traits require the +//! `CallbackContext` type, which is only instantiated by an instance of the +//! `Syscalls` trait. +//! +//! There is no prohibition on a callback calling back into the asynchronous +//! component that called it. In other words, if component B calls +//! `<A as FreeCallback<Done>>::call(...)`, then `call()` can call back into B +//! to start a new operation. Asynchronous components should clean up their +//! state internally before calling callbacks so as to support reentrant calls +//! into themselves! +//! +//! In client code, prefer to implement `FreeCallback` instead of +//! `MethodCallback` when possible, as it is easier to pass a `FreeCallback` to +//! an asynchronous component. +//! +//! To bridge the gap between `FreeCallback` and `MethodCallback`, we also have +//! the `Locator` trait. `Locator` allows `FreeCallback` implementations to find +//! their global state, and also provides `FreeCallback` versions of callbacks +//! for types that implement `MethodCallback`. In general, `Locator` should be +//! implemented by `libtock_runtime::static_component!` (in real Tock apps) and +//! `libtock_unittest::test_component!` (in unit tests), rather than directly by +//! user code. +// TODO: At the time of this writing, neither `libtock_runtime` nor +// `libtock_unittest` are implemented. Remove this TODO when they are +// implemented. + +/// `FreeCallback` is the callback equivalent of a free function: it does not +/// have access to the client component's data. `FreeCallback` is used by +/// asynchronous components -- such as `Syscalls` -- which cannot efficiently +/// store a client reference. +pub trait FreeCallback<AsyncResponse> { + fn call(context: CallbackContext, response: AsyncResponse); +} + +/// `MethodCallback` is a callback method; it can access the client component's +/// data. Note that asynchronous components generally need to use interior +/// mutability to mutate data, as `MethodCallback` is designed under the +/// assumption that there are multiple references to most asynchronous +/// components at any given time. +pub trait MethodCallback<AsyncResponse> { + fn call(&self, context: CallbackContext, response: AsyncResponse); +} + +/// `Syscalls` instantiates a `CallbackContext` when a kernel callback is +/// called. The lifetime prevents the `CallbackContext` from being copied into +/// storage that outlives the callback. Code that is only safe to call from +/// callback context can request a `CallbackContext` argument. `CallbackContext` +/// is a zero-sized type so passing it around has no runtime cost. +#[derive(Clone, Copy)] +pub struct CallbackContext<'c> { + // `_phantom` serves three purposes. It uses the `c lifetime to avoid an + // "unused lifetime" error, it provides the proper variance over 'c, and it + // prevents code outside this crate from directly constructing a + // CallbackContext (because of its visibility control). Code outside this + // crate can copy a CallbackContext, but that is fine as copying the + // CallbackContext preserves its associated lifetime. + pub(crate) _phantom: core::marker::PhantomData<&'c ()>, +} + +/// Provides access to a global instance of type `Target`. Every call to +/// `locate()` on a given Locator type should return a reference to the same +/// instance of `Target`. An instance of `Locator` generally isn't instantiated +/// directly; instead, its type is passed to where it is needed via generic +/// arguments. +/// +/// For convenience, Locator provides a `FreeCallback` implementation for every +/// `MethodCallback` implementation that `Target` has. +pub trait Locator: 'static { + type Target; + fn locate() -> &'static Self::Target; +} + +impl<L: Locator, AsyncResponse> FreeCallback<AsyncResponse> for L +where + L::Target: MethodCallback<AsyncResponse>, +{ + fn call(context: CallbackContext, response: AsyncResponse) { + L::locate().call(context, response); + } +}
diff --git a/core/platform/src/lib.rs b/core/platform/src/lib.rs index 6a23dc5..0a9f936 100644 --- a/core/platform/src/lib.rs +++ b/core/platform/src/lib.rs
@@ -1,7 +1,12 @@ #![no_std] +mod async_traits; mod error_code; +mod raw_syscalls; mod syscalls; +mod syscalls_impl; +pub use async_traits::{CallbackContext, FreeCallback, Locator, MethodCallback}; pub use error_code::ErrorCode; -pub use syscalls::{MemopNoArg, MemopWithArg, Syscalls}; +pub use raw_syscalls::{OneArgMemop, RawSyscalls, YieldType, ZeroArgMemop}; +pub use syscalls::Syscalls;
diff --git a/core/platform/src/raw_syscalls.rs b/core/platform/src/raw_syscalls.rs new file mode 100644 index 0000000..90fc602 --- /dev/null +++ b/core/platform/src/raw_syscalls.rs
@@ -0,0 +1,239 @@ +// TODO: Implement `libtock_runtime` and `libtock_unittest`, which are +// referenced in the comment on `RawSyscalls`. + +/// `RawSyscalls` allows a fake Tock kernel to be injected into components for +/// unit testing. It is implemented by `libtock_runtime::TockSyscalls` and +/// `libtock_unittest::FakeSyscalls`. Components should not use `RawSyscalls` +/// directly; instead, use the `Syscalls` trait, which provides higher-level +/// interfaces to the system calls. + +// RawSyscalls is designed to minimize the amount of handwritten assembly code +// needed without generating unnecessary instructions. This comment describes +// the thought process that led to the choice of methods for RawSyscalls. There +// are a few major considerations affecting its design: +// +// 1. Most system calls only clobber r0-r4 (*), while yield has a far longer +// clobber list. As such, yield must have its own assembly +// implementation. +// 2. The compiler is unable to optimize away unused arguments. For example, +// memop's "get process RAM start address" operation only needs r0 set, +// while memop's "break" operation needs both r0 and r1 set. If our +// inline assembly calls "get process RAM start address" but sets both r0 +// and r1, the compiler doesn't know that r1 will be +// ignored so setting that register will not be optimized away. Therefore +// we want to set the minimum number of argument registers possible. +// 3. The cost of specifying unused return registers is only that of +// unnecessarily marking a register as clobbered. Explanation: After +// inlining, an unused register is marked as "changed by the assembly" +// but can immediately be re-used by the compiler, which is the same as a +// clobbered register. System calls should generally be +// inlined -- and even if they aren't, the unused return values will +// probably be passed in caller-saved registers (this is true for the C +// ABI, so probably true for the Rust ABI), which are treated as +// clobbered regardless. +// +// (*) When this file refers to registers, it uses the same naming convention as +// the Tock 2.0 syscalls TRD. Registers r0-r4 correspond to ARM registers r0-r4 +// and RISC-V registers a0-a4. +// +// Currently, yield takes exactly one argument (to specify what yield type to +// do). Therefore we only need one raw yield call. +// +// Based on these considerations, it would make sense to have the following +// methods: +// yield +// zero_arg_syscall +// one_arg_syscall +// two_arg_syscall +// three_arg_syscall +// four_arg_syscall +// +// However, there are no system calls that take 0 or 3 arguments, so we do not +// need the corresponding methods. This leaves yield, 1-arg, 2-arg, and 4-arg +// system calls. +// +// The 1-arg and 2-arg system calls are only used for memop. Memop currently has +// the property that none of its operations can lead to undefined behavior. +// Therefore, we can rename the 1-arg and 2-arg system calls to zero_arg_memop +// and one_arg_memop and make them safe methods (the argument counts change +// because the number of system call arguments is one greater than the number of +// arguments passed to the specific op). +// +// This only leaves four_arg_syscall, which is used to implement subscribe, +// command, read-write allow, and read-only allow. +// +// Therefore the final design has 4 methods: +// yield +// zero_arg_memop +// one_arg_memop +// four_arg_syscall +// +// If a new system call class that uses fewer than four arguments is added, then +// the above list will need to be revised. +// +// Note that `command` always needs to use four_arg_syscall, even when calling a +// command with fewer arguments, because we don't want to leak the +// (possibly secret) values in the r2 and r3 registers to untrusted capsules. +// Yield and memop do not have this concern and can leave arbitrary data in r2 +// and r3, because they are implemented by the core kernel, which is trusted. +// +// The success type for Memop calls depends on the operation performed. However, +// all *currently defined* memop operations return either Success or Success +// with u32. Therefore, the memop implementations only need to mark r0 and r1 as +// clobbered, not r2 and r3. This choice of clobbers will need to be revisited +// if and when a memop operation that returns more data is added. +// +// The decision of where to use u32 and usize can be a bit tricky. The Tock +// syscall ABI is currently only specified for 32-bit systems, so on real Tock +// systems both types match the size of a register, but the unit test +// environment can be either 32 bit or 64 bit. This interface uses usize for +// values that can contain pointers, so that pointers are not truncated in the +// unit test environment. To keep types as consistent as possible, it uses u32 +// for all values that cannot be pointers. +pub trait RawSyscalls { + // raw_yield should: + // 1. Call syscall class 0 + // 2. Use register r0 for input and output as an inlateout register, + // passing in r0_in and returning its value. + // 3. Mark all caller-saved registers as lateout clobbers. + // 4. NOT provide any of the following options: + // pure (yield has side effects) + // nomem (a callback can read + write globals) + // readonly (a callback can write globals) + // preserves_flags (a callback can change flags) + // noreturn (yield is expected to return) + // nostack (a callback needs the stack) + // + // Design note: This is safe because the yield types that currently exist + // are safe. If an unsafe yield type is added, we will need to make + // raw_yield unsafe. Although raw_yield shouldn't be called by code outside + // this crate, it can be, so that is a backwards-incompatible change. We + // pass YieldType rather than a usize because if we used usize directly then + // this API becomes unsound if the kernel adds support for an unsafe yield + // type (or even one that takes one more argument). + fn raw_yield(r0_in: YieldType) -> u32; + + // four_arg_syscall is used to invoke the subscribe, command, read-write + // allow, and read-only allow system calls. + // + // four_arg_syscall's inline assembly should have the following properties: + // 1. Calls the syscall class specified by class + // 2. Passes r0-r3 in the corresponding registers as inlateout + // registers. Returns r0-r3 in order. + // 3. Does not mark any registers as clobbered. + // 4. Has all of the following options: + // preserves_flags (these system calls do not touch flags) + // nostack (these system calls do not touch the stack) + // 5. Does NOT have any of the following options: + // pure (these system calls have side effects) + // nomem (the compiler needs to write to globals before allow) + // readonly (rw allow can modify memory) + // noreturn (all these system calls are expected to return) + // + // Note that subscribe's application data argument can potentially contain a + // pointer, so r3 can contain a pointer (in addition to r1 and r2, which + // more obviously contain pointers for subscribe and memop). + // + // For subscribe(), the callback pointer should be either 0 (for the null + // callback) or an `unsafe extern fn(u32, u32, u32, usize)`. + /// # Safety + /// `four_arg_syscall` must NOT be used to invoke yield. Otherwise, it has + /// the same safety invariants as the underlying system call, which varies + /// depending on the system call class. + unsafe fn four_arg_syscall( + r0: u32, + r1: u32, + r2: usize, + r3: usize, + class: u8, + ) -> (u32, usize, usize, usize); + + // zero_arg_memop is used to invoke memop operations that do not accept an + // argument register. Because there are no memop commands that set r2 or r3, + // this only needs to return r0 and r1. + // + // Memop commands may panic in the unit test environment, as not all memop + // calls can be sensibly implemented in that environment. + // + // zero_arg_memop's inline assembly should have the following properties: + // 1. Calls syscall class 5 + // 2. Specifies r0 as an inlateout register, and r1 as a lateout + // register. + // 3. Does not mark any registers as clobbered. + // 4. Has all of the following options: + // preserves_flags + // nostack + // nomem (it is okay for the compiler to cache globals + // across memop calls) + // 5. Does NOT have any of the following options: + // pure (two invocations of the same memop can return + // different values) + // readonly (incompatible with nomem) + // noreturn + // + // Design note: like raw_yield, this is safe because memops that currently + // exist are safe. zero_arg_memop takes a ZeroArgMemop rather than a u32 so + // that if the kernel adds an unsafe memop -- or one that can clobber r2/r3 + // -- this API doesn't become unsound. + fn zero_arg_memop(r0_in: ZeroArgMemop) -> (u32, usize); + + // one_arg_memop is used to invoke memop operations that take an argument. + // Because there are no memop operations that set r2 or r3, this only needs + // to return r0 and r1. + // + // one_arg_memop's inline assembly should: + // 1. Call syscall class 5 + // 2. Specify r0 and r1 as inlateout registers, and return (r0, r1) + // 3. Not mark any registers as clobbered. + // 4. Have all of the following options: + // preserves_flags + // nostack + // nomem (the compiler can cache globals across memop + // calls) + // 5. Does NOT have any of the following options: + // pure Two invocations of sbrk can return different values + // readonly Incompatible with nomem + // noreturn + // + // Design note: like raw_yield, this is safe because memops that currently + // exist are safe. zero_arg_memop takes a ZeroArgMemop rather than a u32 so + // that if the kernel adds an unsafe memop -- or one that can clobber r2/r3 + // -- this API doesn't become unsound. + fn one_arg_memop(r0_in: OneArgMemop, r1: usize) -> (u32, usize); +} + +#[non_exhaustive] +#[repr(u32)] +pub enum OneArgMemop { + Brk = 0, + Sbrk = 1, + FlashRegionStart = 8, + FlashRegionEnd = 9, + SpecifyStackTop = 10, + SpecifyHeapStart = 11, + // Note: before adding new memop operations, make sure the assumptions in + // the design notes on `one_arg_memop` are valid for the new operation type. +} + +// TODO: When the numeric values (0 and 1) are assigned to the yield types, +// specify those values here. +#[non_exhaustive] +#[repr(u32)] +pub enum YieldType { + Wait, + NoWait, +} + +#[non_exhaustive] +#[repr(u32)] +pub enum ZeroArgMemop { + MemoryStart = 2, + MemoryEnd = 3, + FlashStart = 4, + FlashEnd = 5, + GrantStart = 6, + FlashRegions = 7, + // Note: before adding new memop operations, make sure the assumptions in + // the design notes on `zero_arg_memop` are valid for the new operation + // type. +}
diff --git a/core/platform/src/syscalls.rs b/core/platform/src/syscalls.rs index 9a78169..8d85380 100644 --- a/core/platform/src/syscalls.rs +++ b/core/platform/src/syscalls.rs
@@ -1,99 +1,26 @@ -//! Provides the Syscalls trait which directly represents Tock's system call -//! APIs. Syscalls is implemented by both `libtock_runtime` which makes system -//! calls into a real Tock kernel, and `libtock_fake` which is a fake Tock -//! kernel. +// TODO: Implement `libtock_runtime` and `libtock_unittest`, which are +// referenced in the comment on `Syscalls`. -// TODO: Implement `libtock_runtime` and `libtock_fake`. +/// `Syscalls` provides safe abstractions over Tock's system calls. It is +/// implemented for `libtock_runtime::TockSyscalls` and +/// `libtock_unittest::FakeSyscalls` (by way of `RawSyscalls`). +pub trait Syscalls { + /// Puts the process to sleep until a callback becomes pending, invokes the + /// callback, then returns. + fn yield_wait(); -/// Syscalls represents Tock's system call APIs. It is designed to be -/// implemented as easily as possible -- its arguments and return values -/// correspond directly to registers in the ABI. For a higher-level abstraction, -/// see Platform. -/// -/// By design, syscalls is designed to be zero-cost in a TBF binary and -/// functional (but not zero-cost) in unit tests. In a TBF binary, Syscalls is -/// implemented with the `'static` lifetime, and is a zero-sized type. Syscalls -/// requires `Copy` in order to support defining it usefully on zero-sized -/// types. When used in unit tests, the Syscalls implementation carries a -/// lifetime local to that unit test. -/// -/// With the exception of `memop`, this trait aligns closely to Tock's -/// kernel::Driver trait. -pub trait Syscalls<'k>: Copy { - /// Calls the `allow` system call. - /// - /// # Safety - /// `allow` is unsafe because callers must guarantee that `pointer` and - /// `length` refer to memory that the kernel can mutate safely. The buffer - /// must last for the lifetime 'k. - // `driver` and `minor` are `usize` because the kernel internally treats - // them as `usize`s. `allow`'s return value is a kernel `ReturnCode`; - // Platform translates the `isize` into a `ReturnCode`. - unsafe fn allow(self, driver: usize, minor: usize, pointer: *mut u8, length: usize) -> isize; + /// Runs the next pending callback, if a callback is pending. Unlike + /// `yield_wait`, `yield_no_wait` returns immediately if no callback is + /// pending. Returns true if a callback was executed, false otherwise. + fn yield_no_wait() -> bool; - /// Calls the `command` system call. - // `driver`, `minor`, `arg1`, and `arg2` are all `usize` (rather than `u32`) - // because the kernel refers to them internally as `usize`s. command returns - // a kernel ReturnCode; Platform is responsible for translating an isize - // into the local ReturnCode. - fn command(self, driver: usize, minor: usize, arg1: usize, arg2: usize) -> isize; + // TODO: Add a subscribe interface. - /// Calls the `memop` system call with an argument. Note that memop() cannot - /// cause memory unsafety, although it can cause the app to fault (e.g. Brk - /// can move the app break below the stack, causing a fault). The isize - /// returned is a kernel ReturnCode. - // Platform performs the translation from isize into ReturnCode to keep - // Syscalls implementations simple. - fn memop_arg(self, op: MemopWithArg, arg: usize) -> isize; + // TODO: Add a command interface. - /// Calls the `memop` system call with no arguments. This version is - /// slightly cheaper because it does not need to set the argument register. - // We're okay with leaking the value in the argument register because - // memop() is always handled by the core kernel, never by an untrusted - // capsule. - fn memop_noarg(self, op: MemopNoArg) -> isize; + // TODO: Add a read-write allow interface. - /// Calls the `subscribe` system call. - /// - /// # Safety - /// `subscribe` is unsafe because the callback can potentially be unsafe, - /// and callers of `subscribe` must assert that calling the callback with - /// the provided `data` value is safe. The callback must last for the 'k - /// lifetime. - // Driver, minor, the callback args, and data are all represented as `usize` - // because that is the type the kernel uses internally to store them (e.g. - // as opposed to u32). - unsafe fn subscribe( - self, - driver: usize, - minor: usize, - callback: Option<unsafe extern "C" fn(usize, usize, usize, usize)>, - data: usize, - ); + // TODO: Add a read-only allow interface. - /// Puts the process to sleep until a callback becomes pending, then invokes - /// the callback. - fn yieldk(self); -} - -#[non_exhaustive] -#[repr(usize)] -pub enum MemopWithArg { - Brk = 0, - Sbrk = 1, - FlashRegionStart = 8, - FlashRegionEnd = 9, - SpecifyStackTop = 10, - SpecifyHeapStart = 11, -} - -#[non_exhaustive] -#[repr(usize)] -pub enum MemopNoArg { - MemoryStart = 2, - MemoryEnd = 3, - FlashStart = 4, - FlashEnd = 5, - GrantStart = 6, - FlashRegions = 7, + // TODO: Add memop() methods. }
diff --git a/core/platform/src/syscalls_impl.rs b/core/platform/src/syscalls_impl.rs new file mode 100644 index 0000000..0af15c5 --- /dev/null +++ b/core/platform/src/syscalls_impl.rs
@@ -0,0 +1,18 @@ +//! Implements `Syscalls` for all types that implement `RawSyscalls`. + +use crate::{RawSyscalls, Syscalls, YieldType}; + +impl<S: RawSyscalls> Syscalls for S { + // ------------------------------------------------------------------------- + // Yield + // ------------------------------------------------------------------------- + + fn yield_wait() { + Self::raw_yield(YieldType::Wait); + } + + fn yield_no_wait() -> bool { + // TODO: Introduce a return type abstraction so this 0 isn't hardcoded. + Self::raw_yield(YieldType::NoWait) != 0 + } +}
diff --git a/test_runner/Cargo.toml b/test_runner/Cargo.toml index 66a407a..9471138 100644 --- a/test_runner/Cargo.toml +++ b/test_runner/Cargo.toml
@@ -3,20 +3,3 @@ version = "0.1.0" authors = ["torfmaster <briefe@kebes.de>"] edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -structopt = { version = "0.3", default-features = false } -futures = "0.3.4" - -[dependencies.async-std] -# async-std 1.7 pulls in crossbeam_utils 1.8, which does not work with the -# nightly Rust toolchain we use. Temporarily block async-std 1.7 until we can -# update our Rust toolchain. -version = "1.5.0, <1.7" -features = ["attributes"] - -[dependencies.tokio] -version = "0.2.12" -features = ["process", "rt-threaded", "macros", "io-util", "time"]
diff --git a/test_runner/src/main.rs b/test_runner/src/main.rs index 0bea1cf..b21b945 100644 --- a/test_runner/src/main.rs +++ b/test_runner/src/main.rs
@@ -1,20 +1,10 @@ use std::fmt; -use std::process::Stdio; +use std::io::{BufRead, BufReader}; +use std::process::{ChildStdout, Command, Stdio}; use std::time::Duration; -use tokio::io::AsyncBufReadExt; -use tokio::io::BufReader; -use tokio::process::Command; -use tokio::time::timeout; -#[tokio::main] -async fn main() -> Result<(), Box<dyn std::error::Error>> { - timeout(Duration::from_secs(10), perform_tests()).await? -} - -async fn perform_tests() -> Result<(), Box<dyn std::error::Error>> { - let mut failed_tests = Vec::new(); - - let tests = Command::new("tock/tools/qemu-build/riscv32-softmmu/qemu-system-riscv32") +fn main() -> Result<(), Box<dyn std::error::Error>> { + let mut tests = Command::new("tock/tools/qemu-build/riscv32-softmmu/qemu-system-riscv32") .arg("-M") .arg("sifive_e,revb=true") .arg("-kernel") @@ -24,15 +14,32 @@ .arg("-nographic") .stdin(Stdio::null()) .stdout(Stdio::piped()) - .kill_on_drop(true) .spawn()?; + let stdout = tests.stdout.take().unwrap(); + let child_handle = std::sync::Arc::new(std::sync::Mutex::new(tests)); + let timeout_handle = child_handle.clone(); + std::thread::spawn(move || { + std::thread::sleep(Duration::from_secs(10)); + let _ = timeout_handle + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .kill(); + }); + let result = process_output(stdout); + let _ = child_handle + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .kill(); + result +} - let stdout = tests.stdout.unwrap(); - +fn process_output(stdout: ChildStdout) -> Result<(), Box<dyn std::error::Error>> { + let mut failed_tests = Vec::new(); let stdout_reader = BufReader::new(stdout); - let mut stdout_lines = stdout_reader.lines(); + let stdout_lines = stdout_reader.lines(); - while let Some(line) = stdout_lines.next_line().await? { + for line in stdout_lines { + let line = line?; println!("UART: {}", line); let test_result = test_succeeded(line, &mut failed_tests); if let Some(true) = test_result {