diff --git a/platform/src/default_config.rs b/platform/src/default_config.rs new file mode 100644 index 000000000..89993f2bd --- /dev/null +++ b/platform/src/default_config.rs @@ -0,0 +1,5 @@ +/// A general purpose syscall configuration, which is used as the default +/// configuration for `libtock_rs`' system calls. +pub struct DefaultConfig; + +impl crate::subscribe::Config for DefaultConfig {} diff --git a/platform/src/exit_on_drop.rs b/platform/src/exit_on_drop.rs new file mode 100644 index 000000000..44c6bf824 --- /dev/null +++ b/platform/src/exit_on_drop.rs @@ -0,0 +1,27 @@ +/// Calls the Exit system call when dropped. Used to catch panic unwinding from +/// a `#![no_std]` context. The caller should `core::mem::forget` the +/// `ExitOnDrop` when it no longer needs to catch unwinding. +/// +/// +/// # Example +/// ``` +/// use libtock_platform::exit_on_drop::ExitOnDrop; +/// fn function_that_must_not_unwind() { +/// let exit_on_drop: ExitOnDrop:: = Default::default(); +/// /* Do something that might unwind here. */ +/// core::mem::forget(exit_on_drop); +/// } +/// ``` +pub struct ExitOnDrop(core::marker::PhantomData); + +impl Default for ExitOnDrop { + fn default() -> ExitOnDrop { + ExitOnDrop(core::marker::PhantomData) + } +} + +impl Drop for ExitOnDrop { + fn drop(&mut self) { + S::exit_terminate(0); + } +} diff --git a/platform/src/lib.rs b/platform/src/lib.rs index ef68b3c9c..40ed324c6 100644 --- a/platform/src/lib.rs +++ b/platform/src/lib.rs @@ -4,10 +4,13 @@ mod async_traits; mod command_return; mod constants; +mod default_config; mod error_code; +pub mod exit_on_drop; mod raw_syscalls; mod register; pub mod return_variant; +pub mod subscribe; mod syscall_scope; mod syscalls; mod syscalls_impl; @@ -17,10 +20,12 @@ mod yield_types; pub use async_traits::{CallbackContext, FreeCallback, Locator, MethodCallback}; pub use command_return::CommandReturn; pub use constants::{exit_id, syscall_class, yield_id}; +pub use default_config::DefaultConfig; pub use error_code::ErrorCode; pub use raw_syscalls::RawSyscalls; pub use register::Register; pub use return_variant::ReturnVariant; +pub use subscribe::{Subscribe, Upcall}; pub use syscall_scope::syscall_scope; pub use syscalls::Syscalls; pub use termination::Termination; diff --git a/platform/src/raw_syscalls.rs b/platform/src/raw_syscalls.rs index 1a036a643..072494485 100644 --- a/platform/src/raw_syscalls.rs +++ b/platform/src/raw_syscalls.rs @@ -59,7 +59,7 @@ use crate::Register; // // These system calls are refined further individually, which is documented on // a per-function basis. -pub unsafe trait RawSyscalls { +pub unsafe trait RawSyscalls: Sized { // 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. diff --git a/platform/src/subscribe.rs b/platform/src/subscribe.rs new file mode 100644 index 000000000..42c736673 --- /dev/null +++ b/platform/src/subscribe.rs @@ -0,0 +1,155 @@ +use crate::syscall_scope::ShareList; +use crate::Syscalls; + +// ----------------------------------------------------------------------------- +// `Subscribe` struct +// ----------------------------------------------------------------------------- + +/// A `Subscribe` instance allows safe code to call Tock's Subscribe system +/// call, by guaranteeing the upcall will be cleaned up before 'scope ends. It +/// is generally used with the `syscall_scope` function, which offers a safe +/// interface for constructing `Subscribe` instances. +pub struct Subscribe<'scope, S: Syscalls, const DRIVER_NUM: u32, const SUBSCRIBE_NUM: u32> { + _syscalls: core::marker::PhantomData, + + // Make this struct invariant with respect to the 'scope lifetime. + // + // Covariance would be unsound, as that would allow code with a + // `Subscribe<'static, ...>` to register an upcall that lasts for a shorter + // lifetime, resulting in use-after-free if the upcall in invoked. + // Contravariance would be sound, but is not necessary and may be confusing. + // + // Additionally, we want to have at least one private member of this struct + // so that code outside this module cannot construct a `Subscribe` without + // calling `ShareList::new`. + _scope: core::marker::PhantomData>, +} + +// TODO: Test Drop impl +impl<'scope, S: Syscalls, const DRIVER_NUM: u32, const SUBSCRIBE_NUM: u32> Drop + for Subscribe<'scope, S, DRIVER_NUM, SUBSCRIBE_NUM> +{ + fn drop(&mut self) { + S::unsubscribe(DRIVER_NUM, SUBSCRIBE_NUM); + } +} + +impl<'scope, S: Syscalls, const DRIVER_NUM: u32, const SUBSCRIBE_NUM: u32> ShareList<'scope> + for Subscribe<'scope, S, DRIVER_NUM, SUBSCRIBE_NUM> +{ + unsafe fn new() -> Subscribe<'scope, S, DRIVER_NUM, SUBSCRIBE_NUM> { + Subscribe { + _syscalls: core::marker::PhantomData, + _scope: core::marker::PhantomData, + } + } +} + +// ----------------------------------------------------------------------------- +// `Upcall` trait +// ----------------------------------------------------------------------------- + +/// A Tock kernel upcall. Upcalls are registered using the Subscribe system +/// call, and are invoked during Yield calls. +/// +/// Each `Upcall` supports one or more subscribe IDs, which are indicated by the +/// `SupportedIds` parameter. The types `AnySubscribeId` and `OneSubscribeId` +/// are provided to use as `SupportedIds` parameters in `Upcall` +/// implementations. +pub trait Upcall { + fn upcall(&self, arg0: u32, arg1: u32, arg2: u32); +} + +pub trait SupportsId {} + +pub struct AnyId; +impl SupportsId + for AnyId +{ +} + +pub struct OneId; +impl SupportsId + for OneId +{ +} + +// ----------------------------------------------------------------------------- +// Upcall implementations that simply store their arguments +// ----------------------------------------------------------------------------- + +/// An implementation of `Upcall` that sets the contained boolean value to +/// `true` when the upcall is invoked. +impl Upcall for core::cell::Cell { + fn upcall(&self, _: u32, _: u32, _: u32) { + self.set(true); + } +} + +/// Implemented for consistency with the other `Cell>` `Upcall` +/// impls. Most users would prefer the `Cell` implementation over this +/// impl, but this may be useful in a generic or macro context. +impl Upcall for core::cell::Cell> { + fn upcall(&self, _: u32, _: u32, _: u32) { + self.set(Some(())); + } +} + +/// An `Upcall` implementation that stores its first argument when called. +impl Upcall for core::cell::Cell> { + fn upcall(&self, arg0: u32, _: u32, _: u32) { + self.set(Some((arg0,))); + } +} + +/// An `Upcall` implementation that stores its first two arguments when called. +impl Upcall for core::cell::Cell> { + fn upcall(&self, arg0: u32, arg1: u32, _: u32) { + self.set(Some((arg0, arg1))); + } +} + +/// An `Upcall` implementation that stores its arguments when called. +impl Upcall for core::cell::Cell> { + fn upcall(&self, arg0: u32, arg1: u32, arg2: u32) { + self.set(Some((arg0, arg1, arg2))); + } +} + +#[cfg(test)] +#[test] +fn upcall_impls() { + let cell_bool = core::cell::Cell::new(false); + cell_bool.upcall(1, 2, 3); + assert!(cell_bool.get()); + + let cell_empty = core::cell::Cell::new(None); + cell_empty.upcall(1, 2, 3); + assert_eq!(cell_empty.get(), Some(())); + + let cell_one = core::cell::Cell::new(None); + cell_one.upcall(1, 2, 3); + assert_eq!(cell_one.get(), Some((1,))); + + let cell_two = core::cell::Cell::new(None); + cell_two.upcall(1, 2, 3); + assert_eq!(cell_two.get(), Some((1, 2))); + + let cell_three = core::cell::Cell::new(None); + cell_three.upcall(1, 2, 3); + assert_eq!(cell_three.get(), Some((1, 2, 3))); +} + +// ----------------------------------------------------------------------------- +// `Config` trait +// ----------------------------------------------------------------------------- + +/// `Config` configures the behavior of the Subscribe system call. It should +/// generally be passed through by drivers, to allow application code to +/// configure error handling. +pub trait Config { + /// Called if a Subscribe call succeeds and returns a non-null upcall. In + /// some applications, this may indicate unexpected reentrance. By default, + /// the non-null upcall is ignored. + fn returned_nonnull_upcall(_driver_num: u32, _subscribe_num: u32) {} +} diff --git a/platform/src/syscalls.rs b/platform/src/syscalls.rs index e87cc89b4..e6c8cadd9 100644 --- a/platform/src/syscalls.rs +++ b/platform/src/syscalls.rs @@ -1,9 +1,11 @@ -use crate::{CommandReturn, YieldNoWaitReturn}; +use crate::{ + subscribe, CommandReturn, ErrorCode, RawSyscalls, Subscribe, Upcall, YieldNoWaitReturn, +}; /// `Syscalls` provides safe abstractions over Tock's system calls. It is /// implemented for `libtock_runtime::TockSyscalls` and /// `libtock_unittest::fake::Kernel` (by way of `RawSyscalls`). -pub trait Syscalls { +pub trait Syscalls: RawSyscalls + Sized { // ------------------------------------------------------------------------- // Yield // ------------------------------------------------------------------------- @@ -17,7 +19,26 @@ pub trait Syscalls { /// callback, then returns. fn yield_wait(); - // TODO: Add a subscribe interface. + // ------------------------------------------------------------------------- + // Subscribe + // ------------------------------------------------------------------------- + + /// Registers an upcall with the kernel. + fn subscribe< + 'scope, + IDS: subscribe::SupportsId, + U: Upcall, + CONFIG: subscribe::Config, + const DRIVER_NUM: u32, + const SUBSCRIBE_NUM: u32, + >( + subscribe: &Subscribe<'scope, Self, DRIVER_NUM, SUBSCRIBE_NUM>, + upcall: &'scope U, + ) -> Result<(), ErrorCode>; + + /// Unregisters the upcall with the given ID. If no upcall is registered + /// with the given ID, `unsubscribe` does nothing. + fn unsubscribe(driver_num: u32, subscribe_num: u32); // ------------------------------------------------------------------------- // Command @@ -31,6 +52,10 @@ pub trait Syscalls { // TODO: Add memop() methods. + // ------------------------------------------------------------------------- + // Exit + // ------------------------------------------------------------------------- + fn exit_terminate(exit_code: u32) -> !; fn exit_restart(exit_code: u32) -> !; diff --git a/platform/src/syscalls_impl.rs b/platform/src/syscalls_impl.rs index 3399c063b..37942fe83 100644 --- a/platform/src/syscalls_impl.rs +++ b/platform/src/syscalls_impl.rs @@ -1,7 +1,9 @@ //! Implements `Syscalls` for all types that implement `RawSyscalls`. use crate::{ - exit_id, syscall_class, yield_id, CommandReturn, RawSyscalls, Syscalls, YieldNoWaitReturn, + exit_id, exit_on_drop, return_variant, subscribe, syscall_class, yield_id, CommandReturn, + ErrorCode, RawSyscalls, Register, ReturnVariant, Subscribe, Syscalls, Upcall, + YieldNoWaitReturn, }; impl Syscalls for S { @@ -33,6 +35,113 @@ impl Syscalls for S { } } + // ------------------------------------------------------------------------- + // Subscribe + // ------------------------------------------------------------------------- + + fn subscribe< + 'scope, + IDS: subscribe::SupportsId, + U: Upcall, + CONFIG: subscribe::Config, + const DRIVER_NUM: u32, + const SUBSCRIBE_NUM: u32, + >( + _subscribe: &Subscribe<'scope, Self, DRIVER_NUM, SUBSCRIBE_NUM>, + upcall: &'scope U, + ) -> Result<(), ErrorCode> { + // The upcall function passed to the Tock kernel. + // + // Safety: data must be a reference to a valid instance of U. + unsafe extern "C" fn kernel_upcall>( + arg0: u32, + arg1: u32, + arg2: u32, + data: Register, + ) { + let upcall: *const U = data.into(); + let exit: exit_on_drop::ExitOnDrop = Default::default(); + unsafe { &*upcall }.upcall(arg0, arg1, arg2); + core::mem::forget(exit); + } + + // Inner function that does the majority of the work. This is not + // monomorphized over DRIVER_NUM and SUBSCRIBE_NUM to keep code size + // small. + // + // Safety: upcall_fcn must be kernel_upcall and upcall_data + // must be a reference to an instance of U that will remain valid as + // long as the 'scope lifetime is alive. Can only be called if a + // Subscribe<'scope, S, driver_num, subscribe_num> exists. + unsafe fn inner( + driver_num: u32, + subscribe_num: u32, + upcall_fcn: Register, + upcall_data: Register, + ) -> Result<(), ErrorCode> { + // Safety: syscall4's documentation indicates it can be used to call + // Subscribe. These arguments follow TRD104. kernel_upcall has the + // required signature. This function's preconditions mean that + // upcall is a reference to an instance of U that will remain valid + // until the 'scope lifetime is alive The existence of the + // Subscribe<'scope, Self, DRIVER_NUM, SUBSCRIBE_NUM> guarantees + // that if this Subscribe succeeds then the upcall will be cleaned + // up before the 'scope lifetime ends, guaranteeing that upcall is + // still alive when kernel_upcall is invoked. + let [r0, r1, _, _] = unsafe { + S::syscall4::<{ syscall_class::SUBSCRIBE }>([ + driver_num.into(), + subscribe_num.into(), + upcall_fcn, + upcall_data, + ]) + }; + + let return_variant: ReturnVariant = r0.as_u32().into(); + if return_variant == return_variant::FAILURE_2_U32 { + // Safety: TRD 104 guarantees that if r0 is Failure with 2 U32, + // then r1 will contain a valid error code. ErrorCode is + // designed to be safely transmuted directly from a kernel error + // code. + Err(unsafe { core::mem::transmute(r1.as_u32() as u16) }) + } else { + // r0 indicates Success with 2 u32s. Confirm the null upcall was + // returned, and it if wasn't then call the configured function. + // We're relying on the optimizer to remove this branch if + // returned_nonnull_upcall is a no-op. + let returned_upcall: *const () = r1.into(); + #[allow(clippy::zero_ptr)] + if returned_upcall != 0 as *const () { + CONFIG::returned_nonnull_upcall(driver_num, subscribe_num); + } + Ok(()) + } + } + + let upcall_fcn = (kernel_upcall:: as usize).into(); + let upcall_data = (upcall as *const U).into(); + // Safety: upcall's type guarantees it is a reference to a U that will + // remain valid for at least the 'scope lifetime. _subscribe is a + // reference to a Subscribe<'scope, Self, DRIVER_NUM, SUBSCRIBE_NUM>, + // proving one exists. upcall_fcn and upcall_data are derived in ways + // that satisfy inner's requirements. + unsafe { inner::(DRIVER_NUM, SUBSCRIBE_NUM, upcall_fcn, upcall_data) } + } + + fn unsubscribe(driver_num: u32, subscribe_num: u32) { + unsafe { + // syscall4's documentation indicates it can be used to call + // Subscribe. The upcall pointer passed is the null upcall, which + // cannot cause undefined behavior on its own. + Self::syscall4::<{ syscall_class::SUBSCRIBE }>([ + driver_num.into(), + subscribe_num.into(), + 0usize.into(), + 0usize.into(), + ]); + } + } + // ------------------------------------------------------------------------- // Command // ------------------------------------------------------------------------- diff --git a/syscalls_tests/src/exit_on_drop.rs b/syscalls_tests/src/exit_on_drop.rs new file mode 100644 index 000000000..3d560c9dd --- /dev/null +++ b/syscalls_tests/src/exit_on_drop.rs @@ -0,0 +1,27 @@ +use libtock_platform::exit_on_drop::ExitOnDrop; +use libtock_unittest::fake; + +// Unwinds if `unwind` is true, otherwise just returns. +fn maybe_unwind(unwind: bool) { + if unwind { + panic!("Triggering stack unwinding."); + } +} + +#[cfg(not(miri))] +#[test] +fn exit() { + let exit = libtock_unittest::exit_test("exit_on_drop::exit", || { + let exit_on_drop: ExitOnDrop = Default::default(); + maybe_unwind(true); + core::mem::forget(exit_on_drop); + }); + assert_eq!(exit, libtock_unittest::ExitCall::Terminate(0)); +} + +#[test] +fn no_exit() { + let exit_on_drop: ExitOnDrop = Default::default(); + maybe_unwind(false); + core::mem::forget(exit_on_drop); +} diff --git a/syscalls_tests/src/lib.rs b/syscalls_tests/src/lib.rs index 202f9f20f..adec718fa 100644 --- a/syscalls_tests/src/lib.rs +++ b/syscalls_tests/src/lib.rs @@ -15,11 +15,15 @@ #[cfg(test)] mod command_tests; +#[cfg(test)] +mod exit_on_drop; + // TODO: Add Exit. // TODO: Add Memop. -// TODO: Add Subscribe. +#[cfg(test)] +mod subscribe_tests; #[cfg(test)] mod yield_tests; diff --git a/syscalls_tests/src/subscribe_tests.rs b/syscalls_tests/src/subscribe_tests.rs new file mode 100644 index 000000000..cc47b6d8e --- /dev/null +++ b/syscalls_tests/src/subscribe_tests.rs @@ -0,0 +1,51 @@ +use libtock_platform::{ + syscall_scope, CommandReturn, DefaultConfig, ErrorCode, Syscalls, YieldNoWaitReturn, +}; +use libtock_unittest::{command_return, fake, upcall}; + +// Fake driver that accepts an upcall. The unit test cases use this to make the +// kernel willing to accept an upcall. The test cases invoke the upcall +// themselves. +struct MockDriver; + +impl fake::Driver for MockDriver { + fn id(&self) -> u32 { + 1 + } + + fn num_upcalls(&self) -> u32 { + 1 + } + + fn command(&self, _: u32, _: u32, _: u32) -> CommandReturn { + command_return::failure(ErrorCode::NoSupport) + } +} + +#[test] +fn failed() { + let _kernel = fake::Kernel::new(); + let done = core::cell::Cell::new(false); + syscall_scope(|subscribe| { + assert_eq!( + fake::Syscalls::subscribe::<_, _, DefaultConfig, 1, 2>(subscribe, &done), + Err(ErrorCode::NoMem) + ); + }); +} + +#[test] +fn success() { + let kernel = fake::Kernel::new(); + kernel.add_driver(&std::rc::Rc::new(MockDriver)); + let called = core::cell::Cell::new(false); + syscall_scope(|subscribe| { + assert_eq!( + fake::Syscalls::subscribe::<_, _, DefaultConfig, 1, 0>(subscribe, &called), + Ok(()) + ); + upcall::schedule(1, 0, (0, 0, 0)).unwrap(); + assert_eq!(fake::Syscalls::yield_no_wait(), YieldNoWaitReturn::Upcall); + assert!(called.get()); + }); +}