blob: f4e37b8697f2e407293aa6734c997616203813f1 [file] [log] [blame]
// TODO: Implement `libtock_unittest`, which is 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).
/// `raw_yield` should only be called by `libtock_platform`.
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)`.
/// `four_arg_syscall` 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);
// 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.
/// `four_arg_syscall` should only be called by `libtock_platform`.
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.
/// `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,
}
#[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.
}