Split `Syscalls` into `Syscalls` and `RawSyscalls`. `RawSyscalls` contains everything I previously called the "low-level API". `Syscalls` contains everything I previously called the "high-level API". A generic impl implements `Syscalls` for every type that implements `RawSyscalls`.
diff --git a/core/platform/src/lib.rs b/core/platform/src/lib.rs index e746d32..699d3da 100644 --- a/core/platform/src/lib.rs +++ b/core/platform/src/lib.rs
@@ -10,10 +10,10 @@ mod allows; mod error_code; -mod syscall_types; +mod raw_syscalls; mod syscalls; pub use allows::{AllowReadable, Allowed}; pub use error_code::ErrorCode; -pub use syscall_types::{OneArgMemop, ReturnType, YieldType, ZeroArgMemop}; +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..b3cfba4 --- /dev/null +++ b/core/platform/src/raw_syscalls.rs
@@ -0,0 +1,190 @@ +// 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. There are a few major +// factors 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. +// +// Currently, yield takes exactly one argument, to specify what yield type to +// do. Therefore we only need one raw yield call. +// +// Subscribe, command, read-write allow, and read-only allow all take four +// argument types. Even when calling command IDs that have unused arguments, we +// still need to clear the argument registers so as to avoid passing +// confidential data to capsules (this is in line with Tock's threat model). As +// such, four_arg_syscall() is used for all subscribe, command, read-only allow, +// and read-write allow system calls. +// +// Memop takes 1 or 2 arguments (operation and an optional argument). Because it +// is part of the core kernel, it is okay for us to leave arbitrary data in the +// argument register for operations where the argument register is unused +// (again, in line with Tock's threat model). Memop returns up to 2 return +// arguments, so we don't need to mark r2 and r3 as clobbered. As such, we need +// two raw memop calls: one for operations without an argument and one for +// operations with an argument. +// +// Because the variables passed in and out of raw system calls represent +// register values, they are of type usize. In cases where it doesn't make sense +// to pass a pointer-sized value, libtock_unittest::FakeSyscalls may panic if a +// too-large value is passed. +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) -> usize; + + // 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) + // + /// # 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: usize, + r1: usize, + r2: usize, + r3: usize, + class: u8, + ) -> (usize, 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 usize + // so that if the kernel adds an unsafe memop this API doesn't become + // unsound. + fn zero_arg_memop(r0_in: ZeroArgMemop) -> (usize, 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 usize + // so that if the kernel adds an unsafe memop this API doesn't become + // unsound. + fn one_arg_memop(r0_in: OneArgMemop, r1: usize) -> (usize, usize); +} + +#[non_exhaustive] +#[repr(usize)] +pub enum OneArgMemop { + Brk = 0, + Sbrk = 1, + FlashRegionStart = 8, + FlashRegionEnd = 9, + SpecifyStackTop = 10, + SpecifyHeapStart = 11, +} + +// TODO: When the numeric values (0 and 1) are assigned to the yield types, +// specify those values here. +#[non_exhaustive] +#[repr(usize)] +pub enum YieldType { + Wait, + NoWait, +} + +#[non_exhaustive] +#[repr(usize)] +pub enum ZeroArgMemop { + MemoryStart = 2, + MemoryEnd = 3, + FlashStart = 4, + FlashEnd = 5, + GrantStart = 6, + FlashRegions = 7, +}
diff --git a/core/platform/src/syscall_types.rs b/core/platform/src/syscall_types.rs deleted file mode 100644 index d44df9d..0000000 --- a/core/platform/src/syscall_types.rs +++ /dev/null
@@ -1,46 +0,0 @@ -// Contains various types used by the `Syscalls` trait. These are in a separate -// file from `Syscalls` to keep the size of syscalls.rs reasonable. - -#[non_exhaustive] -#[repr(usize)] -pub enum OneArgMemop { - Brk = 0, - Sbrk = 1, - FlashRegionStart = 8, - FlashRegionEnd = 9, - SpecifyStackTop = 10, - SpecifyHeapStart = 11, -} - -pub enum ReturnType { - Failure = 0, - FailureWithU32 = 1, - FailureWith2U32 = 2, - FailureWithU64 = 3, - Success = 128, - SuccessWithU32 = 129, - SuccessWith2U32 = 130, - SuccessWithU64 = 131, - SuccessWith3U32 = 132, - SuccessWithU32AndU64 = 133, -} - -// TODO: When the numeric values (0 and 1) are assigned to the yield types, -// specify those values here. -#[non_exhaustive] -#[repr(usize)] -pub enum YieldType { - Wait, - NoWait, -} - -#[non_exhaustive] -#[repr(usize)] -pub enum ZeroArgMemop { - MemoryStart = 2, - MemoryEnd = 3, - FlashStart = 4, - FlashEnd = 5, - GrantStart = 6, - FlashRegions = 7, -}
diff --git a/core/platform/src/syscalls.rs b/core/platform/src/syscalls.rs index c9e7f98..fc3207e 100644 --- a/core/platform/src/syscalls.rs +++ b/core/platform/src/syscalls.rs
@@ -1,199 +1,55 @@ // TODO: Implement `libtock_runtime` and `libtock_unittest`, which are // referenced in the comment on `Syscalls`. -use crate::syscall_types::{OneArgMemop, ReturnType, YieldType, ZeroArgMemop}; +use crate::raw_syscalls::{RawSyscalls, YieldType}; -/// `Syscalls` serves two purposes: -/// High level: It provides safe abstractions over Tock's raw system calls. -/// Low level: It allows a fake Tock kernel to be injected into components -/// for unit testing. -/// -/// To serve these use cases, `Syscalls` has two API layers (called the -/// high-level API and the low-level API). Components needing access to Tock's -/// system calls should use the high-level API, which is safe and has nice -/// abstractions. The low-level API is implemented by -/// `libtock_runtime::TockSyscalls` and `libtock_unittest::FakeSyscalls`, and -/// provides the raw userspace<->kernel interface. +/// `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 { - // ------------------------------------------------------------------------- - // High-level API - // ------------------------------------------------------------------------- - /// Puts the process to sleep until a callback becomes pending, invokes the /// callback, then returns. - fn yield_wait() { - Self::raw_yield(YieldType::Wait); - } + fn yield_wait(); /// 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; + + // TODO: Add a subscribe interface. + + // TODO: Add a command interface. + + // TODO: Add a read-write allow interface. + + // TODO: Add a read-only allow interface. + + // TODO: Add memop() methods. +} + +impl<S: RawSyscalls> Syscalls for S { + fn yield_wait() { + Self::raw_yield(YieldType::Wait); + } + fn yield_no_wait() -> bool { Self::raw_yield(YieldType::NoWait) != ReturnType::Failure as usize } +} - // TODO: Implement a subscribe interface. - - // TODO: Implement a command interface. - - // TODO: Implement a read-write allow interface. - - // TODO: Implement a read-only allow interface. - - // TODO: Implement memop() methods. - - // ------------------------------------------------------------------------- - // Low-level API - // ------------------------------------------------------------------------- - - // This API is designed to minimize the amount of handwritten assembly code - // needed without generating unnecessary instructions. There are a few major - // factors 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. - // - // Currently, yield takes exactly one argument, to specify what yield type - // to do. Therefore we only need one raw yield call. - // - // Subscribe, command, read-write allow, and read-only allow all take four - // argument types. Even when calling command IDs that have unused arguments, - // we still need to clear the argument registers so as to avoid passing - // confidential data to capsules (this is in line with Tock's threat model). - // As such, four_arg_syscall() is used for all subscribe, command, read-only - // allow, and read-write allow system calls. - // - // Memop takes 1 or 2 arguments (operation and an optional argument), and - // being part of the core kernel it is okay for us to leave arbitrary data - // in the argument register if the argument is unused (again, in line with - // Tock's threat model). Memop returns up to 2 return arguments, so we don't - // need to mark r2 and r3 as clobbered. As such, we need two raw memop - // calls: one for operations without an argument and one for operations with - // an argument. - // - // Because the variables passed in and out of raw system calls represent - // register values, they are of type usize. In cases where it doesn't make - // sense to pass a pointer-sized value, libtock_unittest::FakeSyscalls is - // free to panic if a too-large value is passed. - - // 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) -> usize; - - // four_arg_syscall is used to invoke 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) - // - /// # Safety - /// A four_arg_syscall must NOT be used to invoke yield. Otherwise, it is - /// exactly as safe as the underlying system call, which varies depending on - /// the system call class. - unsafe fn four_arg_syscall( - r0: usize, - r1: usize, - r2: usize, - r3: usize, - class: u8, - ) -> (usize, usize, usize, usize); - - // zero_arg_memop is used to invoke memop operations that do not accept an - // argument register. Because the are no memop commands that set r2 or r3, - // this only needs to return r0 and r1. - // - // Many memop commands are not expected to work in the unit test - // environment. If called, those commands may panic. - // - // 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 usize - // so that if the kernel adds an unsafe memop this API doesn't become - // unsound. - fn zero_arg_memop(r0_in: ZeroArgMemop) -> (usize, 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 usize - // so that if the kernel adds an unsafe memop this API doesn't become - // unsound. - fn one_arg_memop(r0_in: OneArgMemop, r1: usize) -> (usize, usize); +// Note: variants are commented out because if they aren't commented out I get a +// "variant is never constructed" error. When we figure out an error handling +// design, this type is likely to move into an error handling-related module, at +// which point we will uncomment the other variants. +enum ReturnType { + Failure = 0, + //FailureWithU32 = 1, + //FailureWith2U32 = 2, + //FailureWithU64 = 3, + //Success = 128, + //SuccessWithU32 = 129, + //SuccessWith2U32 = 130, + //SuccessWithU64 = 131, + //SuccessWith3U32 = 132, + //SuccessWithU32AndU64 = 133, }