Skip to content

Commit

Permalink
Merge #282
Browse files Browse the repository at this point in the history
282: Overhaul RawSyscalls to work with ARM, -Zmiri-track-raw-pointers, the revised Yield, and Exit. r=alistair23 a=jrvanwhy

1. RawSyscalls now supports Yield's new return value semantics as well as the `yield-no-wait` variant of Yield.
2. RawSyscalls now supports Exit.
3. RawSyscalls is now usable in Miri with the `-Zmiri-track-raw-pointers` flag.
4. RawSyscalls can now be implemented on ARM: previously, `class` was a runtime value, but it needs to be an immediate value on ARM.
5. The explanation for the design of RawSyscalls was completely overhauled. Instead of listing a bunch of "design considerations" with no connection to the final design, it shows how testing and efficiency considerations lead to its design. I hope the new description is more amenable to discussion. The new design should be a bit more future-proof than the previous design as well.

The new features added have been stabilized, and can be removed after #280 is merged.

Co-authored-by: Johnathan Van Why <[email protected]>
  • Loading branch information
bors[bot] and jrvanwhy authored Apr 6, 2021
2 parents 84c4bf2 + 837f164 commit b687f0a
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 233 deletions.
4 changes: 3 additions & 1 deletion core/platform/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ mod raw_syscalls;
pub mod return_variant;
mod syscalls;
mod syscalls_impl;
mod yield_types;

pub use async_traits::{CallbackContext, FreeCallback, Locator, MethodCallback};
pub use command_return::CommandReturn;
pub use error_code::ErrorCode;
pub use raw_syscalls::{OneArgMemop, RawSyscalls, YieldType, ZeroArgMemop};
pub use raw_syscalls::RawSyscalls;
pub use return_variant::ReturnVariant;
pub use syscalls::Syscalls;
pub use yield_types::YieldNoWaitReturn;

#[cfg(test)]
mod command_return_tests;
315 changes: 129 additions & 186 deletions core/platform/src/raw_syscalls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,98 +3,77 @@

/// `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`
/// `libtock_unittest::fake::Kernel`. **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:
// The RawSyscalls trait is designed to minimize the complexity and size of its
// implementation, as its implementation is difficult to test (it cannot be used
// in unit tests, with sanitizers, or in Miri). It is also designed to minimize
// the number of unnecessary instructions it generates.
//
// 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.
// Convention: This file uses the same register naming conventions as the Tock
// 2.0 syscall TRD. Registers r0-r4 correspond to ARM registers r0-r4 and RISC-V
// registers a0-a4.
//
// (*) 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.
// Theoretically, RawSyscalls could consist of a single raw system call. To
// start, something like this should work:
//
// Currently, yield takes exactly one argument (to specify what yield type to
// do). Therefore we only need one raw yield call.
// unsafe fn syscall<const CLASS: usize>([usize; 4]) -> [usize; 4];
//
// 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, this will not work with Miri's -Zmiri-track-raw-pointers flag, as it
// causes pointers passed to the kernel via the Allow system calls to be
// untagged. In order to work with -Zmiri-track-raw-pointers, we need to pass
// pointers for the register values. Rust's closest analogue to C's void pointer
// is *mut () or *const (); we use *mut () because it is shorter:
//
// 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.
// unsafe fn syscall<const CLASS: usize>([*mut (); 4]) -> [*mut (); 4];
//
// 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).
// Using a single system call has a major inefficiency. The single raw system
// call would need to clobber every register that any system call can clobber.
// Yield has a far longer clobber list than most system calls, so this would be
// inefficient for the majority of system calls. As a result, we can split yield
// out into its own function, giving the following API:
//
// This only leaves four_arg_syscall, which is used to implement subscribe,
// command, read-write allow, and read-only allow.
// unsafe fn yield([*mut (); 4]) -> [*mut (); 4];
// unsafe fn syscall<const CLASS: usize>([*mut (); 4]) -> [*mut (); 4];
//
// Therefore the final design has 4 methods:
// yield
// zero_arg_memop
// one_arg_memop
// four_arg_syscall
// There is one significant inefficiency remaining. Many system calls, such as
// memop's "get RAM start address" operation, do not need to set all four
// arguments. The compiler cannot optimize away this inefficiency, so to remove
// it we need to split the system calls up based on the number of arguments they
// take:
//
// If a new system call class that uses fewer than four arguments is added, then
// the above list will need to be revised.
// unsafe fn yield0([*mut (); 0]) -> [*mut (); 4];
// unsafe fn yield1([*mut (); 1]) -> [*mut (); 4];
// unsafe fn yield2([*mut (); 2]) -> [*mut (); 4];
// unsafe fn yield3([*mut (); 3]) -> [*mut (); 4];
// unsafe fn yield4([*mut (); 4]) -> [*mut (); 4];
// unsafe fn syscall0<const CLASS: usize>([*mut (); 0]) -> [*mut (); 4];
// unsafe fn syscall1<const CLASS: usize>([*mut (); 1]) -> [*mut (); 4];
// unsafe fn syscall2<const CLASS: usize>([*mut (); 2]) -> [*mut (); 4];
// unsafe fn syscall3<const CLASS: usize>([*mut (); 3]) -> [*mut (); 4];
// unsafe fn syscall4<const CLASS: usize>([*mut (); 4]) -> [*mut (); 4];
//
// 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.
// However, not all of these are used! If we remove the system calls that are
// unused, we are left with the following:
//
// 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.
// unsafe fn yield1([*mut (); 1]) -> [*mut (); 4];
// unsafe fn yield2([*mut (); 2]) -> [*mut (); 4];
// unsafe fn syscall1<const CLASS: usize>([*mut (); 1]) -> [*mut (); 4];
// unsafe fn syscall2<const CLASS: usize>([*mut (); 2]) -> [*mut (); 4];
// unsafe fn syscall4<const CLASS: usize>([*mut (); 4]) -> [*mut (); 4];
//
// 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:
// These system calls are refined further individually, which is documented on
// a per-function basis.
pub unsafe trait RawSyscalls {
// yield1 can only be used to call `yield-wait`, which does not have a
// return value. To simplify the assembly implementation, we remove its
// return value.
//
// yield1 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.
// 2. Pass in r0 as an inlateout register.
// 3. Mark all caller-saved registers as lateout clobbers.
// 4. NOT provide any of the following options:
// pure (yield has side effects)
Expand All @@ -103,142 +82,106 @@ pub trait RawSyscalls {
// 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).
/// `raw_yield` should only be called by `libtock_platform`.
fn raw_yield(r0_in: YieldType) -> u32;
/// `yield1` should only be called by `libtock_platform`.
/// # Safety
/// yield1 may only be used for yield operations that do not return a value.
/// It is exactly as safe as the underlying system call.
unsafe fn yield1(_: [*mut (); 1]);

// 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).
// yield2 can only be used to call `yield-no-wait`. `yield-no-wait` does not
// return any values, so to simplify the assembly we omit return arguments.
//
// For subscribe(), the callback pointer should be either 0 (for the null
// callback) or an `unsafe extern fn(u32, u32, u32, usize)`.
/// `four_arg_syscall` should only be called by `libtock_platform`.
///
// yield2 should:
// 1. Call syscall class 0
// 2. Pass in r0 and r1 as inlateout registers.
// 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)
/// `yield2` should only be called by `libtock_platform`.
/// # 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);
/// yield2 may only be used for yield operations that do not return a value.
/// It has the same safety invariants as the underlying system call.
unsafe fn yield2(_: [*mut (); 2]);

// 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.
// syscall1 is only used to invoke Memop operations. Because there are no
// Memop commands that set r2 or r3, raw_syscall1 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:
// syscall1 should:
// 1. Call the syscall class specified by CLASS.
// 2. Pass r0 as an inlateout register.
// 3. Specify r1 as a lateout register and return its value.
// 4. Not mark any registers as clobbered.
// 5. Have 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:
// 6. 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.
/// `four_arg_syscall` should only be called by `libtock_platform`.
fn zero_arg_memop(r0_in: ZeroArgMemop) -> (u32, usize);
/// `syscall1` should only be called by `libtock_platform`.
/// # Safety
/// This directly makes a system call. It can only be used for core kernel
/// system calls that accept 1 argument and only overwrite r0 and r1 on
/// return. It is unsafe any time the underlying system call is unsafe.
unsafe fn syscall1<const CLASS: usize>(_: [*mut (); 1]) -> [*mut (); 2];

// 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.
// syscall2 is used to invoke Exit as well as Memop operations that take an
// argument. Memop does not currently use more than 2 registers for its
// return value, and Exit does not return, so syscall2 only returns 2
// values.
//
// one_arg_memop's inline assembly should:
// 1. Call syscall class 5
// 2. Specify r0 and r1 as inlateout registers, and return (r0, r1)
// syscall2 should:
// 1. Call the syscall class specified by CLASS.
// 2. Pass r0 and r1 as inlateout registers.
// 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:
// 5. 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.
/// `four_arg_syscall` should only be called by `libtock_platform`.
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,
}
/// `syscall2` should only be called by `libtock_platform`.
/// # Safety
/// `syscall2` directly makes a system call. It can only be used for core
/// kernel system calls that accept 2 arguments and only overwrite r0 and r1
/// on return. It is unsafe any time the underlying system call is unsafe.
unsafe fn syscall2<const CLASS: usize>(_: [*mut (); 2]) -> [*mut (); 2];

#[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.
// syscall4 should:
// 1. Call the syscall class specified by CLASS.
// 2. Pass r0-r3 in the corresponding registers as inlateout registers.
// 3. Not mark any registers as clobbered.
// 4. Have all of the following options:
// preserves_flags (these system calls do not touch flags)
// nostack (these system calls do not touch the stack)
// 5. 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)
//
// For subscribe(), the callback pointer should be either 0 (for the null
// callback) or an `unsafe extern fn(u32, u32, u32, Userdata)`.
/// `syscall4` should only be called by `libtock_platform`.
///
/// # Safety
/// `syscall4` 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 syscall4<const CLASS: usize>(_: [*mut (); 4]) -> [*mut (); 4];
}
Loading

0 comments on commit b687f0a

Please sign in to comment.