diff --git a/.gitignore b/.gitignore index 669bd5bf4..d0ba2d0ec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ target/ *.rs.bk *.pyc -host + +# devenv-installed directory +/host + core.* diff --git a/benchmarks/lucet-benchmarks/src/modules.rs b/benchmarks/lucet-benchmarks/src/modules.rs index 7e655a795..426364bb4 100644 --- a/benchmarks/lucet-benchmarks/src/modules.rs +++ b/benchmarks/lucet-benchmarks/src/modules.rs @@ -1,4 +1,5 @@ -use lucet_runtime::vmctx::lucet_vmctx; +use lucet_runtime::lucet_hostcalls; +use lucet_runtime::vmctx::{lucet_vmctx, Vmctx}; use lucet_runtime_internals::module::{HeapSpec, MockModuleBuilder, Module}; use lucet_wasi_sdk::{CompileOpts, Lucetc}; use lucetc::{Bindings, LucetcOpts}; @@ -11,7 +12,7 @@ fn wasi_bindings() -> Bindings { pub fn compile_hello>(so_file: P) { let wasm_build = Lucetc::new(&["guests/hello.c"]) - .print_output(true) + .with_print_output(true) .with_cflag("-Wall") .with_cflag("-Werror") .with_bindings(wasi_bindings()); @@ -175,3 +176,79 @@ pub fn many_args_mock() -> Arc { .with_export_func(b"f", f as *const extern "C" fn()) .build() } + +pub fn hostcalls_mock() -> Arc { + lucet_hostcalls! { + #[inline(never)] + #[no_mangle] + pub unsafe extern "C" fn hostcall_wrapped( + &mut vmctx, + x1: u64, + x2: u64, + x3: u64, + x4: u64, + x5: u64, + x6: u64, + x7: u64, + x8: u64, + x9: u64, + x10: u64, + x11: u64, + x12: u64, + x13: u64, + x14: u64, + x15: u64, + x16: u64, + ) -> () { + vmctx.heap_mut()[0] = + (x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8 + x9 + x10 + x11 + x12 + x13 + x14 + x15 + x16) + as u8; + assert_eq!(vmctx.heap()[0], 136); + } + } + + #[inline(never)] + #[no_mangle] + pub unsafe extern "C" fn hostcall_raw( + vmctx: *mut lucet_vmctx, + x1: u64, + x2: u64, + x3: u64, + x4: u64, + x5: u64, + x6: u64, + x7: u64, + x8: u64, + x9: u64, + x10: u64, + x11: u64, + x12: u64, + x13: u64, + x14: u64, + x15: u64, + x16: u64, + ) { + let vmctx = Vmctx::from_raw(vmctx); + vmctx.heap_mut()[0] = + (x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8 + x9 + x10 + x11 + x12 + x13 + x14 + x15 + x16) + as u8; + assert_eq!(vmctx.heap()[0], 136); + } + + unsafe extern "C" fn wrapped(vmctx: *mut lucet_vmctx) { + for _ in 0..1000 { + hostcall_wrapped(vmctx, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16); + } + } + + unsafe extern "C" fn raw(vmctx: *mut lucet_vmctx) { + for _ in 0..1000 { + hostcall_raw(vmctx, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16); + } + } + + MockModuleBuilder::new() + .with_export_func(b"wrapped", wrapped as *const extern "C" fn()) + .with_export_func(b"raw", raw as *const extern "C" fn()) + .build() +} diff --git a/benchmarks/lucet-benchmarks/src/seq.rs b/benchmarks/lucet-benchmarks/src/seq.rs index baa3c484b..c090693b6 100644 --- a/benchmarks/lucet-benchmarks/src/seq.rs +++ b/benchmarks/lucet-benchmarks/src/seq.rs @@ -350,6 +350,40 @@ fn run_many_args(c: &mut Criterion) { }); } +fn run_hostcall_wrapped(c: &mut Criterion) { + fn body(inst: &mut InstanceHandle) { + inst.run(b"wrapped", &[]).unwrap(); + } + + let module = hostcalls_mock(); + let region = R::create(1, &Limits::default()).unwrap(); + + c.bench_function(&format!("run_hostcall_wrapped ({})", R::TYPE_NAME), move |b| { + b.iter_batched_ref( + || region.new_instance(module.clone()).unwrap(), + |inst| body(inst), + criterion::BatchSize::PerIteration, + ) + }); +} + +fn run_hostcall_raw(c: &mut Criterion) { + fn body(inst: &mut InstanceHandle) { + inst.run(b"raw", &[]).unwrap(); + } + + let module = hostcalls_mock(); + let region = R::create(1, &Limits::default()).unwrap(); + + c.bench_function(&format!("run_hostcall_raw ({})", R::TYPE_NAME), move |b| { + b.iter_batched_ref( + || region.new_instance(module.clone()).unwrap(), + |inst| body(inst), + criterion::BatchSize::PerIteration, + ) + }); +} + pub fn seq_benches(c: &mut Criterion) { load_mkregion_and_instantiate::(c); instantiate::(c); @@ -362,4 +396,6 @@ pub fn seq_benches(c: &mut Criterion) { run_fib::(c); run_hello::(c); run_many_args::(c); + run_hostcall_wrapped::(c); + run_hostcall_raw::(c); } diff --git a/lucet-runtime/Cargo.toml b/lucet-runtime/Cargo.toml index 272ac1200..33dfb8084 100644 --- a/lucet-runtime/Cargo.toml +++ b/lucet-runtime/Cargo.toml @@ -26,6 +26,7 @@ name = "lucet_runtime" crate-type = ["rlib", "staticlib", "cdylib"] [package.metadata.deb] +name = "fst-lucet-runtime" maintainer = "Adam C. Foltzer " depends = "$auto" priority = "optional" diff --git a/lucet-runtime/lucet-runtime-internals/src/c_api.rs b/lucet-runtime/lucet-runtime-internals/src/c_api.rs index 037f66141..5ce4c6502 100644 --- a/lucet-runtime/lucet-runtime-internals/src/c_api.rs +++ b/lucet-runtime/lucet-runtime-internals/src/c_api.rs @@ -55,7 +55,8 @@ macro_rules! with_ffi_arcs { /// Marker type for the `vmctx` pointer argument. /// -/// This type should only be used with [`Vmctx::from_raw()`](struct.Vmctx.html#method.from_raw). +/// This type should only be used with [`Vmctx::from_raw()`](struct.Vmctx.html#method.from_raw) or +/// the C API. #[repr(C)] pub struct lucet_vmctx { _unused: [u8; 0], @@ -194,8 +195,15 @@ pub type lucet_signal_handler = unsafe extern "C" fn( pub type lucet_fatal_handler = unsafe extern "C" fn(inst: *mut lucet_instance); +pub struct CTerminationDetails { + pub details: *mut c_void, +} + +unsafe impl Send for CTerminationDetails {} +unsafe impl Sync for CTerminationDetails {} + pub mod lucet_state { - use crate::c_api::lucet_val; + use crate::c_api::{lucet_val, CTerminationDetails}; use crate::instance::{State, TerminationDetails}; use crate::module::AddrDetails; use crate::sysdeps::UContext; @@ -243,15 +251,19 @@ pub mod lucet_state { reason: lucet_terminated_reason::Signal, provided: std::ptr::null_mut(), }, - TerminationDetails::GetEmbedCtx => lucet_terminated { - reason: lucet_terminated_reason::GetEmbedCtx, + TerminationDetails::CtxNotFound => lucet_terminated { + reason: lucet_terminated_reason::CtxNotFound, + provided: std::ptr::null_mut(), + }, + TerminationDetails::BorrowError(_) => lucet_terminated { + reason: lucet_terminated_reason::BorrowError, provided: std::ptr::null_mut(), }, TerminationDetails::Provided(p) => lucet_terminated { reason: lucet_terminated_reason::Provided, provided: p .downcast_ref() - .map(|v| *v) + .map(|CTerminationDetails { details }| *details) .unwrap_or(std::ptr::null_mut()), }, }, @@ -298,7 +310,8 @@ pub mod lucet_state { #[derive(Clone, Copy)] pub enum lucet_terminated_reason { Signal, - GetEmbedCtx, + CtxNotFound, + BorrowError, Provided, } diff --git a/lucet-runtime/lucet-runtime-internals/src/embed_ctx.rs b/lucet-runtime/lucet-runtime-internals/src/embed_ctx.rs index 7b9f98f0e..3affe2217 100644 --- a/lucet-runtime/lucet-runtime-internals/src/embed_ctx.rs +++ b/lucet-runtime/lucet-runtime-internals/src/embed_ctx.rs @@ -1,4 +1,5 @@ use std::any::{Any, TypeId}; +use std::cell::{BorrowError, BorrowMutError, Ref, RefCell, RefMut}; use std::collections::HashMap; /// A map that holds at most one value of any type. @@ -6,7 +7,7 @@ use std::collections::HashMap; /// This is similar to the type provided by the `anymap` crate, but we can get away with simpler /// types on the methods due to our more specialized use case. pub struct CtxMap { - map: HashMap>, + map: HashMap>>, } impl CtxMap { @@ -18,25 +19,33 @@ impl CtxMap { self.map.contains_key(&TypeId::of::()) } - pub fn get(&self) -> Option<&T> { + pub fn try_get(&self) -> Option, BorrowError>> { self.map.get(&TypeId::of::()).map(|x| { - x.downcast_ref::() - .expect("value stored with TypeId::of:: is always type T") + x.try_borrow().map(|r| { + Ref::map(r, |b| { + b.downcast_ref::() + .expect("value stored with TypeId::of:: is always type T") + }) + }) }) } - pub fn get_mut(&mut self) -> Option<&mut T> { + pub fn try_get_mut(&mut self) -> Option, BorrowMutError>> { self.map.get_mut(&TypeId::of::()).map(|x| { - x.downcast_mut::() - .expect("value stored with TypeId::of:: is always type T") + x.try_borrow_mut().map(|r| { + RefMut::map(r, |b| { + b.downcast_mut::() + .expect("value stored with TypeId::of:: is always type T") + }) + }) }) } pub fn insert(&mut self, x: T) -> Option { self.map - .insert(TypeId::of::(), Box::new(x) as Box) + .insert(TypeId::of::(), RefCell::new(Box::new(x) as Box)) .map(|x_prev| { - *x_prev + *(x_prev.into_inner()) .downcast::() .expect("value stored with TypeId::of:: is always type T") }) @@ -50,7 +59,8 @@ impl CtxMap { pub fn remove(&mut self) -> Option { self.map.remove(&TypeId::of::()).map(|x| { - *x.downcast::() + *(x.into_inner()) + .downcast::() .expect("value stored with TypeId::of:: is always type T") }) } diff --git a/lucet-runtime/lucet-runtime-internals/src/hostcall_macros.rs b/lucet-runtime/lucet-runtime-internals/src/hostcall_macros.rs new file mode 100644 index 000000000..1ff975fc4 --- /dev/null +++ b/lucet-runtime/lucet-runtime-internals/src/hostcall_macros.rs @@ -0,0 +1,92 @@ +/// The macro that surrounds definitions of Lucet hostcalls in Rust. +/// +/// It is important to use this macro for hostcalls, rather than exporting them directly, as it +/// installs unwind protection that prevents panics from unwinding into the guest stack. +/// +/// Since this is not yet a proc macro, the syntax is unfortunately fairly brittle. The functions it +/// encloses must be of the form: +/// +/// ```ignore +/// #[$attr1] +/// #[$attr2] +/// ... // any number of attributes are supported; in most cases you will want `#[no_mangle]` +/// pub unsafe extern "C" fn $name( // must be `pub unsafe extern "C"` +/// &mut $vmctx, +/// $arg1: $arg1_ty, +/// $arg2: $arg2_ty, +/// ... , // trailing comma must always be present +/// ) -> $ret_ty { // return type must always be present even if it is `()` +/// // body +/// } +/// ``` +#[macro_export] +macro_rules! lucet_hostcalls { + { + $( + $(#[$attr:meta])* + pub unsafe extern "C" fn $name:ident( + &mut $vmctx:ident + $(, $arg:ident : $arg_ty:ty )*, + ) -> $ret_ty:ty { + $($body:tt)* + } + )* + } => { + $( + $(#[$attr])* + pub unsafe extern "C" fn $name( + vmctx_raw: *mut $crate::vmctx::lucet_vmctx, + $( $arg: $arg_ty ),* + ) -> $ret_ty { + #[inline(always)] + unsafe fn hostcall_impl( + $vmctx: &mut $crate::vmctx::Vmctx, + $( $arg : $arg_ty ),* + ) -> $ret_ty { + $($body)* + } + let res = std::panic::catch_unwind(move || { + hostcall_impl(&mut $crate::vmctx::Vmctx::from_raw(vmctx_raw), $( $arg ),*) + }); + match res { + Ok(res) => res, + Err(e) => { + if let Some(details) = e.downcast_ref::<$crate::instance::TerminationDetails>() { + let mut vmctx = $crate::vmctx::Vmctx::from_raw(vmctx_raw); + vmctx.terminate_no_unwind(details.clone()); + } else { + std::panic::resume_unwind(e); + } + } + } + } + )* + } +} + +/// Terminate an instance from within a hostcall, returning an optional value as an error. +/// +/// Use this instead of `panic!` when you want the instance to terminate, but not the entire host +/// program. Like `panic!`, you can pass a format string with arguments, a value that implements +/// `Any`, or nothing to return a default message. +/// +/// Upon termination, the call to `Instance::run()` will return with an +/// `Err(Error::RuntimeTerminated)` value containing the value you pass to this macro. +/// +/// This macro safely unwinds the hostcall stack out to the entrypoint of the hostcall, so any +/// resources that may have been acquired will be properly dropped. +#[macro_export] +macro_rules! lucet_hostcall_terminate { + () => { + lucet_hostcall_terminate!("lucet_hostcall_terminate") + }; + ( $payload:expr ) => { + panic!($crate::instance::TerminationDetails::provide($payload)) + }; + ( $payload:expr, ) => { + lucet_hostcall_terminate!($payload) + }; + ( $fmt:expr, $($arg:tt)+ ) => { + lucet_hostcall_terminate!(format!($fmt, $($arg),+)) + }; +} diff --git a/lucet-runtime/lucet-runtime-internals/src/instance.rs b/lucet-runtime/lucet-runtime-internals/src/instance.rs index bc3d9359c..566e2f2fe 100644 --- a/lucet-runtime/lucet-runtime-internals/src/instance.rs +++ b/lucet-runtime/lucet-runtime-internals/src/instance.rs @@ -17,7 +17,7 @@ use libc::{c_void, siginfo_t, uintptr_t, SIGBUS, SIGSEGV}; use lucet_module_data::TrapCode; use memoffset::offset_of; use std::any::Any; -use std::cell::{RefCell, UnsafeCell}; +use std::cell::{BorrowError, BorrowMutError, Ref, RefCell, RefMut, UnsafeCell}; use std::ffi::{CStr, CString}; use std::mem; use std::ops::{Deref, DerefMut}; @@ -425,13 +425,13 @@ impl Instance { } /// Get a reference to a context value of a particular type, if it exists. - pub fn get_embed_ctx(&self) -> Option<&T> { - self.embed_ctx.get::() + pub fn get_embed_ctx(&self) -> Option, BorrowError>> { + self.embed_ctx.try_get::() } /// Get a mutable reference to a context value of a particular type, if it exists. - pub fn get_embed_ctx_mut(&mut self) -> Option<&mut T> { - self.embed_ctx.get_mut::() + pub fn get_embed_ctx_mut(&mut self) -> Option, BorrowMutError>> { + self.embed_ctx.try_get_mut::() } /// Insert a context value. @@ -753,15 +753,18 @@ impl std::fmt::Display for FaultDetails { /// error has occurred in a hostcall, rather than in WebAssembly code. #[derive(Clone)] pub enum TerminationDetails { + /// Returned when a signal handler terminates the instance. Signal, - GetEmbedCtx, - /// Calls to `Vmctx::terminate()` may attach an arbitrary pointer for extra debugging - /// information. - Provided(Arc), + /// Returned when `get_embed_ctx` or `get_embed_ctx_mut` are used with a type that is not present. + CtxNotFound, + /// Returned when dynamic borrowing rules of methods like `Vmctx::heap()` are violated. + BorrowError(&'static str), + /// Calls to `lucet_hostcall_terminate` provide a payload for use by the embedder. + Provided(Arc), } impl TerminationDetails { - pub fn provide(details: A) -> Self { + pub fn provide(details: A) -> Self { TerminationDetails::Provided(Arc::new(details)) } pub fn provided_details(&self) -> Option<&dyn Any> { @@ -785,17 +788,28 @@ fn termination_details_any_typing() { ); } +impl PartialEq for TerminationDetails { + fn eq(&self, rhs: &TerminationDetails) -> bool { + use TerminationDetails::*; + match (self, rhs) { + (Signal, Signal) => true, + (BorrowError(msg1), BorrowError(msg2)) => msg1 == msg2, + (CtxNotFound, CtxNotFound) => true, + // can't compare `Any` + _ => false, + } + } +} + impl std::fmt::Debug for TerminationDetails { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "TerminationDetails::{}", - match self { - TerminationDetails::Signal => "Signal", - TerminationDetails::GetEmbedCtx => "GetEmbedCtx", - TerminationDetails::Provided(_) => "Provided(Any)", - } - ) + write!(f, "TerminationDetails::")?; + match self { + TerminationDetails::Signal => write!(f, "Signal"), + TerminationDetails::BorrowError(msg) => write!(f, "BorrowError({})", msg), + TerminationDetails::CtxNotFound => write!(f, "CtxNotFound"), + TerminationDetails::Provided(_) => write!(f, "Provided(Any)"), + } } } diff --git a/lucet-runtime/lucet-runtime-internals/src/instance/signals.rs b/lucet-runtime/lucet-runtime-internals/src/instance/signals.rs index da5c1d2a7..68dc76ca6 100644 --- a/lucet-runtime/lucet-runtime-internals/src/instance/signals.rs +++ b/lucet-runtime/lucet-runtime-internals/src/instance/signals.rs @@ -11,7 +11,8 @@ use lucet_module_data::TrapCode; use nix::sys::signal::{ pthread_sigmask, raise, sigaction, SaFlags, SigAction, SigHandler, SigSet, SigmaskHow, Signal, }; -use std::sync::Mutex; +use std::panic; +use std::sync::{Arc, Mutex}; lazy_static! { // TODO: work out an alternative to this that is signal-safe for `reraise_host_signal_in_handler` @@ -223,6 +224,7 @@ struct SignalState { saved_sigfpe: SigAction, saved_sigill: SigAction, saved_sigsegv: SigAction, + saved_panic_hook: Option>>, } // raw pointers in the saved types @@ -246,21 +248,50 @@ unsafe fn setup_guest_signal_state(ostate: &mut Option) { let saved_sigill = sigaction(Signal::SIGILL, &sa).expect("sigaction succeeds"); let saved_sigsegv = sigaction(Signal::SIGSEGV, &sa).expect("sigaction succeeds"); + let saved_panic_hook = Some(setup_guest_panic_hook()); + *ostate = Some(SignalState { counter: 1, saved_sigbus, saved_sigfpe, saved_sigill, saved_sigsegv, + saved_panic_hook, }); } +fn setup_guest_panic_hook() -> Arc> { + let saved_panic_hook = Arc::new(panic::take_hook()); + let closure_saved_panic_hook = saved_panic_hook.clone(); + std::panic::set_hook(Box::new(move |panic_info| { + if panic_info + .payload() + .downcast_ref::() + .is_none() + { + closure_saved_panic_hook(panic_info); + } else { + // this is a panic used to implement instance termination (such as + // `lucet_hostcall_terminate!`), so we don't want to print a backtrace; instead, we do + // nothing + } + })); + saved_panic_hook +} + unsafe fn restore_host_signal_state(state: &mut SignalState) { // restore signal handlers sigaction(Signal::SIGBUS, &state.saved_sigbus).expect("sigaction succeeds"); sigaction(Signal::SIGFPE, &state.saved_sigfpe).expect("sigaction succeeds"); sigaction(Signal::SIGILL, &state.saved_sigill).expect("sigaction succeeds"); sigaction(Signal::SIGSEGV, &state.saved_sigsegv).expect("sigaction succeeds"); + + // restore panic hook + drop(panic::take_hook()); + state + .saved_panic_hook + .take() + .map(|hook| Arc::try_unwrap(hook).map(|hook| panic::set_hook(hook))); } unsafe fn reraise_host_signal_in_handler( diff --git a/lucet-runtime/lucet-runtime-internals/src/lib.rs b/lucet-runtime/lucet-runtime-internals/src/lib.rs index 821f916db..fb7103d4e 100644 --- a/lucet-runtime/lucet-runtime-internals/src/lib.rs +++ b/lucet-runtime/lucet-runtime-internals/src/lib.rs @@ -6,6 +6,8 @@ #[macro_use] pub mod error; +#[macro_use] +pub mod hostcall_macros; #[macro_use] #[cfg(test)] diff --git a/lucet-runtime/lucet-runtime-internals/src/module.rs b/lucet-runtime/lucet-runtime-internals/src/module.rs index 7acf040e1..43672c5f5 100644 --- a/lucet-runtime/lucet-runtime-internals/src/module.rs +++ b/lucet-runtime/lucet-runtime-internals/src/module.rs @@ -1,5 +1,4 @@ mod dl; -mod globals; mod mock; mod sparse_page_data; diff --git a/lucet-runtime/lucet-runtime-internals/src/module/globals.rs b/lucet-runtime/lucet-runtime-internals/src/module/globals.rs deleted file mode 100644 index 8616eb1bc..000000000 --- a/lucet-runtime/lucet-runtime-internals/src/module/globals.rs +++ /dev/null @@ -1,113 +0,0 @@ -#[macro_export] -macro_rules! globals_tests { - ( $TestRegion:path ) => { - use std::sync::Arc; - use $TestRegion as TestRegion; - use $crate::alloc::Limits; - use $crate::error::Error; - use $crate::module::{MockModuleBuilder, Module}; - use $crate::region::Region; - use $crate::vmctx::{lucet_vmctx, Vmctx}; - - fn mock_import_module() -> Arc { - MockModuleBuilder::new() - .with_import(0, "something", "else") - .build() - } - - #[test] - fn reject_import() { - let module = mock_import_module(); - let region = TestRegion::create(1, &Limits::default()).expect("region can be created"); - match region.new_instance(module) { - Ok(_) => panic!("instance creation should not succeed"), - Err(Error::Unsupported(_)) => (), - Err(e) => panic!("unexpected error: {}", e), - } - } - - fn mock_globals_module() -> Arc { - extern "C" fn get_global0(vmctx: *mut lucet_vmctx) -> i64 { - unsafe { Vmctx::from_raw(vmctx) }.globals()[0] - } - - extern "C" fn set_global0(vmctx: *mut lucet_vmctx, val: i64) { - unsafe { Vmctx::from_raw(vmctx) }.globals_mut()[0] = val; - } - - extern "C" fn get_global1(vmctx: *mut lucet_vmctx) -> i64 { - unsafe { Vmctx::from_raw(vmctx) }.globals()[1] - } - - MockModuleBuilder::new() - .with_global(0, -1) - .with_global(1, 420) - .with_export_func(b"get_global0", get_global0 as *const extern "C" fn()) - .with_export_func(b"set_global0", set_global0 as *const extern "C" fn()) - .with_export_func(b"get_global1", get_global1 as *const extern "C" fn()) - .build() - } - - /* replace with use of instance public api to make sure defined globals are initialized - * correctly - */ - - #[test] - fn globals_initialized() { - let module = mock_globals_module(); - let region = TestRegion::create(1, &Limits::default()).expect("region can be created"); - let inst = region - .new_instance(module) - .expect("instance can be created"); - assert_eq!(inst.globals()[0], -1); - assert_eq!(inst.globals()[1], 420); - } - - #[test] - fn get_global0() { - let module = mock_globals_module(); - let region = TestRegion::create(1, &Limits::default()).expect("region can be created"); - let mut inst = region - .new_instance(module) - .expect("instance can be created"); - - let retval = inst.run(b"get_global0", &[]).expect("instance runs"); - assert_eq!(i64::from(retval), -1); - } - - #[test] - fn get_both_globals() { - let module = mock_globals_module(); - let region = TestRegion::create(1, &Limits::default()).expect("region can be created"); - let mut inst = region - .new_instance(module) - .expect("instance can be created"); - - let retval = inst.run(b"get_global0", &[]).expect("instance runs"); - assert_eq!(i64::from(retval), -1); - - let retval = inst.run(b"get_global1", &[]).expect("instance runs"); - assert_eq!(i64::from(retval), 420); - } - - #[test] - fn mutate_global0() { - let module = mock_globals_module(); - let region = TestRegion::create(1, &Limits::default()).expect("region can be created"); - let mut inst = region - .new_instance(module) - .expect("instance can be created"); - - inst.run(b"set_global0", &[666i64.into()]) - .expect("instance runs"); - - let retval = inst.run(b"get_global0", &[]).expect("instance runs"); - assert_eq!(i64::from(retval), 666); - } - }; -} - -#[cfg(test)] -mod tests { - globals_tests!(crate::region::mmap::MmapRegion); -} diff --git a/lucet-runtime/lucet-runtime-internals/src/vmctx.rs b/lucet-runtime/lucet-runtime-internals/src/vmctx.rs index 1c25e8a91..a1700b87f 100644 --- a/lucet-runtime/lucet-runtime-internals/src/vmctx.rs +++ b/lucet-runtime/lucet-runtime-internals/src/vmctx.rs @@ -9,15 +9,39 @@ use crate::alloc::instance_heap_offset; use crate::context::Context; use crate::error::Error; use crate::instance::{ - Instance, InstanceHandle, InstanceInternal, State, TerminationDetails, CURRENT_INSTANCE, - HOST_CTX, + Instance, InstanceInternal, State, TerminationDetails, CURRENT_INSTANCE, HOST_CTX, }; use std::any::Any; +use std::borrow::{Borrow, BorrowMut}; +use std::cell::{Ref, RefCell, RefMut}; /// An opaque handle to a running instance's context. #[derive(Debug)] pub struct Vmctx { vmctx: *mut lucet_vmctx, + /// A view of the underlying instance's heap. + /// + /// This must never be dropped automatically, as the view does not own the heap. Rather, this is + /// a value used to implement dynamic borrowing of the heap contents that are owned and managed + /// by the instance and its `Alloc`. + heap_view: RefCell>, + /// A view of the underlying instance's globals. + /// + /// This must never be dropped automatically, as the view does not own the globals. Rather, this + /// is a value used to implement dynamic borrowing of the globals that are owned and managed by + /// the instance and its `Alloc`. + globals_view: RefCell>, +} + +impl Drop for Vmctx { + fn drop(&mut self) { + let heap_view = self.heap_view.replace(Box::new([])); + let globals_view = self.globals_view.replace(Box::new([])); + // as described in the definition of `Vmctx`, we cannot allow the boxed views of the heap + // and globals to be dropped + Box::leak(heap_view); + Box::leak(globals_view); + } } pub trait VmctxInternal { @@ -46,13 +70,19 @@ impl VmctxInternal for Vmctx { } impl Vmctx { - /// Create a `Vmctx` from the compiler-inserted `vmctx` argument in a guest - /// function. + /// Create a `Vmctx` from the compiler-inserted `vmctx` argument in a guest function. + /// + /// This is almost certainly not what you want to use to get a `Vmctx`; instead use the `&mut + /// Vmctx` argument to a `lucet_hostcalls!`-wrapped function. pub unsafe fn from_raw(vmctx: *mut lucet_vmctx) -> Vmctx { - let res = Vmctx { vmctx }; - // we don't actually need the instance for this call, but asking for it here causes an - // earlier failure if the pointer isn't valid - assert!(res.instance().valid_magic()); + let inst = instance_from_vmctx(vmctx); + assert!(inst.valid_magic()); + + let res = Vmctx { + vmctx, + heap_view: RefCell::new(Box::<[u8]>::from_raw(inst.heap_mut())), + globals_view: RefCell::new(Box::<[i64]>::from_raw(inst.globals_mut())), + }; res } @@ -62,13 +92,55 @@ impl Vmctx { } /// Return the WebAssembly heap as a slice of bytes. - pub fn heap(&self) -> &[u8] { - self.instance().heap() + /// + /// If the heap is already mutably borrowed by `heap_mut()`, the instance will + /// terminate with `TerminationDetails::BorrowError`. + pub fn heap(&self) -> Ref<[u8]> { + unsafe { + self.reconstitute_heap_view_if_needed(); + } + let r = self + .heap_view + .try_borrow() + .unwrap_or_else(|_| panic!(TerminationDetails::BorrowError("heap"))); + Ref::map(r, |b| b.borrow()) } /// Return the WebAssembly heap as a mutable slice of bytes. - pub fn heap_mut(&mut self) -> &mut [u8] { - unsafe { self.instance_mut().heap_mut() } + /// + /// If the heap is already borrowed by `heap()` or `heap_mut()`, the instance will terminate + /// with `TerminationDetails::BorrowError`. + pub fn heap_mut(&self) -> RefMut<[u8]> { + unsafe { + self.reconstitute_heap_view_if_needed(); + } + let r = self + .heap_view + .try_borrow_mut() + .unwrap_or_else(|_| panic!(TerminationDetails::BorrowError("heap_mut"))); + RefMut::map(r, |b| b.borrow_mut()) + } + + /// Check whether the heap has grown, and replace the heap view if it has. + /// + /// This handles the case where `Vmctx::grow_memory()` and `Vmctx::heap()` are called in + /// sequence. Since `Vmctx::grow_memory()` takes `&mut self`, heap references cannot live across + /// it. + /// + /// TODO: There is still an unsound case, though, when a heap reference is held across a call + /// back into the guest via `Vmctx::get_func_from_idx()`. That guest code may grow the heap as + /// well, causing any outstanding heap references to become invalid. We will address this when + /// we rework the interface for calling back into the guest. + unsafe fn reconstitute_heap_view_if_needed(&self) { + let inst = self.instance_mut(); + if inst.heap_mut().len() != self.heap_view.borrow().len() { + let old_heap_view = self + .heap_view + .replace(Box::<[u8]>::from_raw(inst.heap_mut())); + // as described in the definition of `Vmctx`, we cannot allow the boxed view of the heap + // to be dropped + Box::leak(old_heap_view); + } } /// Check whether a given range in the host address space overlaps with the memory that backs @@ -82,24 +154,43 @@ impl Vmctx { self.instance().contains_embed_ctx::() } - /// Get a reference to a context value of a particular type. If it does not exist, - /// the context will terminate. - pub fn get_embed_ctx(&self) -> &T { - unsafe { self.instance_mut().get_embed_ctx_or_term() } + /// Get a reference to a context value of a particular type. + /// + /// If a context of that type does not exist, the instance will terminate with + /// `TerminationDetails::CtxNotFound`. + /// + /// If the context is already mutably borrowed by `get_embed_ctx_mut`, the instance will + /// terminate with `TerminationDetails::BorrowError`. + pub fn get_embed_ctx(&self) -> Ref { + match self.instance().embed_ctx.try_get::() { + Some(Ok(t)) => t, + Some(Err(_)) => panic!(TerminationDetails::BorrowError("get_embed_ctx")), + None => panic!(TerminationDetails::CtxNotFound), + } } - /// Get a mutable reference to a context value of a particular type> If it does not exist, - /// the context will terminate. - pub fn get_embed_ctx_mut(&mut self) -> &mut T { - unsafe { self.instance_mut().get_embed_ctx_mut_or_term() } + /// Get a mutable reference to a context value of a particular type. + /// + /// If a context of that type does not exist, the instance will terminate with + /// `TerminationDetails::CtxNotFound`. + /// + /// If the context is already borrowed by some other use of `get_embed_ctx` or + /// `get_embed_ctx_mut`, the instance will terminate with `TerminationDetails::BorrowError`. + pub fn get_embed_ctx_mut(&self) -> RefMut { + match unsafe { self.instance_mut().embed_ctx.try_get_mut::() } { + Some(Ok(t)) => t, + Some(Err(_)) => panic!(TerminationDetails::BorrowError("get_embed_ctx_mut")), + None => panic!(TerminationDetails::CtxNotFound), + } } - /// Terminate this guest and return to the host context. + /// Terminate this guest and return to the host context without unwinding. /// - /// This will return an `Error::RuntimeTerminated` value to the caller of `Instance::run()`. - pub fn terminate(&mut self, info: I) -> ! { - let details = TerminationDetails::provide(info); - unsafe { self.instance_mut().terminate(details) } + /// This is almost certainly not what you want to use to terminate an instance from a hostcall, + /// as any resources currently in scope will not be dropped. Instead, use + /// `lucet_hostcall_terminate!` which unwinds to the enclosing hostcall body. + pub unsafe fn terminate_no_unwind(&mut self, details: TerminationDetails) -> ! { + self.instance_mut().terminate(details) } /// Grow the guest memory by the given number of WebAssembly pages. @@ -110,13 +201,27 @@ impl Vmctx { } /// Return the WebAssembly globals as a slice of `i64`s. - pub fn globals(&self) -> &[i64] { - self.instance().globals() + /// + /// If the globals are already mutably borrowed by `globals_mut()`, the instance will terminate + /// with `TerminationDetails::BorrowError`. + pub fn globals(&self) -> Ref<[i64]> { + let r = self + .globals_view + .try_borrow() + .unwrap_or_else(|_| panic!(TerminationDetails::BorrowError("globals"))); + Ref::map(r, |b| b.borrow()) } /// Return the WebAssembly globals as a mutable slice of `i64`s. - pub fn globals_mut(&mut self) -> &mut [i64] { - unsafe { self.instance_mut().globals_mut() } + /// + /// If the globals are already borrowed by `globals()` or `globals_mut()`, the instance will + /// terminate with `TerminationDetails::BorrowError`. + pub fn globals_mut(&self) -> RefMut<[i64]> { + let r = self + .globals_view + .try_borrow_mut() + .unwrap_or_else(|_| panic!(TerminationDetails::BorrowError("globals_mut"))); + RefMut::map(r, |b| b.borrow_mut()) } /// Get a function pointer by WebAssembly table and function index. @@ -130,21 +235,24 @@ impl Vmctx { /// from its own context. /// /// ```no_run + /// use lucet_runtime_internals::{lucet_hostcalls, lucet_hostcall_terminate}; /// use lucet_runtime_internals::vmctx::{lucet_vmctx, Vmctx}; - /// #[no_mangle] - /// extern "C" fn hostcall_call_binop( - /// vmctx: *mut lucet_vmctx, - /// binop_table_idx: u32, - /// binop_func_idx: u32, - /// operand1: u32, - /// operand2: u32, - /// ) -> u32 { - /// let mut ctx = unsafe { Vmctx::from_raw(vmctx) }; - /// if let Ok(binop) = ctx.get_func_from_idx(binop_table_idx, binop_func_idx) { - /// let typed_binop = binop as *const extern "C" fn(*mut lucet_vmctx, u32, u32) -> u32; - /// unsafe { (*typed_binop)(vmctx, operand1, operand2) } - /// } else { - /// ctx.terminate("invalid function index") + /// + /// lucet_hostcalls! { + /// #[no_mangle] + /// pub unsafe extern "C" fn hostcall_call_binop( + /// &mut vmctx, + /// binop_table_idx: u32, + /// binop_func_idx: u32, + /// operand1: u32, + /// operand2: u32, + /// ) -> u32 { + /// if let Ok(binop) = vmctx.get_func_from_idx(binop_table_idx, binop_func_idx) { + /// let typed_binop = binop as *const extern "C" fn(*mut lucet_vmctx, u32, u32) -> u32; + /// unsafe { (*typed_binop)(vmctx.as_raw(), operand1, operand2) } + /// } else { + /// lucet_hostcall_terminate!("invalid function index") + /// } /// } /// } pub fn get_func_from_idx( @@ -158,18 +266,6 @@ impl Vmctx { } } -/// Terminating an instance requires mutating the state field, and then jumping back to the -/// host context. The mutable borrow may conflict with a mutable borrow of the embed_ctx if -/// this is performed via a method call. We use a macro so we can convince the borrow checker that -/// this is safe at each use site. -macro_rules! inst_terminate { - ($self:ident, $details:expr) => {{ - $self.state = State::Terminated { details: $details }; - #[allow(unused_unsafe)] // The following unsafe will be incorrectly warned as unused - HOST_CTX.with(|host_ctx| unsafe { Context::set(&*host_ctx.get()) }) - }}; -} - /// Get an `Instance` from the `vmctx` pointer. /// /// Only safe to call from within the guest context. @@ -202,43 +298,13 @@ pub unsafe fn instance_from_vmctx<'a>(vmctx: *mut lucet_vmctx) -> &'a mut Instan } impl Instance { - /// Helper function specific to Vmctx::get_embed_ctx. From the vmctx interface, - /// there is no way to recover if the expected embedder ctx is not set, so we terminate - /// the instance. - fn get_embed_ctx_or_term(&mut self) -> &T { - match self.embed_ctx.get::() { - Some(t) => t, - None => inst_terminate!(self, TerminationDetails::GetEmbedCtx), - } - } - - /// Helper function specific to Vmctx::get_embed_ctx_mut. See above. - fn get_embed_ctx_mut_or_term(&mut self) -> &mut T { - match self.embed_ctx.get_mut::() { - Some(t) => t, - None => inst_terminate!(self, TerminationDetails::GetEmbedCtx), - } - } - - /// Terminate the guest and swap back to the host context. + /// Terminate the guest and swap back to the host context without unwinding. /// - /// Only safe to call from within the guest context. + /// This is almost certainly not what you want to use to terminate from a hostcall; use panics + /// with `TerminationDetails` instead. unsafe fn terminate(&mut self, details: TerminationDetails) -> ! { - inst_terminate!(self, details) + self.state = State::Terminated { details }; + #[allow(unused_unsafe)] // The following unsafe will be incorrectly warned as unused + HOST_CTX.with(|host_ctx| unsafe { Context::set(&*host_ctx.get()) }) } } - -/// Unsafely get a `Vmctx` from an `InstanceHandle`, and fake a current instance TLS variable. -/// -/// This is provided for compatibility with the Terrarium memory management test suite, but should -/// absolutely not be used in newer code. -#[deprecated] -pub unsafe fn vmctx_from_mock_instance(inst: &InstanceHandle) -> Vmctx { - CURRENT_INSTANCE.with(|current_instance| { - let mut current_instance = current_instance.borrow_mut(); - *current_instance = Some(std::ptr::NonNull::new_unchecked( - inst.alloc().slot().start as *mut Instance, - )); - }); - Vmctx::from_raw(inst.alloc().slot().heap as *mut lucet_vmctx) -} diff --git a/lucet-runtime/lucet-runtime-tests/guests/host/bindings.json b/lucet-runtime/lucet-runtime-tests/guests/host/bindings.json index 26b5c8b44..383bcf316 100644 --- a/lucet-runtime/lucet-runtime-tests/guests/host/bindings.json +++ b/lucet-runtime/lucet-runtime-tests/guests/host/bindings.json @@ -1,6 +1,7 @@ { "env": { "hostcall_test_func_hello": "hostcall_test_func_hello", - "hostcall_test_func_hostcall_error": "hostcall_test_func_hostcall_error" + "hostcall_test_func_hostcall_error": "hostcall_test_func_hostcall_error", + "hostcall_test_func_hostcall_error_unwind": "hostcall_test_func_hostcall_error_unwind" } } diff --git a/lucet-runtime/lucet-runtime-tests/guests/host/hostcall_error_unwind.c b/lucet-runtime/lucet-runtime-tests/guests/host/hostcall_error_unwind.c new file mode 100644 index 000000000..6b48ce840 --- /dev/null +++ b/lucet-runtime/lucet-runtime-tests/guests/host/hostcall_error_unwind.c @@ -0,0 +1,9 @@ +#include + +extern void hostcall_test_func_hostcall_error_unwind(void); + +int main(void) +{ + hostcall_test_func_hostcall_error_unwind(); + return 0; +} diff --git a/lucet-runtime/lucet-runtime-tests/src/entrypoint.rs b/lucet-runtime/lucet-runtime-tests/src/entrypoint.rs index 3a1820513..480534329 100644 --- a/lucet-runtime/lucet-runtime-tests/src/entrypoint.rs +++ b/lucet-runtime/lucet-runtime-tests/src/entrypoint.rs @@ -135,13 +135,20 @@ macro_rules! entrypoint_tests { ( $TestRegion:path ) => { use libc::c_void; use lucet_runtime::vmctx::{lucet_vmctx, Vmctx}; - use lucet_runtime::{DlModule, Error, Limits, Module, Region, Val, WASM_PAGE_SIZE}; + use lucet_runtime::{ + lucet_hostcalls, DlModule, Error, Limits, Module, Region, Val, WASM_PAGE_SIZE, + }; use std::sync::Arc; use $TestRegion as TestRegion; use $crate::entrypoint::{mock_calculator_module, wat_calculator_module}; - #[no_mangle] - extern "C" fn black_box(_vmctx: *mut lucet_vmctx, _val: *mut c_void) {} + lucet_hostcalls! { + #[no_mangle] + pub unsafe extern "C" fn black_box( + &mut _vmctx, + _val: *mut c_void, + ) -> () {} + } #[test] fn mock_calc_add_2() { @@ -711,14 +718,19 @@ macro_rules! entrypoint_tests { .expect("instance runs"); } - #[no_mangle] - extern "C" fn callback_hostcall(vmctx: *mut lucet_vmctx, cb_idx: u32, x: u64) -> u64 { - let vmctx = unsafe { Vmctx::from_raw(vmctx) }; - let func = vmctx - .get_func_from_idx(0, cb_idx) - .expect("can get function by index"); - let func = func as *const extern "C" fn(*mut lucet_vmctx, u64) -> u64; - unsafe { (*func)(vmctx.as_raw(), x) + 1 } + lucet_hostcalls! { + #[no_mangle] + pub unsafe extern "C" fn callback_hostcall( + &mut vmctx, + cb_idx: u32, + x: u64, + ) -> u64 { + let func = vmctx + .get_func_from_idx(0, cb_idx) + .expect("can get function by index"); + let func = func as *const extern "C" fn(*mut lucet_vmctx, u64) -> u64; + (*func)(vmctx.as_raw(), x) + 1 + } } #[test] diff --git a/lucet-runtime/lucet-runtime-tests/src/globals.rs b/lucet-runtime/lucet-runtime-tests/src/globals.rs index c6da6d35b..74a7a1a54 100644 --- a/lucet-runtime/lucet-runtime-tests/src/globals.rs +++ b/lucet-runtime/lucet-runtime-tests/src/globals.rs @@ -2,11 +2,11 @@ macro_rules! globals_tests { ( $TestRegion:path ) => { use lucet_runtime::vmctx::{lucet_vmctx, Vmctx}; - use lucet_runtime::{Limits, Region}; - use lucet_runtime_internals::instance::InstanceInternal; + use lucet_runtime::{Error, Limits, Module, Region}; use std::sync::Arc; use $TestRegion as TestRegion; use $crate::build::test_module_wasm; + use $crate::helpers::MockModuleBuilder; #[test] fn defined_globals() { @@ -28,7 +28,7 @@ macro_rules! globals_tests { // [4] = 5 // [8] = 6 - let heap_u32 = unsafe { inst.alloc().heap_u32() }; + let heap_u32 = unsafe { inst.heap_u32() }; assert_eq!(heap_u32[0..=2], [4, 5, 6]); inst.run(b"main", &[]).expect("instance runs"); @@ -38,8 +38,111 @@ macro_rules! globals_tests { // [4] = 2 // [8] = 6 - let heap_u32 = unsafe { inst.alloc().heap_u32() }; + let heap_u32 = unsafe { inst.heap_u32() }; assert_eq!(heap_u32[0..=2], [3, 2, 6]); } + + fn mock_import_module() -> Arc { + MockModuleBuilder::new() + .with_import(0, "something", "else") + .build() + } + + #[test] + fn reject_import() { + let module = mock_import_module(); + let region = TestRegion::create(1, &Limits::default()).expect("region can be created"); + match region.new_instance(module) { + Ok(_) => panic!("instance creation should not succeed"), + Err(Error::Unsupported(_)) => (), + Err(e) => panic!("unexpected error: {}", e), + } + } + + fn mock_globals_module() -> Arc { + extern "C" { + fn lucet_vmctx_get_globals(vmctx: *mut lucet_vmctx) -> *mut i64; + } + + unsafe extern "C" fn get_global0(vmctx: *mut lucet_vmctx) -> i64 { + let globals = std::slice::from_raw_parts(lucet_vmctx_get_globals(vmctx), 2); + globals[0] + } + + unsafe extern "C" fn set_global0(vmctx: *mut lucet_vmctx, val: i64) { + let globals = std::slice::from_raw_parts_mut(lucet_vmctx_get_globals(vmctx), 2); + globals[0] = val; + } + + unsafe extern "C" fn get_global1(vmctx: *mut lucet_vmctx) -> i64 { + let globals = std::slice::from_raw_parts(lucet_vmctx_get_globals(vmctx), 2); + globals[1] + } + + MockModuleBuilder::new() + .with_global(0, -1) + .with_global(1, 420) + .with_export_func(b"get_global0", get_global0 as *const extern "C" fn()) + .with_export_func(b"set_global0", set_global0 as *const extern "C" fn()) + .with_export_func(b"get_global1", get_global1 as *const extern "C" fn()) + .build() + } + + /* replace with use of instance public api to make sure defined globals are initialized + * correctly + */ + + #[test] + fn globals_initialized() { + let module = mock_globals_module(); + let region = TestRegion::create(1, &Limits::default()).expect("region can be created"); + let inst = region + .new_instance(module) + .expect("instance can be created"); + assert_eq!(inst.globals()[0], -1); + assert_eq!(inst.globals()[1], 420); + } + + #[test] + fn get_global0() { + let module = mock_globals_module(); + let region = TestRegion::create(1, &Limits::default()).expect("region can be created"); + let mut inst = region + .new_instance(module) + .expect("instance can be created"); + + let retval = inst.run(b"get_global0", &[]).expect("instance runs"); + assert_eq!(i64::from(retval), -1); + } + + #[test] + fn get_both_globals() { + let module = mock_globals_module(); + let region = TestRegion::create(1, &Limits::default()).expect("region can be created"); + let mut inst = region + .new_instance(module) + .expect("instance can be created"); + + let retval = inst.run(b"get_global0", &[]).expect("instance runs"); + assert_eq!(i64::from(retval), -1); + + let retval = inst.run(b"get_global1", &[]).expect("instance runs"); + assert_eq!(i64::from(retval), 420); + } + + #[test] + fn mutate_global0() { + let module = mock_globals_module(); + let region = TestRegion::create(1, &Limits::default()).expect("region can be created"); + let mut inst = region + .new_instance(module) + .expect("instance can be created"); + + inst.run(b"set_global0", &[666i64.into()]) + .expect("instance runs"); + + let retval = inst.run(b"get_global0", &[]).expect("instance runs"); + assert_eq!(i64::from(retval), 666); + } }; } diff --git a/lucet-runtime/lucet-runtime-tests/src/guest_fault.rs b/lucet-runtime/lucet-runtime-tests/src/guest_fault.rs index b8bf1d372..1a0303ad9 100644 --- a/lucet-runtime/lucet-runtime-tests/src/guest_fault.rs +++ b/lucet-runtime/lucet-runtime-tests/src/guest_fault.rs @@ -1,7 +1,7 @@ use crate::helpers::MockModuleBuilder; use lucet_module_data::{FunctionSpec, TrapCode, TrapSite}; use lucet_runtime_internals::module::Module; -use lucet_runtime_internals::vmctx::{lucet_vmctx, Vmctx}; +use lucet_runtime_internals::vmctx::lucet_vmctx; use std::sync::Arc; pub fn mock_traps_module() -> Arc { @@ -9,7 +9,7 @@ pub fn mock_traps_module() -> Arc { 123 } - extern "C" fn hostcall_main(vmctx: *mut lucet_vmctx) { + extern "C" fn hostcall_main(vmctx: *mut lucet_vmctx) -> () { extern "C" { // actually is defined in this file fn hostcall_test(vmctx: *mut lucet_vmctx); @@ -20,29 +20,33 @@ pub fn mock_traps_module() -> Arc { } } - extern "C" fn infinite_loop(_vmctx: *mut lucet_vmctx) { + extern "C" fn infinite_loop(_vmctx: *mut lucet_vmctx) -> () { loop {} } - extern "C" fn fatal(vmctx: *mut lucet_vmctx) { - let mut vmctx = unsafe { Vmctx::from_raw(vmctx) }; - let heap_base = vmctx.heap_mut().as_mut_ptr(); + extern "C" fn fatal(vmctx: *mut lucet_vmctx) -> () { + extern "C" { + fn lucet_vmctx_get_heap(vmctx: *mut lucet_vmctx) -> *mut u8; + } - // Using the default limits, each instance as of this writing takes up 0x200026000 bytes - // worth of virtual address space. We want to access a point beyond all the instances, so - // that memory is unmapped. We assume no more than 16 instances are mapped - // concurrently. This may change as the library, test configuration, linker, phase of moon, - // etc change, but for now it works. unsafe { + let heap_base = lucet_vmctx_get_heap(vmctx); + + // Using the default limits, each instance as of this writing takes up 0x200026000 bytes + // worth of virtual address space. We want to access a point beyond all the instances, + // so that memory is unmapped. We assume no more than 16 instances are mapped + // concurrently. This may change as the library, test configuration, linker, phase of + // moon, etc change, but for now it works. *heap_base.offset(0x200026000 * 16) = 0; } } - extern "C" fn recoverable_fatal(_vmctx: *mut lucet_vmctx) { + extern "C" fn recoverable_fatal(_vmctx: *mut lucet_vmctx) -> () { use std::os::raw::c_char; extern "C" { fn guest_recoverable_get_ptr() -> *mut c_char; } + unsafe { *guest_recoverable_get_ptr() = '\0' as c_char; } @@ -117,10 +121,10 @@ macro_rules! guest_fault_tests { ( $TestRegion:path ) => { use lazy_static::lazy_static; use libc::{c_void, siginfo_t, SIGSEGV}; - use lucet_module_data::TrapCode; use lucet_runtime::vmctx::{lucet_vmctx, Vmctx}; use lucet_runtime::{ - DlModule, Error, FaultDetails, Instance, Limits, Region, SignalBehavior, + lucet_hostcall_terminate, lucet_hostcalls, DlModule, Error, FaultDetails, Instance, + Limits, Region, SignalBehavior, TrapCode, }; use nix::sys::mman::{mmap, MapFlags, ProtFlags}; use nix::sys::signal::{sigaction, SaFlags, SigAction, SigHandler, SigSet, Signal}; @@ -175,9 +179,13 @@ macro_rules! guest_fault_tests { static HOSTCALL_TEST_ERROR: &'static str = "hostcall_test threw an error!"; - #[no_mangle] - unsafe extern "C" fn hostcall_test(vmctx: *mut lucet_vmctx) { - Vmctx::from_raw(vmctx).terminate(HOSTCALL_TEST_ERROR); + lucet_hostcalls! { + #[no_mangle] + pub unsafe extern "C" fn hostcall_test( + &mut _vmctx, + ) -> () { + lucet_hostcall_terminate!(HOSTCALL_TEST_ERROR); + } } fn run_onetwothree(inst: &mut Instance) { @@ -506,8 +514,12 @@ macro_rules! guest_fault_tests { *HOST_SIGSEGV_TRIGGERED.lock().unwrap() = true; } - extern "C" fn sleepy_guest(_vmctx: *const lucet_vmctx) { - std::thread::sleep(std::time::Duration::from_millis(20)); + lucet_hostcalls! { + pub unsafe extern "C" fn sleepy_guest( + &mut _vmctx, + ) -> () { + std::thread::sleep(std::time::Duration::from_millis(20)); + } } test_ex(|| { diff --git a/lucet-runtime/lucet-runtime-tests/src/helpers.rs b/lucet-runtime/lucet-runtime-tests/src/helpers.rs index 967810c9c..f91f8fa79 100644 --- a/lucet-runtime/lucet-runtime-tests/src/helpers.rs +++ b/lucet-runtime/lucet-runtime-tests/src/helpers.rs @@ -1,7 +1,5 @@ // re-export types that should only be used for testing pub use lucet_runtime_internals::module::{HeapSpec, MockModuleBuilder}; -#[allow(deprecated)] -pub use lucet_runtime_internals::vmctx::vmctx_from_mock_instance; use lazy_static::lazy_static; use std::sync::RwLock; diff --git a/lucet-runtime/lucet-runtime-tests/src/host.rs b/lucet-runtime/lucet-runtime-tests/src/host.rs index f46345133..e34a12aa6 100644 --- a/lucet-runtime/lucet-runtime-tests/src/host.rs +++ b/lucet-runtime/lucet-runtime-tests/src/host.rs @@ -1,13 +1,17 @@ #[macro_export] macro_rules! host_tests { ( $TestRegion:path ) => { + use lazy_static::lazy_static; use libc::c_void; - use lucet_module_data::TrapCode; use lucet_runtime::vmctx::{lucet_vmctx, Vmctx}; - use lucet_runtime::{DlModule, Error, Limits, Region}; - use std::sync::Arc; + use lucet_runtime::{ + lucet_hostcall_terminate, lucet_hostcalls, DlModule, Error, Limits, Region, + TerminationDetails, TrapCode, + }; + use std::sync::{Arc, Mutex}; use $TestRegion as TestRegion; use $crate::build::test_module_c; + use $crate::helpers::MockModuleBuilder; #[test] fn load_module() { let _module = test_module_c("host", "trivial.c").expect("build and load module"); @@ -19,30 +23,83 @@ macro_rules! host_tests { assert!(module.is_err()); } - #[no_mangle] - extern "C" fn hostcall_test_func_hello( - vmctx: *mut lucet_vmctx, - hello_ptr: u32, - hello_len: u32, - ) { - unsafe { - let mut vmctx = Vmctx::from_raw(vmctx); + const ERROR_MESSAGE: &'static str = "hostcall_test_func_hostcall_error"; + + lazy_static! { + static ref HOSTCALL_MUTEX: Mutex<()> = Mutex::new(()); + } + + lucet_hostcalls! { + #[no_mangle] + pub unsafe extern "C" fn hostcall_test_func_hello( + &mut vmctx, + hello_ptr: u32, + hello_len: u32, + ) -> () { let heap = vmctx.heap(); let hello = heap.as_ptr() as usize + hello_ptr as usize; if !vmctx.check_heap(hello as *const c_void, hello_len as usize) { - vmctx.terminate("heap access"); + lucet_hostcall_terminate!("heap access"); } let hello = std::slice::from_raw_parts(hello as *const u8, hello_len as usize); if hello.starts_with(b"hello") { *vmctx.get_embed_ctx_mut::() = true; } } - } - const ERROR_MESSAGE: &'static str = "hostcall_test_func_hostcall_error"; - #[no_mangle] - extern "C" fn hostcall_test_func_hostcall_error(vmctx: *mut lucet_vmctx) { - unsafe { Vmctx::from_raw(vmctx).terminate(ERROR_MESSAGE) } + #[no_mangle] + pub unsafe extern "C" fn hostcall_test_func_hostcall_error( + &mut _vmctx, + ) -> () { + lucet_hostcall_terminate!(ERROR_MESSAGE); + } + + #[allow(unreachable_code)] + #[no_mangle] + pub unsafe extern "C" fn hostcall_test_func_hostcall_error_unwind( + &mut vmctx, + ) -> () { + let lock = HOSTCALL_MUTEX.lock().unwrap(); + unsafe { + lucet_hostcall_terminate!(ERROR_MESSAGE); + } + drop(lock); + } + + #[no_mangle] + pub unsafe extern "C" fn hostcall_bad_borrow( + &mut vmctx, + ) -> bool { + let heap = vmctx.heap(); + let mut other_heap = vmctx.heap_mut(); + heap[0] == other_heap[0] + } + + #[no_mangle] + pub unsafe extern "C" fn hostcall_missing_embed_ctx( + &mut vmctx, + ) -> bool { + struct S { + x: bool + } + let ctx = vmctx.get_embed_ctx::(); + ctx.x + } + + #[no_mangle] + pub unsafe extern "C" fn hostcall_multiple_vmctx( + &mut vmctx, + ) -> bool { + let mut vmctx1 = Vmctx::from_raw(vmctx.as_raw()); + vmctx1.heap_mut()[0] = 0xAF; + drop(vmctx1); + + let mut vmctx2 = Vmctx::from_raw(vmctx.as_raw()); + let res = vmctx2.heap()[0] == 0xAF; + drop(vmctx2); + + res + } } #[test] @@ -77,7 +134,7 @@ macro_rules! host_tests { inst.run(b"main", &[]).expect("instance runs"); - assert!(inst.get_embed_ctx::().unwrap()); + assert!(*inst.get_embed_ctx::().unwrap().unwrap()); } #[test] @@ -103,6 +160,32 @@ macro_rules! host_tests { } } + #[test] + fn run_hostcall_error_unwind() { + let module = + test_module_c("host", "hostcall_error_unwind.c").expect("build and load module"); + let region = TestRegion::create(1, &Limits::default()).expect("region can be created"); + let mut inst = region + .new_instance(module) + .expect("instance can be created"); + + match inst.run(b"main", &[]) { + Err(Error::RuntimeTerminated(term)) => { + assert_eq!( + *term + .provided_details() + .expect("user provided termination reason") + .downcast_ref::<&'static str>() + .expect("error was static str"), + ERROR_MESSAGE + ); + } + res => panic!("unexpected result: {:?}", res), + } + + assert!(HOSTCALL_MUTEX.is_poisoned()); + } + #[test] fn run_fpe() { let module = test_module_c("host", "fpe.c").expect("build and load module"); @@ -120,5 +203,86 @@ macro_rules! host_tests { } } } + + #[test] + fn run_hostcall_bad_borrow() { + extern "C" { + fn hostcall_bad_borrow(vmctx: *mut lucet_vmctx) -> bool; + } + + unsafe extern "C" fn f(vmctx: *mut lucet_vmctx) { + hostcall_bad_borrow(vmctx); + } + + let module = MockModuleBuilder::new() + .with_export_func(b"f", f as *const extern "C" fn()) + .build(); + + let region = TestRegion::create(1, &Limits::default()).expect("region can be created"); + let mut inst = region + .new_instance(module) + .expect("instance can be created"); + + match inst.run(b"f", &[]) { + Err(Error::RuntimeTerminated(details)) => { + assert_eq!(details, TerminationDetails::BorrowError("heap_mut")); + } + res => { + panic!("unexpected result: {:?}", res); + } + } + } + + #[test] + fn run_hostcall_missing_embed_ctx() { + extern "C" { + fn hostcall_missing_embed_ctx(vmctx: *mut lucet_vmctx) -> bool; + } + + unsafe extern "C" fn f(vmctx: *mut lucet_vmctx) { + hostcall_missing_embed_ctx(vmctx); + } + + let module = MockModuleBuilder::new() + .with_export_func(b"f", f as *const extern "C" fn()) + .build(); + + let region = TestRegion::create(1, &Limits::default()).expect("region can be created"); + let mut inst = region + .new_instance(module) + .expect("instance can be created"); + + match inst.run(b"f", &[]) { + Err(Error::RuntimeTerminated(details)) => { + assert_eq!(details, TerminationDetails::CtxNotFound); + } + res => { + panic!("unexpected result: {:?}", res); + } + } + } + + #[test] + fn run_hostcall_multiple_vmctx() { + extern "C" { + fn hostcall_multiple_vmctx(vmctx: *mut lucet_vmctx) -> bool; + } + + unsafe extern "C" fn f(vmctx: *mut lucet_vmctx) { + hostcall_multiple_vmctx(vmctx); + } + + let module = MockModuleBuilder::new() + .with_export_func(b"f", f as *const extern "C" fn()) + .build(); + + let region = TestRegion::create(1, &Limits::default()).expect("region can be created"); + let mut inst = region + .new_instance(module) + .expect("instance can be created"); + + let retval = inst.run(b"f", &[]).expect("instance runs"); + assert_eq!(bool::from(retval), true); + } }; } diff --git a/lucet-runtime/lucet-runtime-tests/src/stack.rs b/lucet-runtime/lucet-runtime-tests/src/stack.rs index 791ab1713..3e448918a 100644 --- a/lucet-runtime/lucet-runtime-tests/src/stack.rs +++ b/lucet-runtime/lucet-runtime-tests/src/stack.rs @@ -83,8 +83,9 @@ fn generate_test_wat(num_locals: usize) -> String { #[macro_export] macro_rules! stack_tests { ( $TestRegion:path ) => { - use lucet_module_data::TrapCode; - use lucet_runtime::{DlModule, Error, InstanceHandle, Limits, Region, UntypedRetVal, Val}; + use lucet_runtime::{ + DlModule, Error, InstanceHandle, Limits, Region, TrapCode, UntypedRetVal, Val, + }; use std::sync::Arc; use $TestRegion as TestRegion; use $crate::stack::stack_testcase; diff --git a/lucet-runtime/lucet-runtime-tests/src/strcmp.rs b/lucet-runtime/lucet-runtime-tests/src/strcmp.rs index 2c6b9e260..80bbe415b 100644 --- a/lucet-runtime/lucet-runtime-tests/src/strcmp.rs +++ b/lucet-runtime/lucet-runtime-tests/src/strcmp.rs @@ -3,16 +3,20 @@ macro_rules! strcmp_tests { ( $TestRegion:path ) => { use libc::{c_char, c_int, c_void, strcmp, uint64_t}; use lucet_runtime::vmctx::lucet_vmctx; - use lucet_runtime::{Error, Limits, Region, Val, WASM_PAGE_SIZE}; + use lucet_runtime::{lucet_hostcalls, Error, Limits, Region, Val, WASM_PAGE_SIZE}; use std::ffi::CString; use std::sync::Arc; use $TestRegion as TestRegion; use $crate::build::test_module_c; - #[no_mangle] - unsafe extern "C" fn hostcall_host_fault(_vmctx: *const lucet_vmctx) { - let oob = (-1isize) as *mut c_char; - *oob = 'x' as c_char; + lucet_hostcalls! { + #[no_mangle] + pub unsafe extern "C" fn hostcall_host_fault( + &mut _vmctx, + ) -> () { + let oob = (-1isize) as *mut c_char; + *oob = 'x' as c_char; + } } fn strcmp_compare(s1: &str, s2: &str) { diff --git a/lucet-runtime/src/c_api.rs b/lucet-runtime/src/c_api.rs index 84c65c382..6be2fe2c9 100644 --- a/lucet-runtime/src/c_api.rs +++ b/lucet-runtime/src/c_api.rs @@ -1,5 +1,3 @@ -extern crate lucet_runtime_internals; - use crate::{DlModule, Instance, Limits, MmapRegion, Module, Region}; use libc::{c_char, c_int, c_void}; use lucet_module_data::TrapCode; @@ -7,9 +5,11 @@ use lucet_runtime_internals::c_api::*; use lucet_runtime_internals::instance::{ instance_handle_from_raw, instance_handle_to_raw, InstanceInternal, }; -use lucet_runtime_internals::vmctx::{instance_from_vmctx, lucet_vmctx, Vmctx, VmctxInternal}; +use lucet_runtime_internals::vmctx::VmctxInternal; use lucet_runtime_internals::WASM_PAGE_SIZE; -use lucet_runtime_internals::{assert_nonnull, with_ffi_arcs}; +use lucet_runtime_internals::{ + assert_nonnull, lucet_hostcall_terminate, lucet_hostcalls, with_ffi_arcs, +}; use num_traits::FromPrimitive; use std::ffi::CStr; use std::ptr; @@ -284,7 +284,7 @@ pub unsafe extern "C" fn lucet_instance_grow_heap( pub unsafe extern "C" fn lucet_instance_embed_ctx(inst: *mut lucet_instance) -> *mut c_void { with_instance_ptr_unchecked!(inst, { inst.get_embed_ctx::<*mut c_void>() - .map(|p| *p) + .map(|r| r.map(|p| *p).unwrap_or(ptr::null_mut())) .unwrap_or(ptr::null_mut()) }) } @@ -363,76 +363,87 @@ pub fn ensure_linked() { }); } -#[no_mangle] -pub unsafe extern "C" fn lucet_vmctx_get_heap(vmctx: *mut lucet_vmctx) -> *mut u8 { - Vmctx::from_raw(vmctx).instance().alloc().slot().heap as *mut u8 -} +lucet_hostcalls! { + #[no_mangle] + pub unsafe extern "C" fn lucet_vmctx_get_heap( + &mut vmctx, + ) -> *mut u8 { + vmctx.instance().alloc().slot().heap as *mut u8 + } -#[no_mangle] -pub unsafe extern "C" fn lucet_vmctx_get_globals(vmctx: *mut lucet_vmctx) -> *mut i64 { - Vmctx::from_raw(vmctx).instance().alloc().slot().globals as *mut i64 -} + #[no_mangle] + pub unsafe extern "C" fn lucet_vmctx_get_globals( + &mut vmctx, + ) -> *mut i64 { + vmctx.instance().alloc().slot().globals as *mut i64 + } -/// Get the number of WebAssembly pages currently in the heap. -#[no_mangle] -pub unsafe extern "C" fn lucet_vmctx_current_memory(vmctx: *mut lucet_vmctx) -> libc::uint32_t { - Vmctx::from_raw(vmctx).instance().alloc().heap_len() as u32 / WASM_PAGE_SIZE -} + #[no_mangle] + /// Get the number of WebAssembly pages currently in the heap. + pub unsafe extern "C" fn lucet_vmctx_current_memory( + &mut vmctx, + ) -> libc::uint32_t { + vmctx.instance().alloc().heap_len() as u32 / WASM_PAGE_SIZE + } -#[no_mangle] -/// Grows the guest heap by the given number of WebAssembly pages. -/// -/// On success, returns the number of pages that existed before the call. On failure, returns `-1`. -pub unsafe extern "C" fn lucet_vmctx_grow_memory( - vmctx: *mut lucet_vmctx, - additional_pages: libc::uint32_t, -) -> libc::int32_t { - let inst = instance_from_vmctx(vmctx); - if let Ok(old_pages) = inst.grow_memory(additional_pages) { - old_pages as libc::int32_t - } else { - -1 + #[no_mangle] + /// Grows the guest heap by the given number of WebAssembly pages. + /// + /// On success, returns the number of pages that existed before the call. On failure, returns `-1`. + pub unsafe extern "C" fn lucet_vmctx_grow_memory( + &mut vmctx, + additional_pages: libc::uint32_t, + ) -> libc::int32_t { + if let Ok(old_pages) = vmctx.instance_mut().grow_memory(additional_pages) { + old_pages as libc::int32_t + } else { + -1 + } } -} -#[no_mangle] -/// Check if a memory region is inside the instance heap. -pub unsafe extern "C" fn lucet_vmctx_check_heap( - vmctx: *mut lucet_vmctx, - ptr: *mut c_void, - len: libc::size_t, -) -> bool { - let inst = instance_from_vmctx(vmctx); - inst.check_heap(ptr, len) -} + #[no_mangle] + /// Check if a memory region is inside the instance heap. + pub unsafe extern "C" fn lucet_vmctx_check_heap( + &mut vmctx, + ptr: *mut c_void, + len: libc::size_t, + ) -> bool { + vmctx.instance().check_heap(ptr, len) + } -#[no_mangle] -pub unsafe extern "C" fn lucet_vmctx_get_func_from_idx( - vmctx: *mut lucet_vmctx, - table_idx: u32, - func_idx: u32, -) -> *const c_void { - let inst = instance_from_vmctx(vmctx); - inst.module() - .get_func_from_idx(table_idx, func_idx) - // the Rust API actually returns a pointer to a function pointer, so we want to dereference - // one layer of that to make it nicer in C - .map(|fptr| *(fptr as *const *const c_void)) - .unwrap_or(std::ptr::null()) -} + #[no_mangle] + pub unsafe extern "C" fn lucet_vmctx_get_func_from_idx( + &mut vmctx, + table_idx: u32, + func_idx: u32, + ) -> *const c_void { + vmctx.instance() + .module() + .get_func_from_idx(table_idx, func_idx) + // the Rust API actually returns a pointer to a function pointer, so we want to dereference + // one layer of that to make it nicer in C + .map(|fptr| *(fptr as *const *const c_void)) + .unwrap_or(std::ptr::null()) + } -#[no_mangle] -pub unsafe extern "C" fn lucet_vmctx_terminate(vmctx: *mut lucet_vmctx, info: *mut c_void) { - Vmctx::from_raw(vmctx).terminate(info); -} + #[no_mangle] + pub unsafe extern "C" fn lucet_vmctx_terminate( + &mut _vmctx, + details: *mut c_void, + ) -> () { + lucet_hostcall_terminate!(CTerminationDetails { details}); + } -#[no_mangle] -/// Get the delegate object for the current instance. -/// -/// TODO: rename -pub unsafe extern "C" fn lucet_vmctx_get_delegate(vmctx: *mut lucet_vmctx) -> *mut c_void { - let inst = instance_from_vmctx(vmctx); - inst.get_embed_ctx::<*mut c_void>() - .map(|p| *p) - .unwrap_or(std::ptr::null_mut()) + #[no_mangle] + /// Get the delegate object for the current instance. + /// + /// TODO: rename + pub unsafe extern "C" fn lucet_vmctx_get_delegate( + &mut vmctx, + ) -> *mut c_void { + vmctx.instance() + .get_embed_ctx::<*mut c_void>() + .map(|r| r.map(|p| *p).unwrap_or(ptr::null_mut())) + .unwrap_or(std::ptr::null_mut()) + } } diff --git a/lucet-runtime/src/lib.rs b/lucet-runtime/src/lib.rs index 9bedc2f2d..aeb91b0a3 100644 --- a/lucet-runtime/src/lib.rs +++ b/lucet-runtime/src/lib.rs @@ -65,23 +65,26 @@ //! demo](https://wasm.fastly-labs.com/), hostcalls are provided for manipulating HTTP requests, //! accessing a key/value store, etc. //! -//! Some simple hostcalls can be implemented simply as an exported C function that takes an opaque -//! pointer argument (usually called `vmctx`). Hostcalls that require access to some underlying -//! state, such as the key/value store in Terrarium, can access a custom embedder context through -//! `vmctx`. For example, to make a `u32` available to hostcalls: +//! Some simple hostcalls can be implemented by wrapping an externed C function with the +//! [`lucet_hostcalls!`](macro.lucet_hostcalls.html] macro. The function must take a special `&mut +//! vmctx` argument for the guest context, similar to `&mut self` on methods. Hostcalls that require +//! access to some underlying state, such as the key/value store in Terrarium, can access a custom +//! embedder context through `vmctx`. For example, to make a `u32` available to hostcalls: //! //! ```no_run -//! use lucet_runtime::{DlModule, Limits, MmapRegion, Region}; +//! use lucet_runtime::{DlModule, Limits, MmapRegion, Region, lucet_hostcalls}; //! use lucet_runtime::vmctx::{Vmctx, lucet_vmctx}; //! //! struct MyContext { x: u32 } //! -//! #[no_mangle] -//! unsafe extern "C" fn foo(vmctx: *mut lucet_vmctx) { -//! let mut vmctx = Vmctx::from_raw(vmctx); -//! let hostcall_context = vmctx -//! .get_embed_ctx_mut::(); -//! hostcall_context.x = 42; +//! lucet_hostcalls! { +//! #[no_mangle] +//! pub unsafe extern "C" fn foo( +//! &mut vmctx, +//! ) -> () { +//! let mut hostcall_context = vmctx.get_embed_ctx_mut::(); +//! hostcall_context.x = 42; +//! } //! } //! //! let module = DlModule::load("/my/lucet/module.so").unwrap(); @@ -94,7 +97,7 @@ //! //! inst.run(b"call_foo", &[]).unwrap(); //! -//! let context_after = inst.get_embed_ctx::().unwrap(); +//! let context_after = inst.get_embed_ctx::().unwrap().unwrap(); //! assert_eq!(context_after.x, 42); //! ``` //! @@ -143,9 +146,8 @@ //! signal handler increments a counter of signals it has seen before setting the fault state: //! //! ```no_run -//! use lucet_module_data::TrapCode; //! use lucet_runtime::{ -//! DlModule, Error, Instance, Limits, MmapRegion, Region, SignalBehavior +//! DlModule, Error, Instance, Limits, MmapRegion, Region, SignalBehavior, TrapCode, //! }; //! use std::sync::atomic::{AtomicUsize, Ordering, ATOMIC_USIZE_INIT}; //! @@ -196,8 +198,9 @@ //! that, for example, a `SIGSEGV` on a non-Lucet thread of a host program will still likely abort //! the entire process. -mod c_api; +pub mod c_api; +pub use lucet_module_data::TrapCode; pub use lucet_runtime_internals::alloc::Limits; pub use lucet_runtime_internals::error::Error; pub use lucet_runtime_internals::instance::{ @@ -207,7 +210,7 @@ pub use lucet_runtime_internals::module::{DlModule, Module}; pub use lucet_runtime_internals::region::mmap::MmapRegion; pub use lucet_runtime_internals::region::{InstanceBuilder, Region, RegionCreate}; pub use lucet_runtime_internals::val::{UntypedRetVal, Val}; -pub use lucet_runtime_internals::WASM_PAGE_SIZE; +pub use lucet_runtime_internals::{lucet_hostcall_terminate, lucet_hostcalls, WASM_PAGE_SIZE}; pub mod vmctx { //! Functions for manipulating instances from hostcalls. diff --git a/lucet-spectest/src/lib.rs b/lucet-spectest/src/lib.rs index e11c4e3ef..f555349a9 100644 --- a/lucet-spectest/src/lib.rs +++ b/lucet-spectest/src/lib.rs @@ -9,8 +9,7 @@ mod result; use crate::script::{ScriptEnv, ScriptError}; use failure::{format_err, Error, ResultExt}; -use lucet_module_data::TrapCode; -use lucet_runtime::{Error as RuntimeError, UntypedRetVal, Val}; +use lucet_runtime::{Error as RuntimeError, TrapCode, UntypedRetVal, Val}; use std::fs; use std::path::PathBuf; use wabt::script::{Action, CommandKind, ScriptParser, Value}; diff --git a/lucet-wasi-sdk/src/lib.rs b/lucet-wasi-sdk/src/lib.rs index 646a090e5..99fa9eada 100644 --- a/lucet-wasi-sdk/src/lib.rs +++ b/lucet-wasi-sdk/src/lib.rs @@ -86,8 +86,8 @@ pub trait CompileOpts { fn cflag>(&mut self, cflag: S); fn with_cflag>(self, cflag: S) -> Self; - fn include>(&mut self, include: S); - fn with_include>(self, include: S) -> Self; + fn include>(&mut self, include: S); + fn with_include>(self, include: S) -> Self; } impl CompileOpts for Compile { @@ -100,11 +100,12 @@ impl CompileOpts for Compile { self } - fn include>(&mut self, include: S) { - self.cflags.push(format!("-I{}", include.as_ref())); + fn include>(&mut self, include: S) { + self.cflags + .push(format!("-I{}", include.as_ref().display())); } - fn with_include>(mut self, include: S) -> Self { + fn with_include>(mut self, include: S) -> Self { self.include(include); self } @@ -150,6 +151,9 @@ impl Compile { for cflag in self.cflags.iter() { cmd.arg(cflag); } + if self.print_output { + println!("running: {:?}", cmd); + } let run = cmd.output().expect("clang executable exists"); CompileError::check(run, self.print_output) } @@ -208,6 +212,9 @@ impl Link { for ldflag in self.ldflags.iter() { cmd.arg(format!("-Wl,{}", ldflag)); } + if self.print_output { + println!("running: {:?}", cmd); + } let run = cmd.output().expect("clang executable exists"); CompileError::check(run, self.print_output) } @@ -303,13 +310,13 @@ impl CompileOpts for T { self } - fn include>(&mut self, include: S) { + fn include>(&mut self, include: S) { self.as_link() .cflags - .push(format!("-I{}", include.as_ref())); + .push(format!("-I{}", include.as_ref().display())); } - fn with_include>(mut self, include: S) -> Self { + fn with_include>(mut self, include: S) -> Self { self.include(include); self } @@ -336,8 +343,12 @@ impl Lucetc { } } - pub fn print_output(mut self, print: bool) -> Self { + pub fn print_output(&mut self, print: bool) { self.link.print_output = print; + } + + pub fn with_print_output(mut self, print: bool) -> Self { + self.print_output(print); self } diff --git a/lucet-wasi/Cargo.toml b/lucet-wasi/Cargo.toml index b20379ee2..8d9ba2025 100644 --- a/lucet-wasi/Cargo.toml +++ b/lucet-wasi/Cargo.toml @@ -29,6 +29,7 @@ name = "lucet_wasi" crate-type = ["rlib", "staticlib", "cdylib"] [package.metadata.deb] +name = "fst-lucet-wasi" maintainer = "Adam C. Foltzer " depends = "$auto" priority = "optional" diff --git a/lucet-wasi/include/lucet_wasi.h b/lucet-wasi/include/lucet_wasi.h index 92d4d05d8..b0c5d6edc 100644 --- a/lucet-wasi/include/lucet_wasi.h +++ b/lucet-wasi/include/lucet_wasi.h @@ -11,6 +11,8 @@ enum lucet_error lucet_wasi_ctx_args(struct lucet_wasi_ctx *wasi_ctx, size_t arg enum lucet_error lucet_wasi_ctx_inherit_env(struct lucet_wasi_ctx *wasi_ctx); +enum lucet_error lucet_wasi_ctx_inherit_stdio(struct lucet_wasi_ctx *wasi_ctx); + void lucet_wasi_ctx_destroy(struct lucet_wasi_ctx *wasi_ctx); enum lucet_error lucet_region_new_instance_with_wasi_ctx(const struct lucet_region * region, diff --git a/lucet-wasi/src/c_api.rs b/lucet-wasi/src/c_api.rs index 484b6c090..be8dc9416 100644 --- a/lucet-wasi/src/c_api.rs +++ b/lucet-wasi/src/c_api.rs @@ -45,6 +45,17 @@ pub unsafe extern "C" fn lucet_wasi_ctx_inherit_env(wasi_ctx: *mut lucet_wasi_ct lucet_error::Ok } +#[no_mangle] +pub unsafe extern "C" fn lucet_wasi_ctx_inherit_stdio( + wasi_ctx: *mut lucet_wasi_ctx, +) -> lucet_error { + assert_nonnull!(wasi_ctx); + let mut b = Box::from_raw(wasi_ctx as *mut WasiCtxBuilder); + *b = b.inherit_stdio(); + Box::into_raw(b); + lucet_error::Ok +} + #[no_mangle] pub unsafe extern "C" fn lucet_wasi_ctx_destroy(wasi_ctx: *mut lucet_wasi_ctx) { Box::from_raw(wasi_ctx as *mut WasiCtxBuilder); diff --git a/lucet-wasi/src/ctx.rs b/lucet-wasi/src/ctx.rs index fcb9bed5b..889ecfc99 100644 --- a/lucet-wasi/src/ctx.rs +++ b/lucet-wasi/src/ctx.rs @@ -19,15 +19,16 @@ pub struct WasiCtxBuilder { impl WasiCtxBuilder { /// Builder for a new `WasiCtx`. pub fn new() -> Self { + let null = dev_null(); WasiCtxBuilder { fds: HashMap::new(), preopens: HashMap::new(), args: vec![], env: HashMap::new(), } - .fd_dup(0, stdin()) - .fd_dup(1, stdout()) - .fd_dup(2, stderr()) + .fd_dup(0, &null) + .fd_dup(1, &null) + .fd_dup(2, &null) } pub fn args(mut self, args: &[&str]) -> Self { @@ -57,6 +58,12 @@ impl WasiCtxBuilder { self } + pub fn inherit_stdio(self) -> Self { + self.fd_dup(0, &stdin()) + .fd_dup(1, &stdout()) + .fd_dup(2, &stderr()) + } + pub fn inherit_env(mut self) -> Self { self.env = std::env::vars() .map(|(k, v)| { @@ -104,7 +111,7 @@ impl WasiCtxBuilder { /// context, so it will not be closed when the `WasiCtx` is dropped. /// /// TODO: handle `dup` errors - pub fn fd_dup(self, wasm_fd: host::__wasi_fd_t, fd: F) -> Self { + pub fn fd_dup(self, wasm_fd: host::__wasi_fd_t, fd: &F) -> Self { // safe because we're getting a valid RawFd from the F directly unsafe { self.raw_fd(wasm_fd, dup(fd.as_raw_fd()).unwrap()) } } @@ -189,6 +196,7 @@ impl WasiCtx { pub fn new(args: &[&str]) -> WasiCtx { WasiCtxBuilder::new() .args(args) + .inherit_stdio() .inherit_env() .build() .expect("default options don't fail") @@ -230,3 +238,7 @@ impl WasiCtx { Ok(fd) } } + +fn dev_null() -> File { + File::open("/dev/null").expect("failed to open /dev/null") +} diff --git a/lucet-wasi/src/hostcalls.rs b/lucet-wasi/src/hostcalls.rs index e6e17660d..ce88e81d7 100644 --- a/lucet-wasi/src/hostcalls.rs +++ b/lucet-wasi/src/hostcalls.rs @@ -9,13 +9,15 @@ //! something nice. #![allow(non_camel_case_types)] +#![allow(unused_unsafe)] use crate::ctx::WasiCtx; use crate::fdentry::{determine_type_rights, FdEntry}; use crate::memory::*; use crate::{host, wasm32}; use cast::From as _0; -use lucet_runtime::vmctx::{lucet_vmctx, Vmctx}; +use lucet_runtime::vmctx::Vmctx; +use lucet_runtime::{lucet_hostcall_terminate, lucet_hostcalls}; use nix::convert_ioctl_res; use nix::libc::c_int; @@ -30,828 +32,1054 @@ const O_RSYNC: nix::fcntl::OFlag = nix::fcntl::OFlag::O_RSYNC; #[cfg(not(target_os = "linux"))] const O_RSYNC: nix::fcntl::OFlag = nix::fcntl::OFlag::O_SYNC; -#[no_mangle] -pub extern "C" fn __wasi_proc_exit(vmctx: *mut lucet_vmctx, rval: wasm32::__wasi_exitcode_t) -> ! { - let mut vmctx = unsafe { Vmctx::from_raw(vmctx) }; - vmctx.terminate(dec_exitcode(rval)) -} +lucet_hostcalls! { + #[no_mangle] + pub unsafe extern "C" fn __wasi_proc_exit( + &mut _vmctx, + rval: wasm32::__wasi_exitcode_t, + ) -> ! { + lucet_hostcall_terminate!(dec_exitcode(rval)); + } -#[no_mangle] -pub extern "C" fn __wasi_args_get( - vmctx_raw: *mut lucet_vmctx, - argv_ptr: wasm32::uintptr_t, - argv_buf: wasm32::uintptr_t, -) -> wasm32::__wasi_errno_t { - let mut vmctx = unsafe { Vmctx::from_raw(vmctx_raw) }; - let ctx: &WasiCtx = vmctx.get_embed_ctx(); + #[no_mangle] + pub unsafe extern "C" fn __wasi_args_get( + &mut vmctx, + argv_ptr: wasm32::uintptr_t, + argv_buf: wasm32::uintptr_t, + ) -> wasm32::__wasi_errno_t { + let ctx = vmctx.get_embed_ctx::(); - let mut argv_buf_offset = 0; - let mut argv = vec![]; + let mut argv_buf_offset = 0; + let mut argv = vec![]; - for arg in ctx.args.iter() { - let arg_bytes = arg.as_bytes_with_nul(); - let arg_ptr = argv_buf + argv_buf_offset; + for arg in ctx.args.iter() { + let arg_bytes = arg.as_bytes_with_nul(); + let arg_ptr = argv_buf + argv_buf_offset; - // nasty aliasing here, but we aren't interfering with the borrow for `ctx` - // TODO: rework vmctx interface to avoid this - let mut vmctx = unsafe { Vmctx::from_raw(vmctx_raw) }; - if let Err(e) = unsafe { enc_slice_of(&mut vmctx, arg_bytes, arg_ptr) } { - return enc_errno(e); - } + if let Err(e) = unsafe { enc_slice_of(vmctx, arg_bytes, arg_ptr) } { + return enc_errno(e); + } - argv.push(arg_ptr); + argv.push(arg_ptr); - argv_buf_offset = if let Some(new_offset) = argv_buf_offset.checked_add( - wasm32::uintptr_t::cast(arg_bytes.len()) - .expect("cast overflow would have been caught by `enc_slice_of` above"), - ) { - new_offset - } else { - return wasm32::__WASI_EOVERFLOW; + argv_buf_offset = if let Some(new_offset) = argv_buf_offset.checked_add( + wasm32::uintptr_t::cast(arg_bytes.len()) + .expect("cast overflow would have been caught by `enc_slice_of` above"), + ) { + new_offset + } else { + return wasm32::__WASI_EOVERFLOW; + } } - } - unsafe { - enc_slice_of(&mut vmctx, argv.as_slice(), argv_ptr) - .map(|_| wasm32::__WASI_ESUCCESS) - .unwrap_or_else(|e| e) + unsafe { + enc_slice_of(vmctx, argv.as_slice(), argv_ptr) + .map(|_| wasm32::__WASI_ESUCCESS) + .unwrap_or_else(|e| e) + } } -} -#[no_mangle] -pub extern "C" fn __wasi_args_sizes_get( - vmctx: *mut lucet_vmctx, - argc_ptr: wasm32::uintptr_t, - argv_buf_size_ptr: wasm32::uintptr_t, -) -> wasm32::__wasi_errno_t { - let mut vmctx = unsafe { Vmctx::from_raw(vmctx) }; + #[no_mangle] + pub unsafe extern "C" fn __wasi_args_sizes_get( + &mut vmctx, + argc_ptr: wasm32::uintptr_t, + argv_buf_size_ptr: wasm32::uintptr_t, + ) -> wasm32::__wasi_errno_t { + let ctx = vmctx.get_embed_ctx::(); + + let argc = ctx.args.len(); + let argv_size = ctx + .args + .iter() + .map(|arg| arg.as_bytes_with_nul().len()) + .sum(); - let ctx: &WasiCtx = vmctx.get_embed_ctx(); + unsafe { + if let Err(e) = enc_usize_byref(vmctx, argc_ptr, argc) { + return enc_errno(e); + } + if let Err(e) = enc_usize_byref(vmctx, argv_buf_size_ptr, argv_size) { + return enc_errno(e); + } + } + wasm32::__WASI_ESUCCESS + } - let argc = ctx.args.len(); - let argv_size = ctx - .args - .iter() - .map(|arg| arg.as_bytes_with_nul().len()) - .sum(); + #[no_mangle] + pub unsafe extern "C" fn __wasi_clock_res_get( + &mut vmctx, + clock_id: wasm32::__wasi_clockid_t, + resolution_ptr: wasm32::uintptr_t, + ) -> wasm32::__wasi_errno_t { + // convert the supported clocks to the libc types, or return EINVAL + let clock_id = match dec_clockid(clock_id) { + host::__WASI_CLOCK_REALTIME => libc::CLOCK_REALTIME, + host::__WASI_CLOCK_MONOTONIC => libc::CLOCK_MONOTONIC, + host::__WASI_CLOCK_PROCESS_CPUTIME_ID => libc::CLOCK_PROCESS_CPUTIME_ID, + host::__WASI_CLOCK_THREAD_CPUTIME_ID => libc::CLOCK_THREAD_CPUTIME_ID, + _ => return wasm32::__WASI_EINVAL, + }; - unsafe { - if let Err(e) = enc_usize_byref(&mut vmctx, argc_ptr, argc) { - return enc_errno(e); - } - if let Err(e) = enc_usize_byref(&mut vmctx, argv_buf_size_ptr, argv_size) { - return enc_errno(e); + // no `nix` wrapper for clock_getres, so we do it ourselves + let mut timespec = unsafe { std::mem::uninitialized::() }; + let res = unsafe { libc::clock_getres(clock_id, &mut timespec as *mut libc::timespec) }; + if res != 0 { + return wasm32::errno_from_nix(nix::errno::Errno::last()); } - } - wasm32::__WASI_ESUCCESS -} -#[no_mangle] -pub extern "C" fn __wasi_clock_res_get( - vmctx: *mut lucet_vmctx, - clock_id: wasm32::__wasi_clockid_t, - resolution_ptr: wasm32::uintptr_t, -) -> wasm32::__wasi_errno_t { - let mut vmctx = unsafe { Vmctx::from_raw(vmctx) }; - - // convert the supported clocks to the libc types, or return EINVAL - let clock_id = match dec_clockid(clock_id) { - host::__WASI_CLOCK_REALTIME => libc::CLOCK_REALTIME, - host::__WASI_CLOCK_MONOTONIC => libc::CLOCK_MONOTONIC, - host::__WASI_CLOCK_PROCESS_CPUTIME_ID => libc::CLOCK_PROCESS_CPUTIME_ID, - host::__WASI_CLOCK_THREAD_CPUTIME_ID => libc::CLOCK_THREAD_CPUTIME_ID, - _ => return wasm32::__WASI_EINVAL, - }; - - // no `nix` wrapper for clock_getres, so we do it ourselves - let mut timespec = unsafe { std::mem::uninitialized::() }; - let res = unsafe { libc::clock_getres(clock_id, &mut timespec as *mut libc::timespec) }; - if res != 0 { - return wasm32::errno_from_nix(nix::errno::Errno::last()); + // convert to nanoseconds, returning EOVERFLOW in case of overflow; this is freelancing a bit + // from the spec but seems like it'll be an unusual situation to hit + (timespec.tv_sec as host::__wasi_timestamp_t) + .checked_mul(1_000_000_000) + .and_then(|sec_ns| sec_ns.checked_add(timespec.tv_nsec as host::__wasi_timestamp_t)) + .map(|resolution| { + // a supported clock can never return zero; this case will probably never get hit, but + // make sure we follow the spec + if resolution == 0 { + wasm32::__WASI_EINVAL + } else { + unsafe { + enc_timestamp_byref(vmctx, resolution_ptr, resolution) + .map(|_| wasm32::__WASI_ESUCCESS) + .unwrap_or_else(|e| e) + } + } + }) + .unwrap_or(wasm32::__WASI_EOVERFLOW) } - // convert to nanoseconds, returning EOVERFLOW in case of overflow; this is freelancing a bit - // from the spec but seems like it'll be an unusual situation to hit - (timespec.tv_sec as host::__wasi_timestamp_t) - .checked_mul(1_000_000_000) - .and_then(|sec_ns| sec_ns.checked_add(timespec.tv_nsec as host::__wasi_timestamp_t)) - .map(|resolution| { - // a supported clock can never return zero; this case will probably never get hit, but - // make sure we follow the spec - if resolution == 0 { - wasm32::__WASI_EINVAL - } else { - unsafe { - enc_timestamp_byref(&mut vmctx, resolution_ptr, resolution) - .map(|_| wasm32::__WASI_ESUCCESS) - .unwrap_or_else(|e| e) - } - } - }) - .unwrap_or(wasm32::__WASI_EOVERFLOW) -} + #[no_mangle] + pub unsafe extern "C" fn __wasi_clock_time_get( + &mut vmctx, + clock_id: wasm32::__wasi_clockid_t, + // ignored for now, but will be useful once we put optional limits on precision to reduce side + // channels + _precision: wasm32::__wasi_timestamp_t, + time_ptr: wasm32::uintptr_t, + ) -> wasm32::__wasi_errno_t { + // convert the supported clocks to the libc types, or return EINVAL + let clock_id = match dec_clockid(clock_id) { + host::__WASI_CLOCK_REALTIME => libc::CLOCK_REALTIME, + host::__WASI_CLOCK_MONOTONIC => libc::CLOCK_MONOTONIC, + host::__WASI_CLOCK_PROCESS_CPUTIME_ID => libc::CLOCK_PROCESS_CPUTIME_ID, + host::__WASI_CLOCK_THREAD_CPUTIME_ID => libc::CLOCK_THREAD_CPUTIME_ID, + _ => return wasm32::__WASI_EINVAL, + }; -#[no_mangle] -pub extern "C" fn __wasi_clock_time_get( - vmctx: *mut lucet_vmctx, - clock_id: wasm32::__wasi_clockid_t, - // ignored for now, but will be useful once we put optional limits on precision to reduce side - // channels - _precision: wasm32::__wasi_timestamp_t, - time_ptr: wasm32::uintptr_t, -) -> wasm32::__wasi_errno_t { - let mut vmctx = unsafe { Vmctx::from_raw(vmctx) }; - - // convert the supported clocks to the libc types, or return EINVAL - let clock_id = match dec_clockid(clock_id) { - host::__WASI_CLOCK_REALTIME => libc::CLOCK_REALTIME, - host::__WASI_CLOCK_MONOTONIC => libc::CLOCK_MONOTONIC, - host::__WASI_CLOCK_PROCESS_CPUTIME_ID => libc::CLOCK_PROCESS_CPUTIME_ID, - host::__WASI_CLOCK_THREAD_CPUTIME_ID => libc::CLOCK_THREAD_CPUTIME_ID, - _ => return wasm32::__WASI_EINVAL, - }; - - // no `nix` wrapper for clock_getres, so we do it ourselves - let mut timespec = unsafe { std::mem::uninitialized::() }; - let res = unsafe { libc::clock_gettime(clock_id, &mut timespec as *mut libc::timespec) }; - if res != 0 { - return wasm32::errno_from_nix(nix::errno::Errno::last()); + // no `nix` wrapper for clock_getres, so we do it ourselves + let mut timespec = unsafe { std::mem::uninitialized::() }; + let res = unsafe { libc::clock_gettime(clock_id, &mut timespec as *mut libc::timespec) }; + if res != 0 { + return wasm32::errno_from_nix(nix::errno::Errno::last()); + } + + // convert to nanoseconds, returning EOVERFLOW in case of overflow; this is freelancing a bit + // from the spec but seems like it'll be an unusual situation to hit + (timespec.tv_sec as host::__wasi_timestamp_t) + .checked_mul(1_000_000_000) + .and_then(|sec_ns| sec_ns.checked_add(timespec.tv_nsec as host::__wasi_timestamp_t)) + .map(|time| unsafe { + enc_timestamp_byref(vmctx, time_ptr, time) + .map(|_| wasm32::__WASI_ESUCCESS) + .unwrap_or_else(|e| e) + }) + .unwrap_or(wasm32::__WASI_EOVERFLOW) } - // convert to nanoseconds, returning EOVERFLOW in case of overflow; this is freelancing a bit - // from the spec but seems like it'll be an unusual situation to hit - (timespec.tv_sec as host::__wasi_timestamp_t) - .checked_mul(1_000_000_000) - .and_then(|sec_ns| sec_ns.checked_add(timespec.tv_nsec as host::__wasi_timestamp_t)) - .map(|time| unsafe { - enc_timestamp_byref(&mut vmctx, time_ptr, time) - .map(|_| wasm32::__WASI_ESUCCESS) - .unwrap_or_else(|e| e) - }) - .unwrap_or(wasm32::__WASI_EOVERFLOW) -} + #[no_mangle] + pub unsafe extern "C" fn __wasi_environ_get( + &mut vmctx, + environ_ptr: wasm32::uintptr_t, + environ_buf: wasm32::uintptr_t, + ) -> wasm32::__wasi_errno_t { + let ctx = vmctx.get_embed_ctx::(); -#[no_mangle] -pub extern "C" fn __wasi_environ_get( - vmctx_raw: *mut lucet_vmctx, - environ_ptr: wasm32::uintptr_t, - environ_buf: wasm32::uintptr_t, -) -> wasm32::__wasi_errno_t { - let mut vmctx = unsafe { Vmctx::from_raw(vmctx_raw) }; - let ctx: &WasiCtx = vmctx.get_embed_ctx(); + let mut environ_buf_offset = 0; + let mut environ = vec![]; + + for pair in ctx.env.iter() { + let env_bytes = pair.as_bytes_with_nul(); + let env_ptr = environ_buf + environ_buf_offset; - let mut environ_buf_offset = 0; - let mut environ = vec![]; + if let Err(e) = unsafe { enc_slice_of(vmctx, env_bytes, env_ptr) } { + return enc_errno(e); + } - for pair in ctx.env.iter() { - let env_bytes = pair.as_bytes_with_nul(); - let env_ptr = environ_buf + environ_buf_offset; + environ.push(env_ptr); - // nasty aliasing here, but we aren't interfering with the borrow for `ctx` - // TODO: rework vmctx interface to avoid this - let mut vmctx = unsafe { Vmctx::from_raw(vmctx_raw) }; - if let Err(e) = unsafe { enc_slice_of(&mut vmctx, env_bytes, env_ptr) } { - return enc_errno(e); + environ_buf_offset = if let Some(new_offset) = environ_buf_offset.checked_add( + wasm32::uintptr_t::cast(env_bytes.len()) + .expect("cast overflow would have been caught by `enc_slice_of` above"), + ) { + new_offset + } else { + return wasm32::__WASI_EOVERFLOW; + } } - environ.push(env_ptr); + unsafe { + enc_slice_of(vmctx, environ.as_slice(), environ_ptr) + .map(|_| wasm32::__WASI_ESUCCESS) + .unwrap_or_else(|e| e) + } + } - environ_buf_offset = if let Some(new_offset) = environ_buf_offset.checked_add( - wasm32::uintptr_t::cast(env_bytes.len()) - .expect("cast overflow would have been caught by `enc_slice_of` above"), - ) { - new_offset + #[no_mangle] + pub unsafe extern "C" fn __wasi_environ_sizes_get( + &mut vmctx, + environ_count_ptr: wasm32::uintptr_t, + environ_size_ptr: wasm32::uintptr_t, + ) -> wasm32::__wasi_errno_t { + let ctx = vmctx.get_embed_ctx::(); + + let environ_count = ctx.env.len(); + if let Some(environ_size) = ctx.env.iter().try_fold(0, |acc: u32, pair| { + acc.checked_add(pair.as_bytes_with_nul().len() as u32) + }) { + unsafe { + if let Err(e) = enc_usize_byref(vmctx, environ_count_ptr, environ_count) { + return enc_errno(e); + } + if let Err(e) = enc_usize_byref(vmctx, environ_size_ptr, environ_size as usize) { + return enc_errno(e); + } + } + wasm32::__WASI_ESUCCESS } else { - return wasm32::__WASI_EOVERFLOW; + wasm32::__WASI_EOVERFLOW } } - unsafe { - enc_slice_of(&mut vmctx, environ.as_slice(), environ_ptr) - .map(|_| wasm32::__WASI_ESUCCESS) - .unwrap_or_else(|e| e) + #[no_mangle] + pub unsafe extern "C" fn __wasi_fd_close( + &mut vmctx, + fd: wasm32::__wasi_fd_t, + ) -> wasm32::__wasi_errno_t { + let mut ctx = vmctx.get_embed_ctx_mut::(); + let fd = dec_fd(fd); + if let Some(fdent) = ctx.fds.get(&fd) { + // can't close preopened files + if fdent.preopen_path.is_some() { + return wasm32::__WASI_ENOTSUP; + } + } + if let Some(mut fdent) = ctx.fds.remove(&fd) { + fdent.fd_object.needs_close = false; + match nix::unistd::close(fdent.fd_object.rawfd) { + Ok(_) => wasm32::__WASI_ESUCCESS, + Err(e) => wasm32::errno_from_nix(e.as_errno().unwrap()), + } + } else { + wasm32::__WASI_EBADF + } } -} -#[no_mangle] -pub extern "C" fn __wasi_environ_sizes_get( - vmctx: *mut lucet_vmctx, - environ_count_ptr: wasm32::uintptr_t, - environ_size_ptr: wasm32::uintptr_t, -) -> wasm32::__wasi_errno_t { - let mut vmctx = unsafe { Vmctx::from_raw(vmctx) }; + #[no_mangle] + pub unsafe extern "C" fn __wasi_fd_fdstat_get( + &mut vmctx, + fd: wasm32::__wasi_fd_t, + fdstat_ptr: wasm32::uintptr_t, // *mut wasm32::__wasi_fdstat_t + ) -> wasm32::__wasi_errno_t { + let host_fd = dec_fd(fd); + let mut host_fdstat = match unsafe { dec_fdstat_byref(vmctx, fdstat_ptr) } { + Ok(host_fdstat) => host_fdstat, + Err(e) => return enc_errno(e), + }; - let ctx: &WasiCtx = vmctx.get_embed_ctx(); + let ctx = vmctx.get_embed_ctx_mut::(); + let errno = if let Some(fe) = ctx.fds.get(&host_fd) { + host_fdstat.fs_filetype = fe.fd_object.ty; + host_fdstat.fs_rights_base = fe.rights_base; + host_fdstat.fs_rights_inheriting = fe.rights_inheriting; + use nix::fcntl::{fcntl, OFlag, F_GETFL}; + match fcntl(fe.fd_object.rawfd, F_GETFL).map(OFlag::from_bits_truncate) { + Ok(flags) => { + host_fdstat.fs_flags = host::fdflags_from_nix(flags); + wasm32::__WASI_ESUCCESS + } + Err(e) => wasm32::errno_from_nix(e.as_errno().unwrap()), + } + } else { + wasm32::__WASI_EBADF + }; - let environ_count = ctx.env.len(); - if let Some(environ_size) = ctx.env.iter().try_fold(0, |acc: u32, pair| { - acc.checked_add(pair.as_bytes_with_nul().len() as u32) - }) { unsafe { - if let Err(e) = enc_usize_byref(&mut vmctx, environ_count_ptr, environ_count) { - return enc_errno(e); - } - if let Err(e) = enc_usize_byref(&mut vmctx, environ_size_ptr, environ_size as usize) { - return enc_errno(e); - } + enc_fdstat_byref(vmctx, fdstat_ptr, host_fdstat) + .expect("can write back into the pointer we read from"); } - wasm32::__WASI_ESUCCESS - } else { - wasm32::__WASI_EOVERFLOW + + errno } -} -#[no_mangle] -pub extern "C" fn __wasi_fd_close( - vmctx: *mut lucet_vmctx, - fd: wasm32::__wasi_fd_t, -) -> wasm32::__wasi_errno_t { - let mut vmctx = unsafe { Vmctx::from_raw(vmctx) }; - let ctx: &mut WasiCtx = vmctx.get_embed_ctx_mut(); - let fd = dec_fd(fd); - if let Some(fdent) = ctx.fds.get(&fd) { - // can't close preopened files - if fdent.preopen_path.is_some() { - return wasm32::__WASI_ENOTSUP; + #[no_mangle] + pub unsafe extern "C" fn __wasi_fd_fdstat_set_flags( + &mut vmctx, + fd: wasm32::__wasi_fd_t, + fdflags: wasm32::__wasi_fdflags_t, + ) -> wasm32::__wasi_errno_t { + let host_fd = dec_fd(fd); + let host_fdflags = dec_fdflags(fdflags); + let nix_flags = host::nix_from_fdflags(host_fdflags); + + let ctx = vmctx.get_embed_ctx_mut::(); + + if let Some(fe) = ctx.fds.get(&host_fd) { + match nix::fcntl::fcntl(fe.fd_object.rawfd, nix::fcntl::F_SETFL(nix_flags)) { + Ok(_) => wasm32::__WASI_ESUCCESS, + Err(e) => wasm32::errno_from_nix(e.as_errno().unwrap()), + } + } else { + wasm32::__WASI_EBADF } } - if let Some(mut fdent) = ctx.fds.remove(&fd) { - fdent.fd_object.needs_close = false; - match nix::unistd::close(fdent.fd_object.rawfd) { - Ok(_) => wasm32::__WASI_ESUCCESS, - Err(e) => wasm32::errno_from_nix(e.as_errno().unwrap()), + + #[no_mangle] + pub unsafe extern "C" fn __wasi_fd_seek( + &mut vmctx, + fd: wasm32::__wasi_fd_t, + offset: wasm32::__wasi_filedelta_t, + whence: wasm32::__wasi_whence_t, + newoffset: wasm32::uintptr_t, + ) -> wasm32::__wasi_errno_t { + let ctx = vmctx.get_embed_ctx_mut::(); + let fd = dec_fd(fd); + let offset = dec_filedelta(offset); + let whence = dec_whence(whence); + + let host_newoffset = { + use nix::unistd::{lseek, Whence}; + let nwhence = match whence as u32 { + host::__WASI_WHENCE_CUR => Whence::SeekCur, + host::__WASI_WHENCE_END => Whence::SeekEnd, + host::__WASI_WHENCE_SET => Whence::SeekSet, + _ => return wasm32::__WASI_EINVAL, + }; + + let rights = if offset == 0 && whence as u32 == host::__WASI_WHENCE_CUR { + host::__WASI_RIGHT_FD_TELL + } else { + host::__WASI_RIGHT_FD_SEEK | host::__WASI_RIGHT_FD_TELL + }; + match ctx.get_fd_entry(fd, rights.into(), 0) { + Ok(fe) => match lseek(fe.fd_object.rawfd, offset, nwhence) { + Ok(newoffset) => newoffset, + Err(e) => return wasm32::errno_from_nix(e.as_errno().unwrap()), + }, + Err(e) => return enc_errno(e), + } + }; + + unsafe { + enc_filesize_byref(vmctx, newoffset, host_newoffset as u64) + .map(|_| wasm32::__WASI_ESUCCESS) + .unwrap_or_else(|e| e) } - } else { - wasm32::__WASI_EBADF } -} -#[no_mangle] -pub extern "C" fn __wasi_fd_fdstat_get( - vmctx: *mut lucet_vmctx, - fd: wasm32::__wasi_fd_t, - fdstat_ptr: wasm32::uintptr_t, // *mut wasm32::__wasi_fdstat_t -) -> wasm32::__wasi_errno_t { - let mut vmctx = unsafe { Vmctx::from_raw(vmctx) }; - - let host_fd = dec_fd(fd); - let mut host_fdstat = match unsafe { dec_fdstat_byref(&mut vmctx, fdstat_ptr) } { - Ok(host_fdstat) => host_fdstat, - Err(e) => return enc_errno(e), - }; - - let ctx: &mut WasiCtx = vmctx.get_embed_ctx_mut(); - let errno = if let Some(fe) = ctx.fds.get(&host_fd) { - host_fdstat.fs_filetype = fe.fd_object.ty; - host_fdstat.fs_rights_base = fe.rights_base; - host_fdstat.fs_rights_inheriting = fe.rights_inheriting; - use nix::fcntl::{fcntl, OFlag, F_GETFL}; - match fcntl(fe.fd_object.rawfd, F_GETFL).map(OFlag::from_bits_truncate) { - Ok(flags) => { - host_fdstat.fs_flags = host::fdflags_from_nix(flags); - wasm32::__WASI_ESUCCESS + #[no_mangle] + pub unsafe extern "C" fn __wasi_fd_prestat_get( + &mut vmctx, + fd: wasm32::__wasi_fd_t, + prestat_ptr: wasm32::uintptr_t, + ) -> wasm32::__wasi_errno_t { + let ctx = vmctx.get_embed_ctx::(); + let fd = dec_fd(fd); + // TODO: is this the correct right for this? + match ctx.get_fd_entry(fd, host::__WASI_RIGHT_PATH_OPEN.into(), 0) { + Ok(fe) => { + if let Some(po_path) = &fe.preopen_path { + if fe.fd_object.ty != host::__WASI_FILETYPE_DIRECTORY as host::__wasi_filetype_t { + return wasm32::__WASI_ENOTDIR; + } + unsafe { + enc_prestat_byref( + vmctx, + prestat_ptr, + host::__wasi_prestat_t { + pr_type: host::__WASI_PREOPENTYPE_DIR as host::__wasi_preopentype_t, + u: host::__wasi_prestat_t___wasi_prestat_u { + dir: + host::__wasi_prestat_t___wasi_prestat_u___wasi_prestat_u_dir_t { + pr_name_len: po_path.as_os_str().as_bytes().len(), + }, + }, + }, + ) + .map(|_| wasm32::__WASI_ESUCCESS) + .unwrap_or_else(|e| e) + } + } else { + wasm32::__WASI_ENOTSUP + } } - Err(e) => wasm32::errno_from_nix(e.as_errno().unwrap()), + Err(e) => enc_errno(e), } - } else { - wasm32::__WASI_EBADF - }; + } - unsafe { - enc_fdstat_byref(&mut vmctx, fdstat_ptr, host_fdstat) - .expect("can write back into the pointer we read from"); + #[no_mangle] + pub unsafe extern "C" fn __wasi_fd_prestat_dir_name( + &mut vmctx, + fd: wasm32::__wasi_fd_t, + path_ptr: wasm32::uintptr_t, + path_len: wasm32::size_t, + ) -> wasm32::__wasi_errno_t { + let ctx = vmctx.get_embed_ctx::(); + let fd = dec_fd(fd); + match ctx.get_fd_entry(fd, host::__WASI_RIGHT_PATH_OPEN.into(), 0) { + Ok(fe) => { + if let Some(po_path) = &fe.preopen_path { + if fe.fd_object.ty != host::__WASI_FILETYPE_DIRECTORY as host::__wasi_filetype_t { + return wasm32::__WASI_ENOTDIR; + } + let path_bytes = po_path.as_os_str().as_bytes(); + if path_bytes.len() > dec_usize(path_len) { + return wasm32::__WASI_ENAMETOOLONG; + } + unsafe { + enc_slice_of(vmctx, path_bytes, path_ptr) + .map(|_| wasm32::__WASI_ESUCCESS) + .unwrap_or_else(|e| e) + } + } else { + wasm32::__WASI_ENOTSUP + } + } + Err(e) => enc_errno(e), + } } - errno -} + #[no_mangle] + pub unsafe extern "C" fn __wasi_fd_read( + &mut vmctx, + fd: wasm32::__wasi_fd_t, + iovs_ptr: wasm32::uintptr_t, + iovs_len: wasm32::size_t, + nread: wasm32::uintptr_t, + ) -> wasm32::__wasi_errno_t { + use nix::sys::uio::{readv, IoVec}; + + let fd = dec_fd(fd); + let mut iovs = match unsafe { dec_ciovec_slice(vmctx, iovs_ptr, iovs_len) } { + Ok(iovs) => iovs, + Err(e) => return enc_errno(e), + }; -#[no_mangle] -pub extern "C" fn __wasi_fd_fdstat_set_flags( - vmctx: *mut lucet_vmctx, - fd: wasm32::__wasi_fd_t, - fdflags: wasm32::__wasi_fdflags_t, -) -> wasm32::__wasi_errno_t { - let mut vmctx = unsafe { Vmctx::from_raw(vmctx) }; + let mut ctx = vmctx.get_embed_ctx_mut::(); + let fe = match ctx.get_fd_entry(fd, host::__WASI_RIGHT_FD_READ.into(), 0) { + Ok(fe) => fe, + Err(e) => return enc_errno(e), + }; - let host_fd = dec_fd(fd); - let host_fdflags = dec_fdflags(fdflags); - let nix_flags = host::nix_from_fdflags(host_fdflags); + let mut iovs: Vec> = iovs + .iter_mut() + .map(|iov| unsafe { host::ciovec_to_nix_mut(iov) }) + .collect(); - let ctx: &mut WasiCtx = vmctx.get_embed_ctx_mut(); + let host_nread = match readv(fe.fd_object.rawfd, &mut iovs) { + Ok(len) => len, + Err(e) => return wasm32::errno_from_nix(e.as_errno().unwrap()), + }; - if let Some(fe) = ctx.fds.get(&host_fd) { - match nix::fcntl::fcntl(fe.fd_object.rawfd, nix::fcntl::F_SETFL(nix_flags)) { - Ok(_) => wasm32::__WASI_ESUCCESS, - Err(e) => wasm32::errno_from_nix(e.as_errno().unwrap()), + if host_nread == 0 { + // we hit eof, so remove the fdentry from the context + let mut fe = ctx.fds.remove(&fd).expect("file entry is still there"); + fe.fd_object.needs_close = false; + } + + unsafe { + enc_usize_byref(vmctx, nread, host_nread) + .map(|_| wasm32::__WASI_ESUCCESS) + .unwrap_or_else(|e| e) } - } else { - wasm32::__WASI_EBADF } -} -#[no_mangle] -pub extern "C" fn __wasi_fd_seek( - vmctx: *mut lucet_vmctx, - fd: wasm32::__wasi_fd_t, - offset: wasm32::__wasi_filedelta_t, - whence: wasm32::__wasi_whence_t, - newoffset: wasm32::uintptr_t, -) -> wasm32::__wasi_errno_t { - let mut vmctx = unsafe { Vmctx::from_raw(vmctx) }; - let ctx: &mut WasiCtx = vmctx.get_embed_ctx_mut(); - let fd = dec_fd(fd); - let offset = dec_filedelta(offset); - let whence = dec_whence(whence); - - let host_newoffset = { - use nix::unistd::{lseek, Whence}; - let nwhence = match whence as u32 { - host::__WASI_WHENCE_CUR => Whence::SeekCur, - host::__WASI_WHENCE_END => Whence::SeekEnd, - host::__WASI_WHENCE_SET => Whence::SeekSet, - _ => return wasm32::__WASI_EINVAL, + #[no_mangle] + pub unsafe extern "C" fn __wasi_fd_write( + &mut vmctx, + fd: wasm32::__wasi_fd_t, + iovs_ptr: wasm32::uintptr_t, + iovs_len: wasm32::size_t, + nwritten: wasm32::uintptr_t, + ) -> wasm32::__wasi_errno_t { + use nix::sys::uio::{writev, IoVec}; + + let fd = dec_fd(fd); + let iovs = match unsafe { dec_ciovec_slice(vmctx, iovs_ptr, iovs_len) } { + Ok(iovs) => iovs, + Err(e) => return enc_errno(e), + }; + + let ctx = vmctx.get_embed_ctx::(); + let fe = match ctx.get_fd_entry(fd, host::__WASI_RIGHT_FD_WRITE.into(), 0) { + Ok(fe) => fe, + Err(e) => return enc_errno(e), }; - let rights = if offset == 0 && whence as u32 == host::__WASI_WHENCE_CUR { - host::__WASI_RIGHT_FD_TELL + let iovs: Vec> = iovs + .iter() + .map(|iov| unsafe { host::ciovec_to_nix(iov) }) + .collect(); + + let host_nwritten = match writev(fe.fd_object.rawfd, &iovs) { + Ok(len) => len, + Err(e) => return wasm32::errno_from_nix(e.as_errno().unwrap()), + }; + + unsafe { + enc_usize_byref(vmctx, nwritten, host_nwritten) + .map(|_| wasm32::__WASI_ESUCCESS) + .unwrap_or_else(|e| e) + } + } + + #[no_mangle] + pub unsafe extern "C" fn __wasi_path_open( + &mut vmctx, + dirfd: wasm32::__wasi_fd_t, + dirflags: wasm32::__wasi_lookupflags_t, + path_ptr: wasm32::uintptr_t, + path_len: wasm32::size_t, + oflags: wasm32::__wasi_oflags_t, + fs_rights_base: wasm32::__wasi_rights_t, + fs_rights_inheriting: wasm32::__wasi_rights_t, + fs_flags: wasm32::__wasi_fdflags_t, + fd_out_ptr: wasm32::uintptr_t, + ) -> wasm32::__wasi_errno_t { + use nix::errno::Errno; + use nix::fcntl::{openat, AtFlags, OFlag}; + use nix::sys::stat::{fstatat, Mode, SFlag}; + + let dirfd = dec_fd(dirfd); + let dirflags = dec_lookupflags(dirflags); + let oflags = dec_oflags(oflags); + let fs_rights_base = dec_rights(fs_rights_base); + let fs_rights_inheriting = dec_rights(fs_rights_inheriting); + let fs_flags = dec_fdflags(fs_flags); + + // which open mode do we need? + let read = fs_rights_base + & ((host::__WASI_RIGHT_FD_READ | host::__WASI_RIGHT_FD_READDIR) as host::__wasi_rights_t) + != 0; + let write = fs_rights_base + & ((host::__WASI_RIGHT_FD_DATASYNC + | host::__WASI_RIGHT_FD_WRITE + | host::__WASI_RIGHT_FD_ALLOCATE + | host::__WASI_RIGHT_PATH_FILESTAT_SET_SIZE) as host::__wasi_rights_t) + != 0; + + let mut nix_all_oflags = if read && write { + OFlag::O_RDWR + } else if read { + OFlag::O_RDONLY } else { - host::__WASI_RIGHT_FD_SEEK | host::__WASI_RIGHT_FD_TELL + OFlag::O_WRONLY }; - match ctx.get_fd_entry(fd, rights.into(), 0) { - Ok(fe) => match lseek(fe.fd_object.rawfd, offset, nwhence) { - Ok(newoffset) => newoffset, - Err(e) => return wasm32::errno_from_nix(e.as_errno().unwrap()), - }, - Err(e) => return enc_errno(e), + + // on non-Capsicum systems, we always want nofollow + nix_all_oflags.insert(OFlag::O_NOFOLLOW); + + // which rights are needed on the dirfd? + let mut needed_base = host::__WASI_RIGHT_PATH_OPEN as host::__wasi_rights_t; + let mut needed_inheriting = fs_rights_base | fs_rights_inheriting; + + // convert open flags + let nix_oflags = host::nix_from_oflags(oflags); + nix_all_oflags.insert(nix_oflags); + if nix_all_oflags.contains(OFlag::O_CREAT) { + needed_base |= host::__WASI_RIGHT_PATH_CREATE_FILE as host::__wasi_rights_t; + } + if nix_all_oflags.contains(OFlag::O_TRUNC) { + needed_inheriting |= host::__WASI_RIGHT_PATH_FILESTAT_SET_SIZE as host::__wasi_rights_t; } - }; - unsafe { - enc_filesize_byref(&mut vmctx, newoffset, host_newoffset as u64) - .map(|_| wasm32::__WASI_ESUCCESS) - .unwrap_or_else(|e| e) - } -} + // convert file descriptor flags + nix_all_oflags.insert(host::nix_from_fdflags(fs_flags)); + if nix_all_oflags.contains(OFlag::O_DSYNC) { + needed_inheriting |= host::__WASI_RIGHT_FD_DATASYNC as host::__wasi_rights_t; + } + if nix_all_oflags.intersects(O_RSYNC | OFlag::O_SYNC) { + needed_inheriting |= host::__WASI_RIGHT_FD_SYNC as host::__wasi_rights_t; + } -#[no_mangle] -pub extern "C" fn __wasi_fd_prestat_get( - vmctx_raw: *mut lucet_vmctx, - fd: wasm32::__wasi_fd_t, - prestat_ptr: wasm32::uintptr_t, -) -> wasm32::__wasi_errno_t { - let vmctx = unsafe { Vmctx::from_raw(vmctx_raw) }; - let ctx: &WasiCtx = vmctx.get_embed_ctx(); - let fd = dec_fd(fd); - // TODO: is this the correct right for this? - match ctx.get_fd_entry(fd, host::__WASI_RIGHT_PATH_OPEN.into(), 0) { - Ok(fe) => { - if let Some(po_path) = &fe.preopen_path { - if fe.fd_object.ty != host::__WASI_FILETYPE_DIRECTORY as host::__wasi_filetype_t { - return wasm32::__WASI_ENOTDIR; - } - // nasty aliasing here, but we aren't interfering with the borrow for `ctx` - // TODO: rework vmctx interface to avoid this - unsafe { - enc_prestat_byref( - &mut Vmctx::from_raw(vmctx_raw), - prestat_ptr, - host::__wasi_prestat_t { - pr_type: host::__WASI_PREOPENTYPE_DIR as host::__wasi_preopentype_t, - u: host::__wasi_prestat_t___wasi_prestat_u { - dir: - host::__wasi_prestat_t___wasi_prestat_u___wasi_prestat_u_dir_t { - pr_name_len: po_path.as_os_str().as_bytes().len(), - }, - }, - }, - ) - .map(|_| wasm32::__WASI_ESUCCESS) - .unwrap_or_else(|e| e) + let path = match unsafe { dec_slice_of::(vmctx, path_ptr, path_len) } { + Ok((ptr, len)) => OsStr::from_bytes(unsafe { std::slice::from_raw_parts(ptr, len) }), + Err(e) => return enc_errno(e), + }; + + let (dir, path) = match path_get( + &vmctx, + dirfd, + dirflags, + path, + needed_base, + needed_inheriting, + nix_oflags.contains(OFlag::O_CREAT), + ) { + Ok((dir, path)) => (dir, path), + Err(e) => return enc_errno(e), + }; + + let new_fd = match openat( + dir, + path.as_os_str(), + nix_all_oflags, + Mode::from_bits_truncate(0o777), + ) { + Ok(fd) => fd, + Err(e) => { + match e.as_errno() { + // Linux returns ENXIO instead of EOPNOTSUPP when opening a socket + Some(Errno::ENXIO) => { + if let Ok(stat) = fstatat(dir, path.as_os_str(), AtFlags::AT_SYMLINK_NOFOLLOW) { + if SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFSOCK) { + return wasm32::__WASI_ENOTSUP; + } else { + return wasm32::__WASI_ENXIO; + } + } else { + return wasm32::__WASI_ENXIO; + } + } + Some(e) => return wasm32::errno_from_nix(e), + None => return wasm32::__WASI_ENOSYS, } - } else { - wasm32::__WASI_ENOTSUP } - } - Err(e) => enc_errno(e), - } -} + }; -#[no_mangle] -pub extern "C" fn __wasi_fd_prestat_dir_name( - vmctx_raw: *mut lucet_vmctx, - fd: wasm32::__wasi_fd_t, - path_ptr: wasm32::uintptr_t, - path_len: wasm32::size_t, -) -> wasm32::__wasi_errno_t { - let vmctx = unsafe { Vmctx::from_raw(vmctx_raw) }; - let ctx: &WasiCtx = vmctx.get_embed_ctx(); - let fd = dec_fd(fd); - match ctx.get_fd_entry(fd, host::__WASI_RIGHT_PATH_OPEN.into(), 0) { - Ok(fe) => { - if let Some(po_path) = &fe.preopen_path { - if fe.fd_object.ty != host::__WASI_FILETYPE_DIRECTORY as host::__wasi_filetype_t { - return wasm32::__WASI_ENOTDIR; - } - let path_bytes = po_path.as_os_str().as_bytes(); - if path_bytes.len() > dec_usize(path_len) { - return wasm32::__WASI_ENAMETOOLONG; - } - // nasty aliasing here, but we aren't interfering with the borrow for `ctx` - // TODO: rework vmctx interface to avoid this - unsafe { - enc_slice_of(&mut Vmctx::from_raw(vmctx_raw), path_bytes, path_ptr) - .map(|_| wasm32::__WASI_ESUCCESS) - .unwrap_or_else(|e| e) + // Determine the type of the new file descriptor and which rights contradict with this type + let guest_fd = match unsafe { determine_type_rights(new_fd) } { + Err(e) => { + // if `close` fails, note it but do not override the underlying errno + nix::unistd::close(new_fd).unwrap_or_else(|e| { + dbg!(e); + }); + return enc_errno(e); + } + Ok((_ty, max_base, max_inheriting)) => { + let mut fe = unsafe { FdEntry::from_raw_fd(new_fd) }; + fe.rights_base &= max_base; + fe.rights_inheriting &= max_inheriting; + match vmctx.get_embed_ctx_mut::().insert_fd_entry(fe) { + Ok(fd) => fd, + Err(e) => return enc_errno(e), } - } else { - wasm32::__WASI_ENOTSUP } + }; + + unsafe { + enc_fd_byref(vmctx, fd_out_ptr, guest_fd) + .map(|_| wasm32::__WASI_ESUCCESS) + .unwrap_or_else(|e| e) } - Err(e) => enc_errno(e), } -} -#[no_mangle] -pub extern "C" fn __wasi_fd_read( - vmctx: *mut lucet_vmctx, - fd: wasm32::__wasi_fd_t, - iovs_ptr: wasm32::uintptr_t, - iovs_len: wasm32::size_t, - nread: wasm32::uintptr_t, -) -> wasm32::__wasi_errno_t { - use nix::sys::uio::{readv, IoVec}; - - let mut vmctx = unsafe { Vmctx::from_raw(vmctx) }; - let fd = dec_fd(fd); - let mut iovs = match unsafe { dec_ciovec_slice(&mut vmctx, iovs_ptr, iovs_len) } { - Ok(iovs) => iovs, - Err(e) => return enc_errno(e), - }; - - let ctx: &mut WasiCtx = vmctx.get_embed_ctx_mut(); - let fe = match ctx.get_fd_entry(fd, host::__WASI_RIGHT_FD_READ.into(), 0) { - Ok(fe) => fe, - Err(e) => return enc_errno(e), - }; - - let mut iovs: Vec> = iovs - .iter_mut() - .map(|iov| unsafe { host::ciovec_to_nix_mut(iov) }) - .collect(); - - let host_nread = match readv(fe.fd_object.rawfd, &mut iovs) { - Ok(len) => len, - Err(e) => return wasm32::errno_from_nix(e.as_errno().unwrap()), - }; - - if host_nread == 0 { - // we hit eof, so remove the fdentry from the context - let mut fe = ctx.fds.remove(&fd).expect("file entry is still there"); - fe.fd_object.needs_close = false; - } + #[no_mangle] + pub unsafe extern "C" fn __wasi_random_get( + &mut vmctx, + buf_ptr: wasm32::uintptr_t, + buf_len: wasm32::size_t, + ) -> wasm32::__wasi_errno_t { + use rand::{thread_rng, RngCore}; + + let buf_len = dec_usize(buf_len); + let buf_ptr = match unsafe { dec_ptr(vmctx, buf_ptr, buf_len) } { + Ok(ptr) => ptr, + Err(e) => return enc_errno(e), + }; - unsafe { - enc_usize_byref(&mut vmctx, nread, host_nread) - .map(|_| wasm32::__WASI_ESUCCESS) - .unwrap_or_else(|e| e) - } -} + let buf = unsafe { std::slice::from_raw_parts_mut(buf_ptr, buf_len) }; -#[no_mangle] -pub extern "C" fn __wasi_fd_write( - vmctx: *mut lucet_vmctx, - fd: wasm32::__wasi_fd_t, - iovs_ptr: wasm32::uintptr_t, - iovs_len: wasm32::size_t, - nwritten: wasm32::uintptr_t, -) -> wasm32::__wasi_errno_t { - use nix::sys::uio::{writev, IoVec}; - - let mut vmctx = unsafe { Vmctx::from_raw(vmctx) }; - let fd = dec_fd(fd); - let iovs = match unsafe { dec_ciovec_slice(&mut vmctx, iovs_ptr, iovs_len) } { - Ok(iovs) => iovs, - Err(e) => return enc_errno(e), - }; - - let ctx: &mut WasiCtx = vmctx.get_embed_ctx_mut(); - let fe = match ctx.get_fd_entry(fd, host::__WASI_RIGHT_FD_WRITE.into(), 0) { - Ok(fe) => fe, - Err(e) => return enc_errno(e), - }; - - let iovs: Vec> = iovs - .iter() - .map(|iov| unsafe { host::ciovec_to_nix(iov) }) - .collect(); - - let host_nwritten = match writev(fe.fd_object.rawfd, &iovs) { - Ok(len) => len, - Err(e) => return wasm32::errno_from_nix(e.as_errno().unwrap()), - }; + thread_rng().fill_bytes(buf); - unsafe { - enc_usize_byref(&mut vmctx, nwritten, host_nwritten) - .map(|_| wasm32::__WASI_ESUCCESS) - .unwrap_or_else(|e| e) + return wasm32::__WASI_ESUCCESS; } -} -#[no_mangle] -pub extern "C" fn __wasi_fd_filestat_get( - vmctx: *mut lucet_vmctx, - fd: wasm32::__wasi_fd_t, - filestat_ptr: wasm32::uintptr_t, -) -> wasm32::__wasi_errno_t { - use nix::sys::stat::fstat; + #[no_mangle] + pub unsafe extern "C" fn __wasi_poll_oneoff( + &mut vmctx, + input: wasm32::uintptr_t, + output: wasm32::uintptr_t, + nsubscriptions: wasm32::size_t, + nevents: wasm32::uintptr_t, + ) -> wasm32::__wasi_errno_t { + if nsubscriptions as u64 > wasm32::__wasi_filesize_t::max_value() { + return wasm32::__WASI_EINVAL; + } + unsafe { enc_pointee(vmctx, nevents, 0) }.unwrap(); + let input_slice_ = + unsafe { dec_slice_of::(vmctx, input, nsubscriptions) } + .unwrap(); + let input_slice = unsafe { slice::from_raw_parts(input_slice_.0, input_slice_.1) }; + + let output_slice_ = + unsafe { dec_slice_of::(vmctx, output, nsubscriptions) } + .unwrap(); + let output_slice = unsafe { slice::from_raw_parts_mut(output_slice_.0, output_slice_.1) }; + + let input: Vec<_> = input_slice.iter().map(|x| dec_subscription(x)).collect(); + + let timeout = input + .iter() + .filter_map(|event| match event { + Ok(event) if event.type_ == wasm32::__WASI_EVENTTYPE_CLOCK => Some(ClockEventData { + delay: wasi_clock_to_relative_ns_delay(unsafe { event.u.clock }) / 1_000_000, + userdata: event.userdata, + }), + _ => None, + }) + .min_by_key(|event| event.delay); + let fd_events: Vec<_> = input + .iter() + .filter_map(|event| match event { + Ok(event) + if event.type_ == wasm32::__WASI_EVENTTYPE_FD_READ + || event.type_ == wasm32::__WASI_EVENTTYPE_FD_WRITE => + { + Some(FdEventData { + fd: unsafe { event.u.fd_readwrite.fd } as c_int, + type_: event.type_, + userdata: event.userdata, + }) + } + _ => None, + }) + .collect(); + if fd_events.is_empty() && timeout.is_none() { + return wasm32::__WASI_ESUCCESS; + } + let mut poll_fds: Vec<_> = fd_events + .iter() + .map(|event| { + let mut flags = nix::poll::EventFlags::empty(); + match event.type_ { + wasm32::__WASI_EVENTTYPE_FD_READ => flags.insert(nix::poll::EventFlags::POLLIN), + wasm32::__WASI_EVENTTYPE_FD_WRITE => flags.insert(nix::poll::EventFlags::POLLOUT), + // An event on a file descriptor can currently only be of type FD_READ or FD_WRITE + // Nothing else has been defined in the specification, and these are also the only two + // events we filtered before. If we get something else here, the code has a serious bug. + _ => unreachable!(), + }; + nix::poll::PollFd::new(event.fd, flags) + }) + .collect(); + let timeout = timeout.map(|ClockEventData { delay, userdata }| ClockEventData { + delay: cmp::min(delay, c_int::max_value() as u128), + userdata, + }); + let poll_timeout = timeout.map(|timeout| timeout.delay as c_int).unwrap_or(-1); + let ready = loop { + match nix::poll::poll(&mut poll_fds, poll_timeout) { + Err(_) => { + if nix::errno::Errno::last() == nix::errno::Errno::EINTR { + continue; + } + return wasm32::errno_from_nix(nix::errno::Errno::last()); + } + Ok(ready) => break ready as usize, + } + }; + if ready == 0 { + return __wasi_poll_oneoff_handle_timeout_event(vmctx, output_slice, nevents, timeout); + } + let events = fd_events.iter().zip(poll_fds.iter()).take(ready); + __wasi_poll_oneoff_handle_fd_event(vmctx, output_slice, nevents, events) + } - let mut vmctx = unsafe { Vmctx::from_raw(vmctx) }; - let host_fd = dec_fd(fd); - let ctx: &mut WasiCtx = vmctx.get_embed_ctx_mut(); + #[no_mangle] + pub unsafe extern "C" fn __wasi_fd_filestat_get( + &mut vmctx, + fd: wasm32::__wasi_fd_t, + filestat_ptr: wasm32::uintptr_t, + ) -> wasm32::__wasi_errno_t { + use nix::sys::stat::fstat; + + let host_fd = dec_fd(fd); + let ctx = vmctx.get_embed_ctx_mut::(); + + let errno = if let Some(fe) = ctx.fds.get(&host_fd) { + match fstat(fe.fd_object.rawfd) { + Err(e) => wasm32::errno_from_nix(e.as_errno().unwrap()), + Ok(filestat) => { + let host_filestat = host::filestat_from_nix(filestat); + unsafe { + enc_filestat_byref(vmctx, filestat_ptr, host_filestat) + .expect("can write into the pointer"); + } + wasm32::__WASI_ESUCCESS + } + } + } else { + wasm32::__WASI_EBADF + }; + errno + } - let errno = if let Some(fe) = ctx.fds.get(&host_fd) { - match fstat(fe.fd_object.rawfd) { + #[no_mangle] + pub unsafe extern "C" fn __wasi_path_filestat_get( + &mut vmctx, + dirfd: wasm32::__wasi_fd_t, + dirflags: wasm32::__wasi_lookupflags_t, + path_ptr: wasm32::uintptr_t, + path_len: wasm32::size_t, + filestat_ptr: wasm32::uintptr_t, + ) -> wasm32::__wasi_errno_t { + use nix::fcntl::AtFlags; + use nix::sys::stat::fstatat; + + let dirfd = dec_fd(dirfd); + let dirflags = dec_lookupflags(dirflags); + let path = match unsafe { dec_slice_of::(vmctx, path_ptr, path_len) } { + Ok((ptr, len)) => OsStr::from_bytes(unsafe { std::slice::from_raw_parts(ptr, len) }), + Err(e) => return enc_errno(e), + }; + let (dir, path) = match path_get( + &vmctx, + dirfd, + dirflags, + path, + host::__WASI_RIGHT_PATH_FILESTAT_GET as host::__wasi_rights_t, + 0, + false, + ) { + Ok((dir, path)) => (dir, path), + Err(e) => return enc_errno(e), + }; + let atflags = match dirflags { + 0 => AtFlags::empty(), + _ => AtFlags::AT_SYMLINK_NOFOLLOW, + }; + match fstatat(dir, path.as_os_str(), atflags) { Err(e) => wasm32::errno_from_nix(e.as_errno().unwrap()), Ok(filestat) => { let host_filestat = host::filestat_from_nix(filestat); unsafe { - enc_filestat_byref(&mut vmctx, filestat_ptr, host_filestat) + enc_filestat_byref(vmctx, filestat_ptr, host_filestat) .expect("can write into the pointer"); } wasm32::__WASI_ESUCCESS } } - } else { - wasm32::__WASI_EBADF - }; - errno -} - -#[no_mangle] -pub extern "C" fn __wasi_path_filestat_get( - vmctx: *mut lucet_vmctx, - dirfd: wasm32::__wasi_fd_t, - dirflags: wasm32::__wasi_lookupflags_t, - path_ptr: wasm32::uintptr_t, - path_len: wasm32::size_t, - filestat_ptr: wasm32::uintptr_t, -) -> wasm32::__wasi_errno_t { - use nix::fcntl::AtFlags; - use nix::sys::stat::fstatat; - let mut vmctx = unsafe { Vmctx::from_raw(vmctx) }; - let dirfd = dec_fd(dirfd); - let dirflags = dec_lookupflags(dirflags); - let path = match unsafe { dec_slice_of::(&mut vmctx, path_ptr, path_len) } { - Ok((ptr, len)) => OsStr::from_bytes(unsafe { std::slice::from_raw_parts(ptr, len) }), - Err(e) => return enc_errno(e), - }; - let (dir, path) = match path_get( - &vmctx, - dirfd, - dirflags, - path, - host::__WASI_RIGHT_PATH_FILESTAT_GET as host::__wasi_rights_t, - 0, - false, - ) { - Ok((dir, path)) => (dir, path), - Err(e) => return enc_errno(e), - }; - let atflags = match dirflags { - 0 => AtFlags::empty(), - _ => AtFlags::AT_SYMLINK_NOFOLLOW, - }; - match fstatat(dir, path.as_os_str(), atflags) { - Err(e) => wasm32::errno_from_nix(e.as_errno().unwrap()), - Ok(filestat) => { - let host_filestat = host::filestat_from_nix(filestat); - unsafe { - enc_filestat_byref(&mut vmctx, filestat_ptr, host_filestat) - .expect("can write into the pointer"); - } - wasm32::__WASI_ESUCCESS - } } -} -#[no_mangle] -pub extern "C" fn __wasi_path_create_directory( - vmctx: *mut lucet_vmctx, - dirfd: wasm32::__wasi_fd_t, - path_ptr: wasm32::uintptr_t, - path_len: wasm32::size_t, -) -> wasm32::__wasi_errno_t { - use nix::errno; - use nix::libc::mkdirat; - let mut vmctx = unsafe { Vmctx::from_raw(vmctx) }; - let dirfd = dec_fd(dirfd); - let path = match unsafe { dec_slice_of::(&mut vmctx, path_ptr, path_len) } { - Ok((ptr, len)) => OsStr::from_bytes(unsafe { std::slice::from_raw_parts(ptr, len) }), - Err(e) => return enc_errno(e), - }; - let (dir, path) = match path_get( - &vmctx, - dirfd, - 0, - path, - (host::__WASI_RIGHT_PATH_OPEN | host::__WASI_RIGHT_PATH_CREATE_DIRECTORY) - as host::__wasi_rights_t, - 0, - false, - ) { - Ok((dir, path)) => (dir, path), - Err(e) => return enc_errno(e), - }; - let path_cstr = match std::ffi::CString::new(path.as_os_str().as_bytes()) { - Ok(path_cstr) => path_cstr, - Err(_) => return wasm32::__WASI_EINVAL, - }; - // nix doesn't expose mkdirat() yet - match unsafe { mkdirat(dir, path_cstr.as_ptr(), 0o777) } { - 0 => wasm32::__WASI_ESUCCESS, - _ => wasm32::errno_from_nix(errno::Errno::last()), + #[no_mangle] + pub unsafe extern "C" fn __wasi_path_create_directory( + &mut vmctx, + dirfd: wasm32::__wasi_fd_t, + path_ptr: wasm32::uintptr_t, + path_len: wasm32::size_t, + ) -> wasm32::__wasi_errno_t { + use nix::errno; + use nix::libc::mkdirat; + + let dirfd = dec_fd(dirfd); + let path = match unsafe { dec_slice_of::(vmctx, path_ptr, path_len) } { + Ok((ptr, len)) => OsStr::from_bytes(unsafe { std::slice::from_raw_parts(ptr, len) }), + Err(e) => return enc_errno(e), + }; + let (dir, path) = match path_get( + &vmctx, + dirfd, + 0, + path, + (host::__WASI_RIGHT_PATH_OPEN | host::__WASI_RIGHT_PATH_CREATE_DIRECTORY) + as host::__wasi_rights_t, + 0, + false, + ) { + Ok((dir, path)) => (dir, path), + Err(e) => return enc_errno(e), + }; + let path_cstr = match std::ffi::CString::new(path.as_os_str().as_bytes()) { + Ok(path_cstr) => path_cstr, + Err(_) => return wasm32::__WASI_EINVAL, + }; + // nix doesn't expose mkdirat() yet + match unsafe { mkdirat(dir, path_cstr.as_ptr(), 0o777) } { + 0 => wasm32::__WASI_ESUCCESS, + _ => wasm32::errno_from_nix(errno::Errno::last()), + } } -} -#[no_mangle] -pub extern "C" fn __wasi_path_unlink_file( - vmctx: *mut lucet_vmctx, - dirfd: wasm32::__wasi_fd_t, - path_ptr: wasm32::uintptr_t, - path_len: wasm32::size_t, -) -> wasm32::__wasi_errno_t { - use nix::errno; - use nix::libc::unlinkat; - let mut vmctx = unsafe { Vmctx::from_raw(vmctx) }; - let dirfd = dec_fd(dirfd); - let path = match unsafe { dec_slice_of::(&mut vmctx, path_ptr, path_len) } { - Ok((ptr, len)) => OsStr::from_bytes(unsafe { std::slice::from_raw_parts(ptr, len) }), - Err(e) => return enc_errno(e), - }; - let (dir, path) = match path_get( - &vmctx, - dirfd, - 0, - path, - host::__WASI_RIGHT_PATH_UNLINK_FILE as host::__wasi_rights_t, - 0, - false, - ) { - Ok((dir, path)) => (dir, path), - Err(e) => return enc_errno(e), - }; - let path_cstr = match std::ffi::CString::new(path.as_os_str().as_bytes()) { - Ok(path_cstr) => path_cstr, - Err(_) => return wasm32::__WASI_EINVAL, - }; - // nix doesn't expose unlinkat() yet - match unsafe { unlinkat(dir, path_cstr.as_ptr(), 0) } { - 0 => wasm32::__WASI_ESUCCESS, - _ => wasm32::errno_from_nix(errno::Errno::last()), + #[no_mangle] + pub unsafe extern "C" fn __wasi_path_unlink_file( + &mut vmctx, + dirfd: wasm32::__wasi_fd_t, + path_ptr: wasm32::uintptr_t, + path_len: wasm32::size_t, + ) -> wasm32::__wasi_errno_t { + use nix::errno; + use nix::libc::unlinkat; + + let dirfd = dec_fd(dirfd); + let path = match unsafe { dec_slice_of::(vmctx, path_ptr, path_len) } { + Ok((ptr, len)) => OsStr::from_bytes(unsafe { std::slice::from_raw_parts(ptr, len) }), + Err(e) => return enc_errno(e), + }; + let (dir, path) = match path_get( + &vmctx, + dirfd, + 0, + path, + host::__WASI_RIGHT_PATH_UNLINK_FILE as host::__wasi_rights_t, + 0, + false, + ) { + Ok((dir, path)) => (dir, path), + Err(e) => return enc_errno(e), + }; + let path_cstr = match std::ffi::CString::new(path.as_os_str().as_bytes()) { + Ok(path_cstr) => path_cstr, + Err(_) => return wasm32::__WASI_EINVAL, + }; + // nix doesn't expose unlinkat() yet + match unsafe { unlinkat(dir, path_cstr.as_ptr(), 0) } { + 0 => wasm32::__WASI_ESUCCESS, + _ => wasm32::errno_from_nix(errno::Errno::last()), + } } } -#[no_mangle] -pub extern "C" fn __wasi_path_open( - vmctx: *mut lucet_vmctx, - dirfd: wasm32::__wasi_fd_t, - dirflags: wasm32::__wasi_lookupflags_t, - path_ptr: wasm32::uintptr_t, - path_len: wasm32::size_t, - oflags: wasm32::__wasi_oflags_t, - fs_rights_base: wasm32::__wasi_rights_t, - fs_rights_inheriting: wasm32::__wasi_rights_t, - fs_flags: wasm32::__wasi_fdflags_t, - fd_out_ptr: wasm32::uintptr_t, -) -> wasm32::__wasi_errno_t { - use nix::errno::Errno; - use nix::fcntl::{openat, AtFlags, OFlag}; - use nix::sys::stat::{fstatat, Mode, SFlag}; - - let dirfd = dec_fd(dirfd); - let dirflags = dec_lookupflags(dirflags); - let oflags = dec_oflags(oflags); - let fs_rights_base = dec_rights(fs_rights_base); - let fs_rights_inheriting = dec_rights(fs_rights_inheriting); - let fs_flags = dec_fdflags(fs_flags); - - // which open mode do we need? - let read = fs_rights_base - & ((host::__WASI_RIGHT_FD_READ | host::__WASI_RIGHT_FD_READDIR) as host::__wasi_rights_t) - != 0; - let write = fs_rights_base - & ((host::__WASI_RIGHT_FD_DATASYNC - | host::__WASI_RIGHT_FD_WRITE - | host::__WASI_RIGHT_FD_ALLOCATE - | host::__WASI_RIGHT_PATH_FILESTAT_SET_SIZE) as host::__wasi_rights_t) - != 0; - - let mut nix_all_oflags = if read && write { - OFlag::O_RDWR - } else if read { - OFlag::O_RDONLY - } else { - OFlag::O_WRONLY - }; - - // on non-Capsicum systems, we always want nofollow - nix_all_oflags.insert(OFlag::O_NOFOLLOW); - - // which rights are needed on the dirfd? - let mut needed_base = host::__WASI_RIGHT_PATH_OPEN as host::__wasi_rights_t; - let mut needed_inheriting = fs_rights_base | fs_rights_inheriting; +// define the `fionread()` function, equivalent to `ioctl(fd, FIONREAD, *bytes)` +nix::ioctl_read_bad!(fionread, nix::libc::FIONREAD, c_int); - // convert open flags - let nix_oflags = host::nix_from_oflags(oflags); - nix_all_oflags.insert(nix_oflags); - if nix_all_oflags.contains(OFlag::O_CREAT) { - needed_base |= host::__WASI_RIGHT_PATH_CREATE_FILE as host::__wasi_rights_t; - } - if nix_all_oflags.contains(OFlag::O_TRUNC) { - needed_inheriting |= host::__WASI_RIGHT_PATH_FILESTAT_SET_SIZE as host::__wasi_rights_t; +fn wasi_clock_to_relative_ns_delay( + wasi_clock: host::__wasi_subscription_t___wasi_subscription_u___wasi_subscription_u_clock_t, +) -> u128 { + if wasi_clock.flags != wasm32::__WASI_SUBSCRIPTION_CLOCK_ABSTIME { + return wasi_clock.timeout as u128; } + let now: u128 = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Current date is before the epoch") + .as_nanos(); + let deadline = wasi_clock.timeout as u128; + deadline.saturating_sub(now) +} - // convert file descriptor flags - nix_all_oflags.insert(host::nix_from_fdflags(fs_flags)); - if nix_all_oflags.contains(OFlag::O_DSYNC) { - needed_inheriting |= host::__WASI_RIGHT_FD_DATASYNC as host::__wasi_rights_t; - } - if nix_all_oflags.intersects(O_RSYNC | OFlag::O_SYNC) { - needed_inheriting |= host::__WASI_RIGHT_FD_SYNC as host::__wasi_rights_t; - } +#[derive(Debug, Copy, Clone)] +struct ClockEventData { + delay: u128, + userdata: host::__wasi_userdata_t, +} +#[derive(Debug, Copy, Clone)] +struct FdEventData { + fd: c_int, + type_: host::__wasi_eventtype_t, + userdata: host::__wasi_userdata_t, +} - let mut vmctx = unsafe { Vmctx::from_raw(vmctx) }; - - let path = match unsafe { dec_slice_of::(&mut vmctx, path_ptr, path_len) } { - Ok((ptr, len)) => OsStr::from_bytes(unsafe { std::slice::from_raw_parts(ptr, len) }), - Err(e) => return enc_errno(e), - }; - - let (dir, path) = match path_get( - &vmctx, - dirfd, - dirflags, - path, - needed_base, - needed_inheriting, - nix_oflags.contains(OFlag::O_CREAT), - ) { - Ok((dir, path)) => (dir, path), - Err(e) => return enc_errno(e), - }; - - let new_fd = match openat( - dir, - path.as_os_str(), - nix_all_oflags, - Mode::from_bits_truncate(0o777), - ) { - Ok(fd) => fd, - Err(e) => { - match e.as_errno() { - // Linux returns ENXIO instead of EOPNOTSUPP when opening a socket - Some(Errno::ENXIO) => { - if let Ok(stat) = fstatat(dir, path.as_os_str(), AtFlags::AT_SYMLINK_NOFOLLOW) { - if SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFSOCK) { - return wasm32::__WASI_ENOTSUP; - } else { - return wasm32::__WASI_ENXIO; - } - } else { - return wasm32::__WASI_ENXIO; - } - } - Some(e) => return wasm32::errno_from_nix(e), - None => return wasm32::__WASI_ENOSYS, - } +fn __wasi_poll_oneoff_handle_timeout_event( + vmctx: &mut Vmctx, + output_slice: &mut [wasm32::__wasi_event_t], + nevents: wasm32::uintptr_t, + timeout: Option, +) -> wasm32::__wasi_errno_t { + if let Some(ClockEventData { userdata, .. }) = timeout { + let output_event = host::__wasi_event_t { + userdata, + type_: wasm32::__WASI_EVENTTYPE_CLOCK, + error: wasm32::__WASI_ESUCCESS, + u: host::__wasi_event_t___wasi_event_u { + fd_readwrite: host::__wasi_event_t___wasi_event_u___wasi_event_u_fd_readwrite_t { + nbytes: 0, + flags: 0, + }, + }, + }; + output_slice[0] = enc_event(output_event); + if let Err(e) = unsafe { enc_pointee(vmctx, nevents, 1) } { + return enc_errno(e); } - }; - - // Determine the type of the new file descriptor and which rights contradict with this type - let guest_fd = match unsafe { determine_type_rights(new_fd) } { - Err(e) => { - // if `close` fails, note it but do not override the underlying errno - nix::unistd::close(new_fd).unwrap_or_else(|e| { - dbg!(e); - }); + } else { + // shouldn't happen + if let Err(e) = unsafe { enc_pointee(vmctx, nevents, 0) } { return enc_errno(e); } - Ok((_ty, max_base, max_inheriting)) => { - let mut fe = unsafe { FdEntry::from_raw_fd(new_fd) }; - fe.rights_base &= max_base; - fe.rights_inheriting &= max_inheriting; - match vmctx.get_embed_ctx_mut::().insert_fd_entry(fe) { - Ok(fd) => fd, - Err(e) => return enc_errno(e), - } - } - }; + } + wasm32::__WASI_ESUCCESS +} - unsafe { - enc_fd_byref(&mut vmctx, fd_out_ptr, guest_fd) - .map(|_| wasm32::__WASI_ESUCCESS) - .unwrap_or_else(|e| e) +fn __wasi_poll_oneoff_handle_fd_event<'t>( + vmctx: &mut Vmctx, + output_slice: &mut [wasm32::__wasi_event_t], + nevents: wasm32::uintptr_t, + events: impl Iterator, +) -> wasm32::__wasi_errno_t { + let mut output_slice_cur = output_slice.iter_mut(); + let mut revents_count = 0; + for (fd_event, poll_fd) in events { + let revents = match poll_fd.revents() { + Some(revents) => revents, + None => continue, + }; + let mut nbytes = 0; + if fd_event.type_ == wasm32::__WASI_EVENTTYPE_FD_READ { + let _ = unsafe { fionread(fd_event.fd, &mut nbytes) }; + } + let output_event = if revents.contains(nix::poll::EventFlags::POLLNVAL) { + host::__wasi_event_t { + userdata: fd_event.userdata, + type_: fd_event.type_, + error: wasm32::__WASI_EBADF, + u: host::__wasi_event_t___wasi_event_u { + fd_readwrite: + host::__wasi_event_t___wasi_event_u___wasi_event_u_fd_readwrite_t { + nbytes: 0, + flags: wasm32::__WASI_EVENT_FD_READWRITE_HANGUP, + }, + }, + } + } else if revents.contains(nix::poll::EventFlags::POLLERR) { + host::__wasi_event_t { + userdata: fd_event.userdata, + type_: fd_event.type_, + error: wasm32::__WASI_EIO, + u: host::__wasi_event_t___wasi_event_u { + fd_readwrite: + host::__wasi_event_t___wasi_event_u___wasi_event_u_fd_readwrite_t { + nbytes: 0, + flags: wasm32::__WASI_EVENT_FD_READWRITE_HANGUP, + }, + }, + } + } else if revents.contains(nix::poll::EventFlags::POLLHUP) { + host::__wasi_event_t { + userdata: fd_event.userdata, + type_: fd_event.type_, + error: wasm32::__WASI_ESUCCESS, + u: host::__wasi_event_t___wasi_event_u { + fd_readwrite: + host::__wasi_event_t___wasi_event_u___wasi_event_u_fd_readwrite_t { + nbytes: 0, + flags: wasm32::__WASI_EVENT_FD_READWRITE_HANGUP, + }, + }, + } + } else if revents.contains(nix::poll::EventFlags::POLLIN) + | revents.contains(nix::poll::EventFlags::POLLOUT) + { + host::__wasi_event_t { + userdata: fd_event.userdata, + type_: fd_event.type_, + error: wasm32::__WASI_ESUCCESS, + u: host::__wasi_event_t___wasi_event_u { + fd_readwrite: + host::__wasi_event_t___wasi_event_u___wasi_event_u_fd_readwrite_t { + nbytes: nbytes as host::__wasi_filesize_t, + flags: 0, + }, + }, + } + } else { + continue; + }; + *output_slice_cur.next().unwrap() = enc_event(output_event); + revents_count += 1; + } + if let Err(e) = unsafe { enc_pointee(vmctx, nevents, revents_count) } { + return enc_errno(e); } + wasm32::__WASI_ESUCCESS } /// Normalizes a path to ensure that the target path is located under the directory provided. @@ -902,7 +1130,7 @@ pub fn path_get>( Err(errno) } - let ctx: &WasiCtx = vmctx.get_embed_ctx(); + let ctx = vmctx.get_embed_ctx::(); let dirfe = ctx.get_fd_entry(dirfd, needed_base, needed_inheriting)?; @@ -1121,264 +1349,6 @@ pub fn path_get>( } } -#[no_mangle] -pub extern "C" fn __wasi_random_get( - vmctx: *mut lucet_vmctx, - buf_ptr: wasm32::uintptr_t, - buf_len: wasm32::size_t, -) -> wasm32::__wasi_errno_t { - use rand::{thread_rng, RngCore}; - - let mut vmctx = unsafe { Vmctx::from_raw(vmctx) }; - - let buf_len = dec_usize(buf_len); - let buf_ptr = match unsafe { dec_ptr(&mut vmctx, buf_ptr, buf_len) } { - Ok(ptr) => ptr, - Err(e) => return enc_errno(e), - }; - - let buf = unsafe { std::slice::from_raw_parts_mut(buf_ptr, buf_len) }; - - thread_rng().fill_bytes(buf); - - return wasm32::__WASI_ESUCCESS; -} - -// define the `fionread()` function, equivalent to `ioctl(fd, FIONREAD, *bytes)` -nix::ioctl_read_bad!(fionread, nix::libc::FIONREAD, c_int); - -fn wasi_clock_to_relative_ns_delay( - wasi_clock: host::__wasi_subscription_t___wasi_subscription_u___wasi_subscription_u_clock_t, -) -> u128 { - if wasi_clock.flags != wasm32::__WASI_SUBSCRIPTION_CLOCK_ABSTIME { - return wasi_clock.timeout as u128; - } - let now: u128 = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("Current date is before the epoch") - .as_nanos(); - let deadline = wasi_clock.timeout as u128; - deadline.saturating_sub(now) -} - -#[derive(Debug, Copy, Clone)] -struct ClockEventData { - delay: u128, - userdata: host::__wasi_userdata_t, -} -#[derive(Debug, Copy, Clone)] -struct FdEventData { - fd: c_int, - type_: host::__wasi_eventtype_t, - userdata: host::__wasi_userdata_t, -} - -fn __wasi_poll_oneoff_handle_timeout_event( - vmctx: &mut Vmctx, - output_slice: &mut [wasm32::__wasi_event_t], - nevents: wasm32::uintptr_t, - timeout: Option, -) -> wasm32::__wasi_errno_t { - if let Some(ClockEventData { userdata, .. }) = timeout { - let output_event = host::__wasi_event_t { - userdata, - type_: wasm32::__WASI_EVENTTYPE_CLOCK, - error: wasm32::__WASI_ESUCCESS, - u: host::__wasi_event_t___wasi_event_u { - fd_readwrite: host::__wasi_event_t___wasi_event_u___wasi_event_u_fd_readwrite_t { - nbytes: 0, - flags: 0, - }, - }, - }; - output_slice[0] = enc_event(output_event); - if let Err(e) = unsafe { enc_pointee(vmctx, nevents, 1) } { - return enc_errno(e); - } - } else { - // shouldn't happen - if let Err(e) = unsafe { enc_pointee(vmctx, nevents, 0) } { - return enc_errno(e); - } - } - wasm32::__WASI_ESUCCESS -} - -fn __wasi_poll_oneoff_handle_fd_event<'t>( - vmctx: &mut Vmctx, - output_slice: &mut [wasm32::__wasi_event_t], - nevents: wasm32::uintptr_t, - events: impl Iterator, -) -> wasm32::__wasi_errno_t { - let mut output_slice_cur = output_slice.iter_mut(); - let mut revents_count = 0; - for (fd_event, poll_fd) in events { - let revents = match poll_fd.revents() { - Some(revents) => revents, - None => continue, - }; - let mut nbytes = 0; - if fd_event.type_ == wasm32::__WASI_EVENTTYPE_FD_READ { - let _ = unsafe { fionread(fd_event.fd, &mut nbytes) }; - } - let output_event = if revents.contains(nix::poll::EventFlags::POLLNVAL) { - host::__wasi_event_t { - userdata: fd_event.userdata, - type_: fd_event.type_, - error: wasm32::__WASI_EBADF, - u: host::__wasi_event_t___wasi_event_u { - fd_readwrite: - host::__wasi_event_t___wasi_event_u___wasi_event_u_fd_readwrite_t { - nbytes: 0, - flags: wasm32::__WASI_EVENT_FD_READWRITE_HANGUP, - }, - }, - } - } else if revents.contains(nix::poll::EventFlags::POLLERR) { - host::__wasi_event_t { - userdata: fd_event.userdata, - type_: fd_event.type_, - error: wasm32::__WASI_EIO, - u: host::__wasi_event_t___wasi_event_u { - fd_readwrite: - host::__wasi_event_t___wasi_event_u___wasi_event_u_fd_readwrite_t { - nbytes: 0, - flags: wasm32::__WASI_EVENT_FD_READWRITE_HANGUP, - }, - }, - } - } else if revents.contains(nix::poll::EventFlags::POLLHUP) { - host::__wasi_event_t { - userdata: fd_event.userdata, - type_: fd_event.type_, - error: wasm32::__WASI_ESUCCESS, - u: host::__wasi_event_t___wasi_event_u { - fd_readwrite: - host::__wasi_event_t___wasi_event_u___wasi_event_u_fd_readwrite_t { - nbytes: 0, - flags: wasm32::__WASI_EVENT_FD_READWRITE_HANGUP, - }, - }, - } - } else if revents.contains(nix::poll::EventFlags::POLLIN) - | revents.contains(nix::poll::EventFlags::POLLOUT) - { - host::__wasi_event_t { - userdata: fd_event.userdata, - type_: fd_event.type_, - error: wasm32::__WASI_ESUCCESS, - u: host::__wasi_event_t___wasi_event_u { - fd_readwrite: - host::__wasi_event_t___wasi_event_u___wasi_event_u_fd_readwrite_t { - nbytes: nbytes as host::__wasi_filesize_t, - flags: 0, - }, - }, - } - } else { - continue; - }; - *output_slice_cur.next().unwrap() = enc_event(output_event); - revents_count += 1; - } - if let Err(e) = unsafe { enc_pointee(vmctx, nevents, revents_count) } { - return enc_errno(e); - } - wasm32::__WASI_ESUCCESS -} - -#[no_mangle] -pub extern "C" fn __wasi_poll_oneoff( - vmctx: *mut lucet_vmctx, - input: wasm32::uintptr_t, - output: wasm32::uintptr_t, - nsubscriptions: wasm32::size_t, - nevents: wasm32::uintptr_t, -) -> wasm32::__wasi_errno_t { - if nsubscriptions as u64 > wasm32::__wasi_filesize_t::max_value() { - return wasm32::__WASI_EINVAL; - } - let mut vmctx = unsafe { Vmctx::from_raw(vmctx) }; - unsafe { enc_pointee(&mut vmctx, nevents, 0) }.unwrap(); - let input_slice_ = - unsafe { dec_slice_of::(&mut vmctx, input, nsubscriptions) } - .unwrap(); - let input_slice = unsafe { slice::from_raw_parts(input_slice_.0, input_slice_.1) }; - - let output_slice_ = - unsafe { dec_slice_of::(&mut vmctx, output, nsubscriptions) } - .unwrap(); - let output_slice = unsafe { slice::from_raw_parts_mut(output_slice_.0, output_slice_.1) }; - - let input: Vec<_> = input_slice.iter().map(|x| dec_subscription(x)).collect(); - - let timeout = input - .iter() - .filter_map(|event| match event { - Ok(event) if event.type_ == wasm32::__WASI_EVENTTYPE_CLOCK => Some(ClockEventData { - delay: wasi_clock_to_relative_ns_delay(unsafe { event.u.clock }) / 1_000_000, - userdata: event.userdata, - }), - _ => None, - }) - .min_by_key(|event| event.delay); - let fd_events: Vec<_> = input - .iter() - .filter_map(|event| match event { - Ok(event) - if event.type_ == wasm32::__WASI_EVENTTYPE_FD_READ - || event.type_ == wasm32::__WASI_EVENTTYPE_FD_WRITE => - { - Some(FdEventData { - fd: unsafe { event.u.fd_readwrite.fd } as c_int, - type_: event.type_, - userdata: event.userdata, - }) - } - _ => None, - }) - .collect(); - if fd_events.is_empty() && timeout.is_none() { - return wasm32::__WASI_ESUCCESS; - } - let mut poll_fds: Vec<_> = fd_events - .iter() - .map(|event| { - let mut flags = nix::poll::EventFlags::empty(); - match event.type_ { - wasm32::__WASI_EVENTTYPE_FD_READ => flags.insert(nix::poll::EventFlags::POLLIN), - wasm32::__WASI_EVENTTYPE_FD_WRITE => flags.insert(nix::poll::EventFlags::POLLOUT), - // An event on a file descriptor can currently only be of type FD_READ or FD_WRITE - // Nothing else has been defined in the specification, and these are also the only two - // events we filtered before. If we get something else here, the code has a serious bug. - _ => unreachable!(), - }; - nix::poll::PollFd::new(event.fd, flags) - }) - .collect(); - let timeout = timeout.map(|ClockEventData { delay, userdata }| ClockEventData { - delay: cmp::min(delay, c_int::max_value() as u128), - userdata, - }); - let poll_timeout = timeout.map(|timeout| timeout.delay as c_int).unwrap_or(-1); - let ready = loop { - match nix::poll::poll(&mut poll_fds, poll_timeout) { - Err(_) => { - if nix::errno::Errno::last() == nix::errno::Errno::EINTR { - continue; - } - return wasm32::errno_from_nix(nix::errno::Errno::last()); - } - Ok(ready) => break ready as usize, - } - }; - if ready == 0 { - return __wasi_poll_oneoff_handle_timeout_event(&mut vmctx, output_slice, nevents, timeout); - } - let events = fd_events.iter().zip(poll_fds.iter()).take(ready); - __wasi_poll_oneoff_handle_fd_event(&mut vmctx, output_slice, nevents, events) -} - #[doc(hidden)] pub fn ensure_linked() { unsafe { diff --git a/lucet-wasi/src/main.rs b/lucet-wasi/src/main.rs index eb9837c6b..67ee5b90c 100644 --- a/lucet-wasi/src/main.rs +++ b/lucet-wasi/src/main.rs @@ -175,7 +175,10 @@ fn run(config: Config) { let args = std::iter::once(config.lucet_module) .chain(config.guest_args.into_iter()) .collect::>(); - let mut ctx = WasiCtxBuilder::new().args(&args).inherit_env(); + let mut ctx = WasiCtxBuilder::new() + .args(&args) + .inherit_stdio() + .inherit_env(); for (dir, guest_path) in config.preopen_dirs { ctx = ctx.preopened_dir(dir, guest_path); } diff --git a/lucet-wasi/src/memory.rs b/lucet-wasi/src/memory.rs index 57f81e17b..2ae54ac59 100644 --- a/lucet-wasi/src/memory.rs +++ b/lucet-wasi/src/memory.rs @@ -23,11 +23,11 @@ macro_rules! bail_errno { } pub unsafe fn dec_ptr( - vmctx: &mut Vmctx, + vmctx: &Vmctx, ptr: wasm32::uintptr_t, len: usize, ) -> Result<*mut u8, host::__wasi_errno_t> { - let heap = vmctx.heap_mut(); + let mut heap = vmctx.heap_mut(); // check that `len` fits in the wasm32 address space if len > wasm32::UINTPTR_MAX as usize { @@ -44,7 +44,7 @@ pub unsafe fn dec_ptr( } pub unsafe fn dec_ptr_to( - vmctx: &mut Vmctx, + vmctx: &Vmctx, ptr: wasm32::uintptr_t, ) -> Result<*mut T, host::__wasi_errno_t> { // check that the ptr is aligned @@ -55,14 +55,14 @@ pub unsafe fn dec_ptr_to( } pub unsafe fn dec_pointee( - vmctx: &mut Vmctx, + vmctx: &Vmctx, ptr: wasm32::uintptr_t, ) -> Result { dec_ptr_to::(vmctx, ptr).map(|p| p.read()) } pub unsafe fn enc_pointee( - vmctx: &mut Vmctx, + vmctx: &Vmctx, ptr: wasm32::uintptr_t, t: T, ) -> Result<(), host::__wasi_errno_t> { @@ -70,7 +70,7 @@ pub unsafe fn enc_pointee( } pub unsafe fn dec_slice_of( - vmctx: &mut Vmctx, + vmctx: &Vmctx, ptr: wasm32::uintptr_t, len: wasm32::size_t, ) -> Result<(*mut T, usize), host::__wasi_errno_t> { @@ -91,7 +91,7 @@ pub unsafe fn dec_slice_of( } pub unsafe fn enc_slice_of( - vmctx: &mut Vmctx, + vmctx: &Vmctx, slice: &[T], ptr: wasm32::uintptr_t, ) -> Result<(), host::__wasi_errno_t> { @@ -120,7 +120,7 @@ macro_rules! dec_enc_scalar { } pub unsafe fn $dec_byref( - vmctx: &mut Vmctx, + vmctx: &Vmctx, ptr: wasm32::uintptr_t, ) -> Result { dec_pointee::(vmctx, ptr).map($dec) @@ -131,7 +131,7 @@ macro_rules! dec_enc_scalar { } pub unsafe fn $enc_byref( - vmctx: &mut Vmctx, + vmctx: &Vmctx, ptr: wasm32::uintptr_t, x: host::$ty, ) -> Result<(), host::__wasi_errno_t> { @@ -141,7 +141,7 @@ macro_rules! dec_enc_scalar { } pub unsafe fn dec_ciovec( - vmctx: &mut Vmctx, + vmctx: &Vmctx, ciovec: &wasm32::__wasi_ciovec_t, ) -> Result { let len = dec_usize(ciovec.buf_len); @@ -152,7 +152,7 @@ pub unsafe fn dec_ciovec( } pub unsafe fn dec_ciovec_slice( - vmctx: &mut Vmctx, + vmctx: &Vmctx, ptr: wasm32::uintptr_t, len: wasm32::size_t, ) -> Result, host::__wasi_errno_t> { @@ -226,7 +226,7 @@ pub fn dec_filestat(filestat: wasm32::__wasi_filestat_t) -> host::__wasi_filesta } pub unsafe fn dec_filestat_byref( - vmctx: &mut Vmctx, + vmctx: &Vmctx, filestat_ptr: wasm32::uintptr_t, ) -> Result { dec_pointee::(vmctx, filestat_ptr).map(dec_filestat) @@ -246,7 +246,7 @@ pub fn enc_filestat(filestat: host::__wasi_filestat_t) -> wasm32::__wasi_filesta } pub unsafe fn enc_filestat_byref( - vmctx: &mut Vmctx, + vmctx: &Vmctx, filestat_ptr: wasm32::uintptr_t, host_filestat: host::__wasi_filestat_t, ) -> Result<(), host::__wasi_errno_t> { @@ -264,7 +264,7 @@ pub fn dec_fdstat(fdstat: wasm32::__wasi_fdstat_t) -> host::__wasi_fdstat_t { } pub unsafe fn dec_fdstat_byref( - vmctx: &mut Vmctx, + vmctx: &Vmctx, fdstat_ptr: wasm32::uintptr_t, ) -> Result { dec_pointee::(vmctx, fdstat_ptr).map(dec_fdstat) @@ -281,7 +281,7 @@ pub fn enc_fdstat(fdstat: host::__wasi_fdstat_t) -> wasm32::__wasi_fdstat_t { } pub unsafe fn enc_fdstat_byref( - vmctx: &mut Vmctx, + vmctx: &Vmctx, fdstat_ptr: wasm32::uintptr_t, host_fdstat: host::__wasi_fdstat_t, ) -> Result<(), host::__wasi_errno_t> { @@ -348,7 +348,7 @@ pub fn dec_prestat( } pub unsafe fn dec_prestat_byref( - vmctx: &mut Vmctx, + vmctx: &Vmctx, prestat_ptr: wasm32::uintptr_t, ) -> Result { dec_pointee::(vmctx, prestat_ptr).and_then(dec_prestat) @@ -374,7 +374,7 @@ pub fn enc_prestat( } pub unsafe fn enc_prestat_byref( - vmctx: &mut Vmctx, + vmctx: &Vmctx, prestat_ptr: wasm32::uintptr_t, host_prestat: host::__wasi_prestat_t, ) -> Result<(), host::__wasi_errno_t> { @@ -406,7 +406,7 @@ pub fn enc_usize(size: usize) -> wasm32::size_t { } pub unsafe fn enc_usize_byref( - vmctx: &mut Vmctx, + vmctx: &Vmctx, usize_ptr: wasm32::uintptr_t, host_usize: usize, ) -> Result<(), host::__wasi_errno_t> { diff --git a/lucetc/src/lib.rs b/lucetc/src/lib.rs index e00f2206f..f2035f6bf 100644 --- a/lucetc/src/lib.rs +++ b/lucetc/src/lib.rs @@ -65,6 +65,15 @@ pub trait LucetcOpts { fn max_reserved_size(&mut self, max_reserved_size: u64); fn with_max_reserved_size(self, max_reserved_size: u64) -> Self; + /// Set the reserved size exactly. + /// + /// Equivalent to setting the minimum and maximum reserved sizes to the same value. + fn reserved_size(&mut self, reserved_size: u64); + /// Set the reserved size exactly. + /// + /// Equivalent to setting the minimum and maximum reserved sizes to the same value. + fn with_reserved_size(self, reserved_size: u64) -> Self; + fn guard_size(&mut self, guard_size: u64); fn with_guard_size(self, guard_size: u64) -> Self; } @@ -117,6 +126,16 @@ impl LucetcOpts for T { self } + fn reserved_size(&mut self, reserved_size: u64) { + self.as_lucetc().heap.min_reserved_size = reserved_size; + self.as_lucetc().heap.max_reserved_size = reserved_size; + } + + fn with_reserved_size(mut self, reserved_size: u64) -> Self { + self.reserved_size(reserved_size); + self + } + fn guard_size(&mut self, guard_size: u64) { self.as_lucetc().heap.guard_size = guard_size; } diff --git a/lucetc/src/main.rs b/lucetc/src/main.rs index 88539167a..c8f0fe82d 100644 --- a/lucetc/src/main.rs +++ b/lucetc/src/main.rs @@ -58,6 +58,11 @@ pub fn run(opts: &Options) -> Result<(), Error> { c.max_reserved_size(max_reserved_size); } + // this comes after min and max, so it overrides them if present + if let Some(reserved_size) = opts.reserved_size { + c.reserved_size(reserved_size); + } + if let Some(guard_size) = opts.guard_size { c.guard_size(guard_size); } diff --git a/lucetc/src/options.rs b/lucetc/src/options.rs index 46ade8c18..427b0a45b 100644 --- a/lucetc/src/options.rs +++ b/lucetc/src/options.rs @@ -38,6 +38,7 @@ pub struct Options { pub builtins_path: Option, pub min_reserved_size: Option, pub max_reserved_size: Option, + pub reserved_size: Option, pub guard_size: Option, pub opt_level: OptLevel, } @@ -80,6 +81,12 @@ impl Options { None }; + let reserved_size = if let Some(reserved_str) = m.value_of("reserved_size") { + Some(parse_humansized(reserved_str)?) + } else { + None + }; + let guard_size = if let Some(guard_str) = m.value_of("guard_size") { Some(parse_humansized(guard_str)?) } else { @@ -102,6 +109,7 @@ impl Options { builtins_path, min_reserved_size, max_reserved_size, + reserved_size, guard_size, opt_level, }) @@ -154,6 +162,13 @@ impl Options { .multiple(false) .help("maximum size of usable linear memory region. must be multiple of 4k. default: 4 GiB"), ) + .arg( + Arg::with_name("reserved_size") + .long("--reserved-size") + .takes_value(true) + .multiple(false) + .help("exact size of usable linear memory region, overriding --{min,max}-reserved-size. must be multiple of 4k"), + ) .arg( Arg::with_name("guard_size") .long("--guard-size")