From 0b6e7c295adee4579355c560f10497c701270e04 Mon Sep 17 00:00:00 2001 From: Hudson Ayers Date: Sun, 21 Jul 2024 23:51:50 -0400 Subject: [PATCH] Initial memop syscall implementation + tests --- platform/src/register.rs | 15 +- platform/src/syscalls.rs | 37 ++++- platform/src/syscalls_impl.rs | 147 +++++++++++++++++ runtime/src/startup/mod.rs | 30 ++-- syscalls_tests/src/lib.rs | 3 +- syscalls_tests/src/memop_tests.rs | 152 ++++++++++++++++++ unittest/src/expected_syscall.rs | 15 +- unittest/src/fake/kernel.rs | 1 + unittest/src/fake/syscalls/memop_impl.rs | 92 +++++++++++ .../src/fake/syscalls/memop_impl_tests.rs | 84 ++++++++++ unittest/src/fake/syscalls/mod.rs | 3 + .../src/fake/syscalls/raw_syscalls_impl.rs | 6 +- .../fake/syscalls/raw_syscalls_impl_tests.rs | 22 ++- unittest/src/kernel_data.rs | 1 + unittest/src/syscall_log.rs | 11 +- 15 files changed, 596 insertions(+), 23 deletions(-) create mode 100644 syscalls_tests/src/memop_tests.rs create mode 100644 unittest/src/fake/syscalls/memop_impl.rs create mode 100644 unittest/src/fake/syscalls/memop_impl_tests.rs diff --git a/platform/src/register.rs b/platform/src/register.rs index 0afde2f1..312276a5 100644 --- a/platform/src/register.rs +++ b/platform/src/register.rs @@ -8,7 +8,7 @@ use core::mem::transmute; /// wraps, but instead use the conversion functions in this module. // Register is repr(transparent) so that an upcall's application data can be // soundly passed as a Register. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[repr(transparent)] pub struct Register(pub *mut ()); @@ -28,6 +28,12 @@ impl From for Register { } } +impl From for Register { + fn from(value: i32) -> Register { + (value as usize).into() + } +} + impl From for Register { fn from(value: usize) -> Register { // Note: clippy is wrong here; transmute has different semantics than @@ -72,6 +78,13 @@ impl Register { pub fn as_u32(self) -> u32 { self.0 as u32 } + + /// Casts this register to a i32, truncating it if it is larger than + /// 32 bits. This conversion should be avoided in host-based test code; use + /// the `TryFrom for i32` implementation instead. + pub fn as_i32(self) -> i32 { + self.0 as i32 + } } impl From for usize { diff --git a/platform/src/syscalls.rs b/platform/src/syscalls.rs index 74dba69b..546ba6c6 100644 --- a/platform/src/syscalls.rs +++ b/platform/src/syscalls.rs @@ -77,7 +77,42 @@ pub trait Syscalls: RawSyscalls + Sized { /// `unallow_ro` does nothing. fn unallow_ro(driver_num: u32, buffer_num: u32); - // TODO: Add memop() methods. + // ------------------------------------------------------------------------- + // Memop + // ------------------------------------------------------------------------- + + /// Changes the location of the program break to the specified address and + /// returns an error if it fails to do so. + /// + /// # Safety + /// Callers of this function must ensure that they do not pass an + /// address below any address that includes a currently reachable object. + unsafe fn memop_brk(addr: *const u8) -> Result<(), ErrorCode>; + + /// Changes the location of the program break by the passed increment, + /// and returns the previous break address. + /// + /// # Safety + /// Callers of this function must ensure that they do not pass an + /// increment that would deallocate memory containing any currently + /// reachable object. + unsafe fn memop_sbrk(incr: i32) -> Result<*const u8, ErrorCode>; + + /// Increments the program break by the passed increment, + /// and returns the previous break address. + fn memop_increment_brk(incr: u32) -> Result<*const u8, ErrorCode>; + + /// Gets the address of the start of this application's RAM allocation. + fn memop_app_ram_start() -> Result<*const u8, ErrorCode>; + + /// Tells the kernel where the start of the app stack is, to support + /// debugging. + fn memop_debug_stack_start(stack_top: *const u8) -> Result<(), ErrorCode>; + + /// Tells the kernel the initial program break, to support debugging. + fn memop_debug_heap_start(initial_break: *const u8) -> Result<(), ErrorCode>; + + // TODO: Add remaining memop() methods (3-9). // ------------------------------------------------------------------------- // Exit diff --git a/platform/src/syscalls_impl.rs b/platform/src/syscalls_impl.rs index 96bfbec2..d2057ede 100644 --- a/platform/src/syscalls_impl.rs +++ b/platform/src/syscalls_impl.rs @@ -352,6 +352,153 @@ impl Syscalls for S { } } + // ------------------------------------------------------------------------- + // Memop + // ------------------------------------------------------------------------- + + // Safety: Callers of this function must ensure that they do not pass an + // address below any address that includes a currently reachable object. + unsafe fn memop_brk(addr: *const u8) -> Result<(), ErrorCode> { + // Safety: syscall2's documentation indicates it can be used to call Memop. + let [r0, r1] = + unsafe { Self::syscall2::<{ syscall_class::MEMOP }>([0u32.into(), addr.into()]) }; + let return_variant: ReturnVariant = r0.as_u32().into(); + // TRD 104 guarantees that memop 0, 10, and 11 return either Success + // or Failure. We check the return variant by comparing against Failure + // for 2 reasons: + // + // 1. On RISC-V with compressed instructions, it generates smaller + // code. FAILURE has value 0, which can be loaded into a + // register with a single compressed instruction, whereas + // loading SUCCESS uses an uncompressed instruction. + // 2. In the event the kernel malfunctions and returns a different + // return variant, the success path is actually safer than the + // failure path. The failure path assumes that r1 contains an + // ErrorCode, and produces UB if it has an out of range value. + if return_variant == return_variant::FAILURE { + // Safety: TRD 104 guarantees that if r0 is Failure, + // 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()) }) + } else { + Ok(()) + } + } + + // Safety: Callers of this function must ensure that they do not pass an + // increment that would deallocate memory containing any currently + // reachable object. + unsafe fn memop_sbrk(incr: i32) -> Result<*const u8, ErrorCode> { + // Safety: syscall2's documentation indicates it can be used to call Memop. + let [r0, r1] = + unsafe { Self::syscall2::<{ syscall_class::MEMOP }>([1u32.into(), incr.into()]) }; + let return_variant: ReturnVariant = r0.as_u32().into(); + // TRD 104 guarantees that memop 1, returns either Success with U32 + // or Failure. We check the return variant by comparing against Failure + // for 1 reason: + // + // 1. On RISC-V with compressed instructions, it generates smaller + // code. FAILURE has value 0, which can be loaded into a + // register with a single compressed instruction, whereas + // loading SUCCESS_U32 uses an uncompressed instruction. + if return_variant == return_variant::FAILURE { + // Safety: TRD 104 guarantees that if r0 is Failure, + // 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()) }) + } else { + Ok(r1.into()) + } + } + + fn memop_increment_brk(incr: u32) -> Result<*const u8, ErrorCode> { + // Safety: memop_sbrk is safe if the passed increment is positive + unsafe { Self::memop_sbrk(i32::try_from(incr).map_err(|_| ErrorCode::Invalid)?) } + } + + fn memop_app_ram_start() -> Result<*const u8, ErrorCode> { + // Safety: syscall1's documentation indicates it can be used to call Memop operations + // that only accept a memop operation number. + let [r0, r1] = unsafe { Self::syscall1::<{ syscall_class::MEMOP }>([2u32.into()]) }; + let return_variant: ReturnVariant = r0.as_u32().into(); + // TRD 104 guarantees that memop 2 returns either Success + // or Failure. We check the return variant by comparing against Failure + // for 1 reason: + // + // 1. On RISC-V with compressed instructions, it generates smaller + // code. FAILURE has value 0, which can be loaded into a + // register with a single compressed instruction, whereas + // loading SUCCESS_U32 uses an uncompressed instruction. + if return_variant == return_variant::FAILURE { + // Safety: TRD 104 guarantees that if r0 is Failure, + // 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()) }) + } else { + Ok(r1.into()) + } + } + + fn memop_debug_stack_start(stack_top: *const u8) -> Result<(), ErrorCode> { + // Safety: syscall2's documentation indicates it can be used to call Memop. + let [r0, r1] = + unsafe { Self::syscall2::<{ syscall_class::MEMOP }>([10u32.into(), stack_top.into()]) }; + let return_variant: ReturnVariant = r0.as_u32().into(); + // TRD 104 guarantees that memop 0, 10, and 11 return either Success + // or Failure. We check the return variant by comparing against Failure + // for 2 reasons: + // + // 1. On RISC-V with compressed instructions, it generates smaller + // code. FAILURE has value 0, which can be loaded into a + // register with a single compressed instruction, whereas + // loading SUCCESS uses an uncompressed instruction. + // 2. In the event the kernel malfunctions and returns a different + // return variant, the success path is actually safer than the + // failure path. The failure path assumes that r1 contains an + // ErrorCode, and produces UB if it has an out of range value. + if return_variant == return_variant::FAILURE { + // Safety: TRD 104 guarantees that if r0 is Failure, + // 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()) }) + } else { + Ok(()) + } + } + + fn memop_debug_heap_start(initial_break: *const u8) -> Result<(), ErrorCode> { + // Safety: syscall2's documentation indicates it can be used to call Memop. + let [r0, r1] = unsafe { + Self::syscall2::<{ syscall_class::MEMOP }>([11u32.into(), initial_break.into()]) + }; + let return_variant: ReturnVariant = r0.as_u32().into(); + // TRD 104 guarantees that memop 0, 10, and 11 return either Success + // or Failure. We check the return variant by comparing against Failure + // for 2 reasons: + // + // 1. On RISC-V with compressed instructions, it generates smaller + // code. FAILURE has value 0, which can be loaded into a + // register with a single compressed instruction, whereas + // loading SUCCESS uses an uncompressed instruction. + // 2. In the event the kernel malfunctions and returns a different + // return variant, the success path is actually safer than the + // failure path. The failure path assumes that r1 contains an + // ErrorCode, and produces UB if it has an out of range value. + if return_variant == return_variant::FAILURE { + // Safety: TRD 104 guarantees that if r0 is Failure, + // 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()) }) + } else { + Ok(()) + } + } + // ------------------------------------------------------------------------- // Exit // ------------------------------------------------------------------------- diff --git a/runtime/src/startup/mod.rs b/runtime/src/startup/mod.rs index 769bfe9a..db2493a4 100644 --- a/runtime/src/startup/mod.rs +++ b/runtime/src/startup/mod.rs @@ -1,7 +1,7 @@ //! Runtime components related to process startup. use crate::TockSyscalls; -use libtock_platform::{syscall_class, RawSyscalls, Termination}; +use libtock_platform::{Syscalls, Termination}; // Include the correct `start` symbol (the program entry point) for the // architecture. @@ -85,21 +85,12 @@ extern "C" fn rust_start() -> ! { static rt_header: RtHeader; } - // TODO: Implement a safe memop API in libtock_platform and migrate these - // calls to that API. - // - // Safety: Memop operations 10 and 11 are always memory-safe, as they do not - // impact the execution of this process. #[cfg(not(feature = "no_debug_memop"))] + // Safety: rt_header is defined in the linker script, valid for its type, + // and not modified anywhere unsafe { - TockSyscalls::syscall2::<{ syscall_class::MEMOP }>([ - 10u32.into(), - rt_header.stack_top.into(), - ]); - TockSyscalls::syscall2::<{ syscall_class::MEMOP }>([ - 11u32.into(), - rt_header.initial_break.into(), - ]); + let _ = TockSyscalls::memop_debug_stack_start(rt_header.stack_top as *const u8); + let _ = TockSyscalls::memop_debug_heap_start(rt_header.initial_break as *const u8); } // Safety: libtock_unsafe_main is defined by the set_main! macro, and its @@ -108,3 +99,14 @@ extern "C" fn rust_start() -> ! { libtock_unsafe_main(); } } + +/// Function which an allocator can call to learn the initial +/// start of the heap region +pub fn get_heap_start() -> *mut () { + extern "Rust" { + static rt_header: RtHeader; + } + // Safety: rt_header is defined in the linker script, valid for its type, + // and not modified anywhere + unsafe { rt_header.initial_break } +} diff --git a/syscalls_tests/src/lib.rs b/syscalls_tests/src/lib.rs index 5c629777..353c0a90 100644 --- a/syscalls_tests/src/lib.rs +++ b/syscalls_tests/src/lib.rs @@ -24,7 +24,8 @@ mod exit_on_drop; // TODO: Add Exit. -// TODO: Add Memop. +#[cfg(test)] +mod memop_tests; #[cfg(test)] mod subscribe_tests; diff --git a/syscalls_tests/src/memop_tests.rs b/syscalls_tests/src/memop_tests.rs new file mode 100644 index 00000000..f7715f0d --- /dev/null +++ b/syscalls_tests/src/memop_tests.rs @@ -0,0 +1,152 @@ +//! Tests for the Memop system call implementation in +//! `libtock_platform::Syscalls`. + +use libtock_platform::{ErrorCode, Syscalls}; +use libtock_unittest::{fake, ExpectedSyscall, SyscallLogEntry}; + +#[test] +fn memop() { + let kernel = fake::Kernel::new(); + kernel.add_expected_syscall(ExpectedSyscall::Memop { + memop_num: 1, + argument0: 3.into(), + return_error: Some(ErrorCode::NoMem), + }); + assert_eq!( + unsafe { fake::Syscalls::memop_sbrk(3) }, + Err(ErrorCode::NoMem) + ); + assert_eq!( + kernel.take_syscall_log(), + [SyscallLogEntry::Memop { + memop_num: 1, + argument0: 3.into(), + }] + ); +} + +#[test] +fn brk_test() { + let kernel = fake::Kernel::new(); + let fake_mem_buf = [0; 8]; + kernel.add_expected_syscall(ExpectedSyscall::Memop { + memop_num: 0, + argument0: fake_mem_buf.as_ptr().into(), + return_error: None, + }); + assert_eq!( + unsafe { fake::Syscalls::memop_brk(fake_mem_buf.as_ptr()) }, + Ok(()) + ); + assert_eq!( + kernel.take_syscall_log(), + [SyscallLogEntry::Memop { + memop_num: 0, + argument0: fake_mem_buf.as_ptr().into(), + }] + ); +} + +#[test] +fn sbrk_test() { + let kernel = fake::Kernel::new(); + let fake_mem_buf = [0; 8]; + kernel.add_expected_syscall(ExpectedSyscall::Memop { + memop_num: 0, + argument0: fake_mem_buf.as_ptr().into(), + return_error: None, + }); + assert_eq!( + unsafe { fake::Syscalls::memop_brk(fake_mem_buf.as_ptr()) }, + Ok(()) + ); + kernel.add_expected_syscall(ExpectedSyscall::Memop { + memop_num: 1, + argument0: 4.into(), + return_error: None, + }); + assert_eq!( + unsafe { fake::Syscalls::memop_sbrk(4) }, + Ok((&fake_mem_buf[4]) as *const u8) + ); +} + +#[test] +fn increment_brk_test() { + let kernel = fake::Kernel::new(); + let fake_mem_buf = [0; 8]; + kernel.add_expected_syscall(ExpectedSyscall::Memop { + memop_num: 0, + argument0: fake_mem_buf.as_ptr().into(), + return_error: None, + }); + assert_eq!( + unsafe { fake::Syscalls::memop_brk(fake_mem_buf.as_ptr()) }, + Ok(()) + ); + kernel.add_expected_syscall(ExpectedSyscall::Memop { + memop_num: 1, + argument0: 4.into(), + return_error: None, + }); + assert_eq!( + fake::Syscalls::memop_increment_brk(4), + Ok((&fake_mem_buf[4]) as *const u8) + ); +} + +#[test] +fn app_ram_start_test() { + let kernel = fake::Kernel::new(); + kernel.add_expected_syscall(ExpectedSyscall::Memop { + memop_num: 2, + argument0: 0.into(), + return_error: None, + }); + assert!(fake::Syscalls::memop_app_ram_start().is_ok()); + assert_eq!( + kernel.take_syscall_log(), + [SyscallLogEntry::Memop { + memop_num: 2, + argument0: 0.into(), + }] + ); +} + +#[test] +fn debug_stack_start_test() { + let kernel = fake::Kernel::new(); + let fake_stack = [0; 8]; + kernel.add_expected_syscall(ExpectedSyscall::Memop { + memop_num: 10, + argument0: fake_stack.as_ptr().into(), + return_error: None, + }); + assert!(fake::Syscalls::memop_debug_stack_start(fake_stack.as_ptr()).is_ok()); + assert_eq!( + kernel.take_syscall_log(), + [SyscallLogEntry::Memop { + memop_num: 10, + argument0: fake_stack.as_ptr().into(), + }] + ); +} + +#[test] +fn debug_heap_start_test() { + let kernel = fake::Kernel::new(); + let fake_heap = [0; 8]; + kernel.add_expected_syscall(ExpectedSyscall::Memop { + memop_num: 11, + argument0: fake_heap.as_ptr().into(), + return_error: None, + }); + assert!(fake::Syscalls::memop_debug_heap_start(fake_heap.as_ptr()).is_ok()); + assert_eq!( + kernel.take_syscall_log(), + [SyscallLogEntry::Memop { + memop_num: 11, + argument0: fake_heap.as_ptr().into(), + }] + ); +} diff --git a/unittest/src/expected_syscall.rs b/unittest/src/expected_syscall.rs index 6ace1b11..64193bb1 100644 --- a/unittest/src/expected_syscall.rs +++ b/unittest/src/expected_syscall.rs @@ -1,3 +1,5 @@ +use libtock_platform::Register; + /// Unit tests can use `ExpectedSyscall` to alter `fake::Kernel`'s behavior for /// a particular system call. An example use case is error injection: unit tests /// can add a `ExpectedSyscall` to the fake kernel's queue to insert errors in @@ -70,7 +72,18 @@ pub enum ExpectedSyscall { // invoked and the provided error will be returned instead. return_error: Option, }, - // TODO: Add Memop. + + // ------------------------------------------------------------------------- + // Memop + // ------------------------------------------------------------------------- + Memop { + memop_num: u32, + argument0: Register, // Necessary for Miri ptr provenance tests of brk + + // If set to Some(_), the driver's memop method will not be + // invoked and the provided error will be returned instead. + return_error: Option, + }, // TODO: Add Exit. } diff --git a/unittest/src/fake/kernel.rs b/unittest/src/fake/kernel.rs index 1eaa1337..c26c72b8 100644 --- a/unittest/src/fake/kernel.rs +++ b/unittest/src/fake/kernel.rs @@ -37,6 +37,7 @@ impl Kernel { expected_syscalls: Default::default(), syscall_log: Vec::new(), upcall_queue: Default::default(), + memory_break: core::ptr::null(), })) }); if let Some(old_kernel_data) = old_option { diff --git a/unittest/src/fake/syscalls/memop_impl.rs b/unittest/src/fake/syscalls/memop_impl.rs new file mode 100644 index 00000000..e97798e3 --- /dev/null +++ b/unittest/src/fake/syscalls/memop_impl.rs @@ -0,0 +1,92 @@ +//! `fake::Kernel`'s implementation of the Memop system call. + +use crate::kernel_data::with_kernel_data; +use crate::{ExpectedSyscall, SyscallLogEntry}; +use libtock_platform::{return_variant, ErrorCode, Register}; +use std::convert::TryInto; + +pub(super) fn memop(memop_num: Register, argument0: Register) -> [Register; 2] { + let memop_num = memop_num.try_into().expect("Too large memop num"); + + let (return_error, memop_return, memop_r1) = with_kernel_data(|option_kernel_data| { + let kernel_data = option_kernel_data.expect("Memop called but no fake::Kernel exists"); + + kernel_data.syscall_log.push(SyscallLogEntry::Memop { + memop_num, + argument0, + }); + + // Check for an expected syscall entry. Sets return_error to None if + // the expected syscall queue is empty or if it expected this syscall + // but did not specify a return override. Panics if a different syscall + // was expected (either a non-Memop syscall, or a Memop call with + // different arguments). + let return_error = match kernel_data.expected_syscalls.pop_front() { + None => None, + Some(ExpectedSyscall::Memop { + memop_num: expected_memop_num, + argument0: expected_argument0, + return_error, + }) => { + assert_eq!( + memop_num, expected_memop_num, + "expected different memop_num" + ); + assert_eq!( + usize::from(argument0), + usize::from(expected_argument0), + "expected different argument0" + ); + return_error + } + Some(expected_syscall) => expected_syscall.panic_wrong_call("Memop"), + }; + + // Emulate the memop call + // TODO: This emulation could be improved by adding data to kernel_data to allow us to + // better track what input arguments might be expected to return errors. + let (memop_return, memop_r1) = match memop_num { + 0 => { + /* brk */ + if Into::<*const u8>::into(argument0).is_null() { + (return_variant::FAILURE, ErrorCode::Invalid.into()) + } else { + kernel_data.memory_break = argument0.into(); + (return_variant::SUCCESS, 0.into()) + } + } + 1 => { + /* sbrk */ + let current_brk = kernel_data.memory_break; + let new_brk = current_brk.wrapping_byte_offset(argument0.as_i32() as isize); + kernel_data.memory_break = new_brk; + (return_variant::SUCCESS, kernel_data.memory_break.into()) + } + 2 => { + /* app_ram_start */ + // just pick a random number to always return, for now + (return_variant::SUCCESS, 0x123400.into()) + } + 10 => { + /* debug_stack_start */ + (return_variant::SUCCESS, 0.into()) + } + 11 => { + /* debug_heap_start */ + (return_variant::SUCCESS, 0.into()) + } + _ => { + panic!("Memop num not supported by test infrastructure"); + } + }; + (return_error, memop_return, memop_r1) + }); + + // Convert the return value into the representative register values. + // If there is an return_error, return a Failure along with that ErrorCode. + let (return_variant, r1) = return_error.map_or((memop_return, memop_r1), |override_errcode| { + (return_variant::FAILURE, override_errcode.into()) + }); + let r0: u32 = return_variant.into(); + [r0.into(), r1] +} diff --git a/unittest/src/fake/syscalls/memop_impl_tests.rs b/unittest/src/fake/syscalls/memop_impl_tests.rs new file mode 100644 index 00000000..eab3e338 --- /dev/null +++ b/unittest/src/fake/syscalls/memop_impl_tests.rs @@ -0,0 +1,84 @@ +use super::memop_impl::*; +use crate::{fake, ExpectedSyscall}; +use libtock_platform::{return_variant, ErrorCode, ReturnVariant}; +use std::convert::TryInto; +use std::panic::catch_unwind; + +// Tests memop with expected syscalls that don't match this memop call. +#[test] +fn expected_wrong_memop() { + let kernel = fake::Kernel::new(); + let expected_syscall = ExpectedSyscall::Memop { + memop_num: 1, + argument0: 1u32.into(), + return_error: None, + }; + + kernel.add_expected_syscall(expected_syscall); + assert!(catch_unwind(|| memop(0u32.into(), 1u32.into())) + .expect_err("failed to catch wrong memop_num") + .downcast_ref::() + .expect("wrong panic payload type") + .contains("expected different memop_num")); + + kernel.add_expected_syscall(expected_syscall); + assert!(catch_unwind(|| memop(1u32.into(), 0u32.into())) + .expect_err("failed to catch wrong memop argument0") + .downcast_ref::() + .expect("wrong panic payload type") + .contains("expected different argument0")); +} + +#[test] +fn no_kernel() { + let result = catch_unwind(|| memop(1u32.into(), 1u32.into())); + assert!(result + .expect_err("failed to catch missing kernel") + .downcast_ref::() + .expect("wrong panic payload type") + .contains("no fake::Kernel exists")); +} + +#[test] +fn return_error() { + let kernel = fake::Kernel::new(); + kernel.add_expected_syscall(ExpectedSyscall::Memop { + memop_num: 1, + argument0: 4u32.into(), + return_error: Some(ErrorCode::NoMem), + }); + let [r0, r1] = memop(1u32.into(), 4u32.into()); + let r0: u32 = r0.try_into().expect("too large r0"); + let r1: u32 = r1.try_into().expect("too large r1"); + let return_variant: ReturnVariant = r0.into(); + assert_eq!(return_variant, return_variant::FAILURE); + assert_eq!(r1, ErrorCode::NoMem as u32); +} + +#[cfg(target_pointer_width = "64")] +#[test] +fn too_large_memop_num() { + let _kernel = fake::Kernel::new(); + let result = catch_unwind(|| memop((u32::MAX as usize + 1).into(), 1u32.into())); + assert!(result + .expect_err("failed to catch too-large memop num") + .downcast_ref::() + .expect("wrong panic payload type") + .contains("Too large memop num")); +} + +#[test] +fn memop_using_syscall1() { + let kernel = fake::Kernel::new(); + kernel.add_expected_syscall(ExpectedSyscall::Memop { + memop_num: 2, + argument0: 0u32.into(), + return_error: None, + }); + let [r0, r1] = memop(2u32.into(), 0u32.into()); + let r0: u32 = r0.try_into().expect("too large r0"); + let _r1: u32 = r1.try_into().expect("too large r1"); + let return_variant: ReturnVariant = r0.into(); + assert_eq!(return_variant, return_variant::SUCCESS); + // No assertion for return value, could be any value from real kernel. +} diff --git a/unittest/src/fake/syscalls/mod.rs b/unittest/src/fake/syscalls/mod.rs index 7ba3f197..c55f5824 100644 --- a/unittest/src/fake/syscalls/mod.rs +++ b/unittest/src/fake/syscalls/mod.rs @@ -2,6 +2,7 @@ mod allow_ro_impl; mod allow_rw_impl; mod command_impl; mod exit_impl; +mod memop_impl; mod raw_syscalls_impl; mod subscribe_impl; mod yield_impl; @@ -20,6 +21,8 @@ mod command_impl_tests; #[cfg(all(not(miri), test))] mod exit_impl_tests; #[cfg(test)] +mod memop_impl_tests; +#[cfg(test)] mod raw_syscalls_impl_tests; #[cfg(test)] mod subscribe_impl_tests; diff --git a/unittest/src/fake/syscalls/raw_syscalls_impl.rs b/unittest/src/fake/syscalls/raw_syscalls_impl.rs index e2bca0e8..a2423795 100644 --- a/unittest/src/fake/syscalls/raw_syscalls_impl.rs +++ b/unittest/src/fake/syscalls/raw_syscalls_impl.rs @@ -25,9 +25,9 @@ unsafe impl RawSyscalls for crate::fake::Syscalls { } } - unsafe fn syscall1([Register(_r0)]: [Register; 1]) -> [Register; 2] { + unsafe fn syscall1([r0]: [Register; 1]) -> [Register; 2] { match CLASS { - syscall_class::MEMOP => unimplemented!("TODO: Add Memop"), + syscall_class::MEMOP => super::memop_impl::memop(r0, 0u32.into()), _ => panic!("Unknown syscall1 call. Class: {}", CLASS), } } @@ -35,7 +35,7 @@ unsafe impl RawSyscalls for crate::fake::Syscalls { unsafe fn syscall2([r0, r1]: [Register; 2]) -> [Register; 2] { crate::fake::syscalls::assert_valid((r0, r1)); match CLASS { - syscall_class::MEMOP => unimplemented!("TODO: Add Memop"), + syscall_class::MEMOP => super::memop_impl::memop(r0, r1), syscall_class::EXIT => super::exit_impl::exit(r0, r1), _ => panic!("Unknown syscall2 call. Class: {}", CLASS), } diff --git a/unittest/src/fake/syscalls/raw_syscalls_impl_tests.rs b/unittest/src/fake/syscalls/raw_syscalls_impl_tests.rs index 64ec6b97..5b3b5407 100644 --- a/unittest/src/fake/syscalls/raw_syscalls_impl_tests.rs +++ b/unittest/src/fake/syscalls/raw_syscalls_impl_tests.rs @@ -52,7 +52,27 @@ fn allow_rw() { // TODO: Implement Exit. -// TODO: Implement Memop. +#[test] +fn memop() { + let kernel = fake::Kernel::new(); + unsafe { + fake::Syscalls::syscall2::<{ syscall_class::MEMOP }>([1u32.into(), 2u32.into()]); + fake::Syscalls::syscall1::<{ syscall_class::MEMOP }>([2u32.into()]); + } + assert_eq!( + kernel.take_syscall_log(), + [ + SyscallLogEntry::Memop { + memop_num: 1, + argument0: 2.into(), + }, + SyscallLogEntry::Memop { + memop_num: 2, + argument0: 0.into(), + } + ] + ); +} // TODO: Implement Subscribe. diff --git a/unittest/src/kernel_data.rs b/unittest/src/kernel_data.rs index 3948ee41..6d0388b8 100644 --- a/unittest/src/kernel_data.rs +++ b/unittest/src/kernel_data.rs @@ -23,6 +23,7 @@ pub(crate) struct KernelData { pub expected_syscalls: std::collections::VecDeque, pub syscall_log: Vec, pub upcall_queue: crate::upcall::UpcallQueue, + pub memory_break: *const u8, } // KERNEL_DATA is set to Some in `fake::Kernel::new` and set to None when the diff --git a/unittest/src/syscall_log.rs b/unittest/src/syscall_log.rs index 74f1ab48..cccf27c8 100644 --- a/unittest/src/syscall_log.rs +++ b/unittest/src/syscall_log.rs @@ -1,3 +1,5 @@ +use libtock_platform::Register; + /// SyscallLogEntry represents a system call made during test execution. #[derive(Debug, Eq, PartialEq)] pub enum SyscallLogEntry { @@ -43,6 +45,13 @@ pub enum SyscallLogEntry { buffer_num: u32, len: usize, }, - // TODO: Add Memop. + + // ------------------------------------------------------------------------- + // Memop + // ------------------------------------------------------------------------- + Memop { + memop_num: u32, + argument0: Register, // Necessary for Miri ptr provenance of brk() + }, // TODO: Add Exit. }