From eb02806bc8ba6ef8877b9f56b76945e1d6436e3e Mon Sep 17 00:00:00 2001 From: Simonas Kazlauskas Date: Thu, 6 Jun 2024 15:06:40 +0300 Subject: [PATCH] =?UTF-8?q?vm:=20import=20construction=20=E2=86=92=20respe?= =?UTF-8?q?ctive=20VM=20impls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I always found it weird that we had VM specific code in what's a generic part of the near-vm-runner. Well -- it has actually started bothering me in real ways, so it gets moved. I didn't do very much to make sure it ends up pretty. And I don't think I want to for the VMs that're not wasmtime or near_vm. Good thing is that the wasmer0/2 code can be largely ignored sans changes to `VMLogic` (which I'm considering addressing next...) --- runtime/near-vm-runner/src/imports.rs | 451 +----------------- .../src/near_vm_runner/runner.rs | 142 +++++- runtime/near-vm-runner/src/wasmer2_runner.rs | 153 +++++- runtime/near-vm-runner/src/wasmer_runner.rs | 51 +- runtime/near-vm-runner/src/wasmtime_runner.rs | 81 +++- 5 files changed, 417 insertions(+), 461 deletions(-) diff --git a/runtime/near-vm-runner/src/imports.rs b/runtime/near-vm-runner/src/imports.rs index e00c6143afe..cfdb626313c 100644 --- a/runtime/near-vm-runner/src/imports.rs +++ b/runtime/near-vm-runner/src/imports.rs @@ -47,13 +47,13 @@ //! make imports retroactively available to old transactions. So //! `for_each_available_import` takes care to invoke `M!` only for currently //! available imports. - -#[cfg(any( +#![cfg(any( feature = "wasmer0_vm", feature = "wasmer2_vm", feature = "near_vm", feature = "wasmtime_vm" ))] + macro_rules! call_with_name { ( $M:ident => @in $mod:ident : $func:ident < [ $( $arg_name:ident : $arg_type:ident ),* ] -> [ $( $returns:ident ),* ] > ) => { $M!($mod / $func : $func < [ $( $arg_name : $arg_type ),* ] -> [ $( $returns ),* ] >) @@ -73,17 +73,11 @@ macro_rules! imports { $( @as $name:ident : )? $func:ident < [ $( $arg_name:ident : $arg_type:ident ),* ] -> [ $( $returns:ident ),* ] >,)* ) => { - #[cfg(any( - feature = "wasmer0_vm", - feature = "wasmer2_vm", - feature = "near_vm", - feature = "wasmtime_vm" - ))] macro_rules! for_each_available_import { ($config:expr, $M:ident) => {$( $(#[cfg(feature = $feature_name)])? if true $(&& ($config).$config_field)? { - call_with_name!($M => $( @in $mod : )? $( @as $name : )? $func < [ $( $arg_name : $arg_type ),* ] -> [ $( $returns ),* ] >); + $crate::imports::call_with_name!($M => $( @in $mod : )? $( @as $name : )? $func < [ $( $arg_name : $arg_type ),* ] -> [ $( $returns ),* ] >); } )*} } @@ -297,439 +291,8 @@ imports! { ##["test_features"] burn_gas<[gas: u64] -> []>, } -#[cfg(all(feature = "wasmer0_vm", target_arch = "x86_64"))] -pub(crate) mod wasmer { - use crate::logic::{VMLogic, VMLogicError}; - use std::ffi::c_void; - - #[derive(Clone, Copy)] - struct ImportReference(pub *mut c_void); - unsafe impl Send for ImportReference {} - unsafe impl Sync for ImportReference {} - - pub(crate) fn build( - memory: wasmer_runtime::memory::Memory, - logic: &mut VMLogic<'_>, - ) -> wasmer_runtime::ImportObject { - let raw_ptr = logic as *mut _ as *mut c_void; - let import_reference = ImportReference(raw_ptr); - let mut import_object = wasmer_runtime::ImportObject::new_with_data(move || { - let dtor = (|_: *mut c_void| {}) as fn(*mut c_void); - ({ import_reference }.0, dtor) - }); - - let mut ns_internal = wasmer_runtime_core::import::Namespace::new(); - let mut ns_env = wasmer_runtime_core::import::Namespace::new(); - ns_env.insert("memory", memory); - - macro_rules! add_import { - ( - $mod:ident / $name:ident : $func:ident < [ $( $arg_name:ident : $arg_type:ident ),* ] -> [ $( $returns:ident ),* ] > - ) => { - #[allow(unused_parens)] - fn $name( ctx: &mut wasmer_runtime::Ctx, $( $arg_name: $arg_type ),* ) -> Result<($( $returns ),*), VMLogicError> { - const TRACE: bool = $crate::imports::should_trace_host_function(stringify!($name)); - let _span = TRACE.then(|| { - tracing::trace_span!(target: "vm::host_function", stringify!($name)).entered() - }); - let logic: &mut VMLogic<'_> = unsafe { &mut *(ctx.data as *mut VMLogic<'_>) }; - logic.$func( $( $arg_name, )* ) - } - - match stringify!($mod) { - "env" => ns_env.insert(stringify!($name), wasmer_runtime::func!($name)), - "internal" => ns_internal.insert(stringify!($name), wasmer_runtime::func!($name)), - _ => unimplemented!(), - } - }; - } - for_each_available_import!(logic.config, add_import); - - import_object.register("env", ns_env); - import_object.register("internal", ns_internal); - import_object - } -} - -#[cfg(all(feature = "wasmer2_vm", target_arch = "x86_64"))] -pub(crate) mod wasmer2 { - use crate::logic::VMLogic; - use std::sync::Arc; - use wasmer_engine::Engine; - use wasmer_engine_universal::UniversalEngine; - use wasmer_vm::{ - ExportFunction, ExportFunctionMetadata, Resolver, VMFunction, VMFunctionKind, VMMemory, - }; - - pub(crate) struct Wasmer2Imports<'engine, 'vmlogic, 'vmlogic_refs> { - pub(crate) memory: VMMemory, - // Note: this same object is also referenced by the `metadata` field! - pub(crate) vmlogic: &'vmlogic mut VMLogic<'vmlogic_refs>, - pub(crate) metadata: Arc, - pub(crate) engine: &'engine UniversalEngine, - } - - trait Wasmer2Type { - type Wasmer; - fn to_wasmer(self) -> Self::Wasmer; - fn ty() -> wasmer_types::Type; - } - macro_rules! wasmer_types { - ($($native:ty as $wasmer:ty => $type_expr:expr;)*) => { - $(impl Wasmer2Type for $native { - type Wasmer = $wasmer; - fn to_wasmer(self) -> $wasmer { - self as _ - } - fn ty() -> wasmer_types::Type { - $type_expr - } - })* - } - } - wasmer_types! { - u32 as i32 => wasmer_types::Type::I32; - u64 as i64 => wasmer_types::Type::I64; - } - - macro_rules! return_ty { - ($return_type: ident = [ ]) => { - type $return_type = (); - fn make_ret() -> () {} - }; - ($return_type: ident = [ $($returns: ident),* ]) => { - #[repr(C)] - struct $return_type($(<$returns as Wasmer2Type>::Wasmer),*); - fn make_ret($($returns: $returns),*) -> Ret { Ret($($returns.to_wasmer()),*) } - } - } - - impl<'e, 'l, 'lr> Resolver for Wasmer2Imports<'e, 'l, 'lr> { - fn resolve(&self, _index: u32, module: &str, field: &str) -> Option { - if module == "env" && field == "memory" { - return Some(wasmer_vm::Export::Memory(self.memory.clone())); - } - - macro_rules! add_import { - ( - $mod:ident / $name:ident : $func:ident < - [ $( $arg_name:ident : $arg_type:ident ),* ] - -> [ $( $returns:ident ),* ] - > - ) => { - return_ty!(Ret = [ $($returns),* ]); - - extern "C" fn $name(env: *mut VMLogic<'_>, $( $arg_name: $arg_type ),* ) - -> Ret { - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - const TRACE: bool = $crate::imports::should_trace_host_function(stringify!($name)); - let _span = TRACE.then(|| { - tracing::trace_span!(target: "vm::host_function", stringify!($name)).entered() - }); - - // SAFETY: This code should only be executable within `'vmlogic` - // lifetime and so it is safe to dereference the `env` pointer which is - // known to be derived from a valid `&'vmlogic mut VMLogic<'_>` in the - // first place. - unsafe { (*env).$func( $( $arg_name, )* ) } - })); - // We want to ensure that the only kind of error that host function calls - // return are VMLogicError. This is important because we later attempt to - // downcast the `RuntimeError`s into `VMLogicError`. - let result: Result, _> = result; - #[allow(unused_parens)] - match result { - Ok(Ok(($($returns),*))) => make_ret($($returns),*), - Ok(Err(trap)) => unsafe { - // SAFETY: this can only be called by a WASM contract, so all the - // necessary hooks are known to be in place. - wasmer_vm::raise_user_trap(Box::new(trap)) - }, - Err(e) => unsafe { - // SAFETY: this can only be called by a WASM contract, so all the - // necessary hooks are known to be in place. - wasmer_vm::resume_panic(e) - }, - } - } - // TODO: a phf hashmap would probably work better here. - if module == stringify!($mod) && field == stringify!($name) { - let args = [$(<$arg_type as Wasmer2Type>::ty()),*]; - let rets = [$(<$returns as Wasmer2Type>::ty()),*]; - let signature = wasmer_types::FunctionTypeRef::new(&args[..], &rets[..]); - let signature = self.engine.register_signature(signature); - return Some(wasmer_vm::Export::Function(ExportFunction { - vm_function: VMFunction { - address: $name as *const _, - // SAFETY: here we erase the lifetime of the `vmlogic` reference, - // but we believe that the lifetimes on `Wasmer2Imports` enforce - // sufficiently that it isn't possible to call this exported - // function when vmlogic is no loger live. - vmctx: wasmer_vm::VMFunctionEnvironment { - host_env: self.vmlogic as *const _ as *mut _ - }, - signature, - kind: VMFunctionKind::Static, - call_trampoline: None, - instance_ref: None, - }, - metadata: Some(Arc::clone(&self.metadata)), - })); - } - }; - } - for_each_available_import!(self.vmlogic.config, add_import); - return None; - } - } - - pub(crate) fn build<'e, 'a, 'b>( - memory: VMMemory, - logic: &'a mut VMLogic<'b>, - engine: &'e UniversalEngine, - ) -> Wasmer2Imports<'e, 'a, 'b> { - let metadata = unsafe { - // SAFETY: the functions here are thread-safe. We ensure that the lifetime of `VMLogic` - // is sufficiently long by tying the lifetime of VMLogic to the return type which - // contains this metadata. - ExportFunctionMetadata::new(logic as *mut _ as *mut _, None, |ptr| ptr, |_| {}) - }; - Wasmer2Imports { memory, vmlogic: logic, metadata: Arc::new(metadata), engine } - } -} - -#[cfg(all(feature = "near_vm", target_arch = "x86_64"))] -pub(crate) mod near_vm { - use crate::logic::VMLogic; - use near_vm_engine::universal::UniversalEngine; - use near_vm_vm::{ - ExportFunction, ExportFunctionMetadata, Resolver, VMFunction, VMFunctionKind, VMMemory, - }; - use std::sync::Arc; - - pub(crate) struct NearVmImports<'engine, 'vmlogic, 'vmlogic_refs> { - pub(crate) memory: VMMemory, - // Note: this same object is also referenced by the `metadata` field! - pub(crate) vmlogic: &'vmlogic mut VMLogic<'vmlogic_refs>, - pub(crate) metadata: Arc, - pub(crate) engine: &'engine UniversalEngine, - } - - trait NearVmType { - type NearVm; - fn to_near_vm(self) -> Self::NearVm; - fn ty() -> near_vm_types::Type; - } - macro_rules! near_vm_types { - ($($native:ty as $near_vm:ty => $type_expr:expr;)*) => { - $(impl NearVmType for $native { - type NearVm = $near_vm; - fn to_near_vm(self) -> $near_vm { - self as _ - } - fn ty() -> near_vm_types::Type { - $type_expr - } - })* - } - } - near_vm_types! { - u32 as i32 => near_vm_types::Type::I32; - u64 as i64 => near_vm_types::Type::I64; - } - - macro_rules! return_ty { - ($return_type: ident = [ ]) => { - type $return_type = (); - fn make_ret() -> () {} - }; - ($return_type: ident = [ $($returns: ident),* ]) => { - #[repr(C)] - struct $return_type($(<$returns as NearVmType>::NearVm),*); - fn make_ret($($returns: $returns),*) -> Ret { Ret($($returns.to_near_vm()),*) } - } - } - - impl<'e, 'l, 'lr> Resolver for NearVmImports<'e, 'l, 'lr> { - fn resolve(&self, _index: u32, module: &str, field: &str) -> Option { - if module == "env" && field == "memory" { - return Some(near_vm_vm::Export::Memory(self.memory.clone())); - } - - macro_rules! add_import { - ( - $mod:ident / $name:ident : $func:ident < - [ $( $arg_name:ident : $arg_type:ident ),* ] - -> [ $( $returns:ident ),* ] - > - ) => { - return_ty!(Ret = [ $($returns),* ]); - - extern "C" fn $name(env: *mut VMLogic<'_>, $( $arg_name: $arg_type ),* ) - -> Ret { - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - const TRACE: bool = $crate::imports::should_trace_host_function(stringify!($name)); - let _span = TRACE.then(|| { - tracing::trace_span!(target: "vm::host_function", stringify!($name)).entered() - }); - - // SAFETY: This code should only be executable within `'vmlogic` - // lifetime and so it is safe to dereference the `env` pointer which is - // known to be derived from a valid `&'vmlogic mut VMLogic<'_>` in the - // first place. - unsafe { (*env).$func( $( $arg_name, )* ) } - })); - // We want to ensure that the only kind of error that host function calls - // return are VMLogicError. This is important because we later attempt to - // downcast the `RuntimeError`s into `VMLogicError`. - let result: Result, _> = result; - #[allow(unused_parens)] - match result { - Ok(Ok(($($returns),*))) => make_ret($($returns),*), - Ok(Err(trap)) => unsafe { - // SAFETY: this can only be called by a WASM contract, so all the - // necessary hooks are known to be in place. - near_vm_vm::raise_user_trap(Box::new(trap)) - }, - Err(e) => unsafe { - // SAFETY: this can only be called by a WASM contract, so all the - // necessary hooks are known to be in place. - near_vm_vm::resume_panic(e) - }, - } - } - // TODO: a phf hashmap would probably work better here. - if module == stringify!($mod) && field == stringify!($name) { - let args = [$(<$arg_type as NearVmType>::ty()),*]; - let rets = [$(<$returns as NearVmType>::ty()),*]; - let signature = near_vm_types::FunctionType::new(&args[..], &rets[..]); - let signature = self.engine.register_signature(signature); - return Some(near_vm_vm::Export::Function(ExportFunction { - vm_function: VMFunction { - address: $name as *const _, - // SAFETY: here we erase the lifetime of the `vmlogic` reference, - // but we believe that the lifetimes on `NearVmImports` enforce - // sufficiently that it isn't possible to call this exported - // function when vmlogic is no loger live. - vmctx: near_vm_vm::VMFunctionEnvironment { - host_env: self.vmlogic as *const _ as *mut _ - }, - signature, - kind: VMFunctionKind::Static, - call_trampoline: None, - instance_ref: None, - }, - metadata: Some(Arc::clone(&self.metadata)), - })); - } - }; - } - for_each_available_import!(self.vmlogic.config, add_import); - return None; - } - } - - pub(crate) fn build<'e, 'a, 'b>( - memory: VMMemory, - logic: &'a mut VMLogic<'b>, - engine: &'e UniversalEngine, - ) -> NearVmImports<'e, 'a, 'b> { - let metadata = unsafe { - // SAFETY: the functions here are thread-safe. We ensure that the lifetime of `VMLogic` - // is sufficiently long by tying the lifetime of VMLogic to the return type which - // contains this metadata. - ExportFunctionMetadata::new(logic as *mut _ as *mut _, None, |ptr| ptr, |_| {}) - }; - NearVmImports { memory, vmlogic: logic, metadata: Arc::new(metadata), engine } - } -} - -#[cfg(feature = "wasmtime_vm")] -pub(crate) mod wasmtime { - use crate::logic::{VMLogic, VMLogicError}; - use std::cell::UnsafeCell; - use std::ffi::c_void; - - /// This is a container from which an error can be taken out by value. This is necessary as - /// `anyhow` does not really give any opportunity to grab causes by value and the VM Logic - /// errors end up a couple layers deep in a causal chain. - #[derive(Debug)] - pub(crate) struct ErrorContainer(std::sync::Mutex>); - impl ErrorContainer { - pub(crate) fn take(&self) -> Option { - let mut guard = self.0.lock().unwrap_or_else(|e| e.into_inner()); - guard.take() - } - } - impl std::error::Error for ErrorContainer {} - impl std::fmt::Display for ErrorContainer { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("VMLogic error occurred and is now stored in an opaque storage container") - } - } - - thread_local! { - static CALLER_CONTEXT: UnsafeCell<*mut c_void> = const { UnsafeCell::new(core::ptr::null_mut()) }; - } - - pub(crate) fn link<'a, 'b>( - linker: &mut wasmtime::Linker<()>, - memory: wasmtime::Memory, - store: &wasmtime::Store<()>, - logic: &'a mut VMLogic<'b>, - ) { - // Unfortunately, due to the Wasmtime implementation we have to do tricks with the - // lifetimes of the logic instance and pass raw pointers here. - // FIXME(nagisa): I believe this is no longer required, we just need to look at this code - // again. - let raw_logic = logic as *mut _ as *mut c_void; - CALLER_CONTEXT.with(|caller_context| unsafe { *caller_context.get() = raw_logic }); - linker.define(store, "env", "memory", memory).expect("cannot define memory"); - - macro_rules! add_import { - ( - $mod:ident / $name:ident : $func:ident < [ $( $arg_name:ident : $arg_type:ident ),* ] -> [ $( $returns:ident ),* ] > - ) => { - #[allow(unused_parens)] - fn $name(caller: wasmtime::Caller<'_, ()>, $( $arg_name: $arg_type ),* ) -> anyhow::Result<($( $returns ),*)> { - const TRACE: bool = $crate::imports::should_trace_host_function(stringify!($name)); - let _span = TRACE.then(|| { - tracing::trace_span!(target: "vm::host_function", stringify!($name)).entered() - }); - // the below is bad. don't do this at home. it probably works thanks to the exact way the system is setup. - // Thanksfully, this doesn't run in production, and hopefully should be possible to remove before we even - // consider doing so. - let data = CALLER_CONTEXT.with(|caller_context| { - unsafe { - *caller_context.get() - } - }); - unsafe { - // Transmute the lifetime of caller so it's possible to put it in a thread-local. - crate::wasmtime_runner::CALLER.with(|runner_caller| *runner_caller.borrow_mut() = std::mem::transmute(caller)); - } - let logic: &mut VMLogic<'_> = unsafe { &mut *(data as *mut VMLogic<'_>) }; - match logic.$func( $( $arg_name as $arg_type, )* ) { - Ok(result) => Ok(result as ($( $returns ),* ) ), - Err(err) => { - Err(ErrorContainer(std::sync::Mutex::new(Some(err))).into()) - } - } - } - - linker.func_wrap(stringify!($mod), stringify!($name), $name).expect("cannot link external"); - }; - } - for_each_available_import!(logic.config, add_import); - } -} +pub(crate) use {call_with_name, for_each_available_import}; -#[cfg(any( - feature = "wasmer0_vm", - feature = "wasmer2_vm", - feature = "near_vm", - feature = "wasmtime_vm" -))] pub(crate) const fn should_trace_host_function(host_function: &str) -> bool { match host_function { _ if str_eq(host_function, "gas") => false, @@ -740,12 +303,6 @@ pub(crate) const fn should_trace_host_function(host_function: &str) -> bool { /// Constant-time string equality, work-around for `"foo" == "bar"` not working /// in const context yet. -#[cfg(any( - feature = "wasmer0_vm", - feature = "wasmer2_vm", - feature = "near_vm", - feature = "wasmtime_vm" -))] const fn str_eq(s1: &str, s2: &str) -> bool { let s1 = s1.as_bytes(); let s2 = s2.as_bytes(); diff --git a/runtime/near-vm-runner/src/near_vm_runner/runner.rs b/runtime/near-vm-runner/src/near_vm_runner/runner.rs index 4cdbce60f19..c6b6e7eeaff 100644 --- a/runtime/near-vm-runner/src/near_vm_runner/runner.rs +++ b/runtime/near-vm-runner/src/near_vm_runner/runner.rs @@ -1,7 +1,6 @@ use super::{NearVmMemory, VM_CONFIG}; use crate::cache::CompiledContractInfo; use crate::errors::ContractPrecompilatonResult; -use crate::imports::near_vm::NearVmImports; use crate::logic::errors::{ CacheError, CompilationError, FunctionCallError, MethodResolveError, VMRunnerError, WasmTrap, }; @@ -25,7 +24,8 @@ use near_vm_engine::universal::{ }; use near_vm_types::{FunctionIndex, InstanceConfig, MemoryType, Pages, WASM_PAGE_SIZE}; use near_vm_vm::{ - Artifact, Instantiatable, LinearMemory, LinearTable, MemoryStyle, TrapCode, VMMemory, + Artifact, ExportFunction, ExportFunctionMetadata, Instantiatable, LinearMemory, LinearTable, + MemoryStyle, Resolver, TrapCode, VMFunction, VMFunctionKind, VMMemory, }; use std::mem::size_of; use std::sync::{Arc, OnceLock}; @@ -610,7 +610,7 @@ impl crate::runner::VM for NearVM { promise_results, method_name, |vmmemory, mut logic, artifact| { - let import = imports::near_vm::build(vmmemory, &mut logic, artifact.engine()); + let import = build_imports(vmmemory, &mut logic, artifact.engine()); let entrypoint = match get_entrypoint_index(&*artifact, method_name) { Ok(index) => index, Err(e) => { @@ -639,6 +639,142 @@ impl crate::runner::VM for NearVM { } } +pub(crate) struct NearVmImports<'engine, 'vmlogic, 'vmlogic_refs> { + pub(crate) memory: VMMemory, + // Note: this same object is also referenced by the `metadata` field! + pub(crate) vmlogic: &'vmlogic mut VMLogic<'vmlogic_refs>, + pub(crate) metadata: Arc, + pub(crate) engine: &'engine UniversalEngine, +} + +trait NearVmType { + type NearVm; + fn to_near_vm(self) -> Self::NearVm; + fn ty() -> near_vm_types::Type; +} +macro_rules! near_vm_types { + ($($native:ty as $near_vm:ty => $type_expr:expr;)*) => { + $(impl NearVmType for $native { + type NearVm = $near_vm; + fn to_near_vm(self) -> $near_vm { + self as _ + } + fn ty() -> near_vm_types::Type { + $type_expr + } + })* + } + } +near_vm_types! { + u32 as i32 => near_vm_types::Type::I32; + u64 as i64 => near_vm_types::Type::I64; +} + +macro_rules! return_ty { + ($return_type: ident = [ ]) => { + type $return_type = (); + fn make_ret() -> () {} + }; + ($return_type: ident = [ $($returns: ident),* ]) => { + #[repr(C)] + struct $return_type($(<$returns as NearVmType>::NearVm),*); + fn make_ret($($returns: $returns),*) -> Ret { Ret($($returns.to_near_vm()),*) } + } + } + +impl<'e, 'l, 'lr> Resolver for NearVmImports<'e, 'l, 'lr> { + fn resolve(&self, _index: u32, module: &str, field: &str) -> Option { + if module == "env" && field == "memory" { + return Some(near_vm_vm::Export::Memory(self.memory.clone())); + } + + macro_rules! add_import { + ( + $mod:ident / $name:ident : $func:ident < + [ $( $arg_name:ident : $arg_type:ident ),* ] + -> [ $( $returns:ident ),* ] + > + ) => { + return_ty!(Ret = [ $($returns),* ]); + + extern "C" fn $name(env: *mut VMLogic<'_>, $( $arg_name: $arg_type ),* ) + -> Ret { + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + const TRACE: bool = $crate::imports::should_trace_host_function(stringify!($name)); + let _span = TRACE.then(|| { + tracing::trace_span!(target: "vm::host_function", stringify!($name)).entered() + }); + + // SAFETY: This code should only be executable within `'vmlogic` + // lifetime and so it is safe to dereference the `env` pointer which is + // known to be derived from a valid `&'vmlogic mut VMLogic<'_>` in the + // first place. + unsafe { (*env).$func( $( $arg_name, )* ) } + })); + // We want to ensure that the only kind of error that host function calls + // return are VMLogicError. This is important because we later attempt to + // downcast the `RuntimeError`s into `VMLogicError`. + let result: Result, _> = result; + #[allow(unused_parens)] + match result { + Ok(Ok(($($returns),*))) => make_ret($($returns),*), + Ok(Err(trap)) => unsafe { + // SAFETY: this can only be called by a WASM contract, so all the + // necessary hooks are known to be in place. + near_vm_vm::raise_user_trap(Box::new(trap)) + }, + Err(e) => unsafe { + // SAFETY: this can only be called by a WASM contract, so all the + // necessary hooks are known to be in place. + near_vm_vm::resume_panic(e) + }, + } + } + // TODO: a phf hashmap would probably work better here. + if module == stringify!($mod) && field == stringify!($name) { + let args = [$(<$arg_type as NearVmType>::ty()),*]; + let rets = [$(<$returns as NearVmType>::ty()),*]; + let signature = near_vm_types::FunctionType::new(&args[..], &rets[..]); + let signature = self.engine.register_signature(signature); + return Some(near_vm_vm::Export::Function(ExportFunction { + vm_function: VMFunction { + address: $name as *const _, + // SAFETY: here we erase the lifetime of the `vmlogic` reference, + // but we believe that the lifetimes on `NearVmImports` enforce + // sufficiently that it isn't possible to call this exported + // function when vmlogic is no loger live. + vmctx: near_vm_vm::VMFunctionEnvironment { + host_env: self.vmlogic as *const _ as *mut _ + }, + signature, + kind: VMFunctionKind::Static, + call_trampoline: None, + instance_ref: None, + }, + metadata: Some(Arc::clone(&self.metadata)), + })); + } + }; + } + imports::for_each_available_import!(self.vmlogic.config, add_import); + return None; + } +} + +pub(crate) fn build_imports<'e, 'a, 'b>( + memory: VMMemory, + logic: &'a mut VMLogic<'b>, + engine: &'e UniversalEngine, +) -> NearVmImports<'e, 'a, 'b> { + let metadata = unsafe { + // SAFETY: the functions here are thread-safe. We ensure that the lifetime of `VMLogic` + // is sufficiently long by tying the lifetime of VMLogic to the return type which + // contains this metadata. + ExportFunctionMetadata::new(logic as *mut _ as *mut _, None, |ptr| ptr, |_| {}) + }; + NearVmImports { memory, vmlogic: logic, metadata: Arc::new(metadata), engine } +} + #[cfg(test)] mod tests { #[test] diff --git a/runtime/near-vm-runner/src/wasmer2_runner.rs b/runtime/near-vm-runner/src/wasmer2_runner.rs index 5b38051147b..d2616366d57 100644 --- a/runtime/near-vm-runner/src/wasmer2_runner.rs +++ b/runtime/near-vm-runner/src/wasmer2_runner.rs @@ -1,6 +1,5 @@ use crate::cache::{CompiledContract, CompiledContractInfo, ContractRuntimeCache}; use crate::errors::ContractPrecompilatonResult; -use crate::imports::wasmer2::Wasmer2Imports; use crate::logic::errors::{ CacheError, CompilationError, FunctionCallError, MethodResolveError, VMRunnerError, WasmTrap, }; @@ -25,7 +24,8 @@ use wasmer_engine_universal::{ }; use wasmer_types::{FunctionIndex, InstanceConfig, MemoryType, Pages, WASM_PAGE_SIZE}; use wasmer_vm::{ - Artifact, Instantiatable, LinearMemory, LinearTable, Memory, MemoryStyle, TrapCode, VMMemory, + Artifact, ExportFunction, ExportFunctionMetadata, Instantiatable, LinearMemory, LinearTable, + Memory, MemoryStyle, Resolver, TrapCode, VMFunction, VMFunctionKind, VMMemory, }; #[derive(Clone)] @@ -608,7 +608,7 @@ impl crate::runner::VM for Wasmer2VM { if let Err(e) = result { return Ok(VMOutcome::abort(logic, e)); } - let import = imports::wasmer2::build(vmmemory, &mut logic, artifact.engine()); + let import = build_imports(vmmemory, &mut logic, artifact.engine()); let entrypoint = match get_entrypoint_index(&*artifact, method_name) { Ok(index) => index, Err(e) => return Ok(VMOutcome::abort_but_nop_outcome_in_old_protocol(logic, e)), @@ -633,7 +633,148 @@ impl crate::runner::VM for Wasmer2VM { } } -#[test] -fn test_memory_like() { - crate::logic::test_utils::test_memory_like(|| Box::new(Wasmer2Memory::new(1, 1).unwrap())); +pub(crate) struct Wasmer2Imports<'engine, 'vmlogic, 'vmlogic_refs> { + pub(crate) memory: VMMemory, + // Note: this same object is also referenced by the `metadata` field! + pub(crate) vmlogic: &'vmlogic mut VMLogic<'vmlogic_refs>, + pub(crate) metadata: Arc, + pub(crate) engine: &'engine UniversalEngine, +} + +trait Wasmer2Type { + type Wasmer; + fn to_wasmer(self) -> Self::Wasmer; + fn ty() -> wasmer_types::Type; +} +macro_rules! wasmer_types { + ($($native:ty as $wasmer:ty => $type_expr:expr;)*) => { + $(impl Wasmer2Type for $native { + type Wasmer = $wasmer; + fn to_wasmer(self) -> $wasmer { + self as _ + } + fn ty() -> wasmer_types::Type { + $type_expr + } + })* + } +} +wasmer_types! { + u32 as i32 => wasmer_types::Type::I32; + u64 as i64 => wasmer_types::Type::I64; +} + +macro_rules! return_ty { + ($return_type: ident = [ ]) => { + type $return_type = (); + fn make_ret() -> () {} + }; + ($return_type: ident = [ $($returns: ident),* ]) => { + #[repr(C)] + struct $return_type($(<$returns as Wasmer2Type>::Wasmer),*); + fn make_ret($($returns: $returns),*) -> Ret { Ret($($returns.to_wasmer()),*) } + } +} + +impl<'e, 'l, 'lr> Resolver for Wasmer2Imports<'e, 'l, 'lr> { + fn resolve(&self, _index: u32, module: &str, field: &str) -> Option { + if module == "env" && field == "memory" { + return Some(wasmer_vm::Export::Memory(self.memory.clone())); + } + + macro_rules! add_import { + ( + $mod:ident / $name:ident : $func:ident < + [ $( $arg_name:ident : $arg_type:ident ),* ] + -> [ $( $returns:ident ),* ] + > + ) => { + return_ty!(Ret = [ $($returns),* ]); + + extern "C" fn $name(env: *mut VMLogic<'_>, $( $arg_name: $arg_type ),* ) + -> Ret { + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + const TRACE: bool = $crate::imports::should_trace_host_function(stringify!($name)); + let _span = TRACE.then(|| { + tracing::trace_span!(target: "vm::host_function", stringify!($name)).entered() + }); + + // SAFETY: This code should only be executable within `'vmlogic` + // lifetime and so it is safe to dereference the `env` pointer which is + // known to be derived from a valid `&'vmlogic mut VMLogic<'_>` in the + // first place. + unsafe { (*env).$func( $( $arg_name, )* ) } + })); + // We want to ensure that the only kind of error that host function calls + // return are VMLogicError. This is important because we later attempt to + // downcast the `RuntimeError`s into `VMLogicError`. + let result: Result, _> = result; + #[allow(unused_parens)] + match result { + Ok(Ok(($($returns),*))) => make_ret($($returns),*), + Ok(Err(trap)) => unsafe { + // SAFETY: this can only be called by a WASM contract, so all the + // necessary hooks are known to be in place. + wasmer_vm::raise_user_trap(Box::new(trap)) + }, + Err(e) => unsafe { + // SAFETY: this can only be called by a WASM contract, so all the + // necessary hooks are known to be in place. + wasmer_vm::resume_panic(e) + }, + } + } + // TODO: a phf hashmap would probably work better here. + if module == stringify!($mod) && field == stringify!($name) { + let args = [$(<$arg_type as Wasmer2Type>::ty()),*]; + let rets = [$(<$returns as Wasmer2Type>::ty()),*]; + let signature = wasmer_types::FunctionTypeRef::new(&args[..], &rets[..]); + let signature = self.engine.register_signature(signature); + return Some(wasmer_vm::Export::Function(ExportFunction { + vm_function: VMFunction { + address: $name as *const _, + // SAFETY: here we erase the lifetime of the `vmlogic` reference, + // but we believe that the lifetimes on `Wasmer2Imports` enforce + // sufficiently that it isn't possible to call this exported + // function when vmlogic is no loger live. + vmctx: wasmer_vm::VMFunctionEnvironment { + host_env: self.vmlogic as *const _ as *mut _ + }, + signature, + kind: VMFunctionKind::Static, + call_trampoline: None, + instance_ref: None, + }, + metadata: Some(Arc::clone(&self.metadata)), + })); + } + }; + } + imports::for_each_available_import!(self.vmlogic.config, add_import); + return None; + } +} + +pub(crate) fn build_imports<'e, 'a, 'b>( + memory: VMMemory, + logic: &'a mut VMLogic<'b>, + engine: &'e UniversalEngine, +) -> Wasmer2Imports<'e, 'a, 'b> { + let metadata = unsafe { + // SAFETY: the functions here are thread-safe. We ensure that the lifetime of `VMLogic` + // is sufficiently long by tying the lifetime of VMLogic to the return type which + // contains this metadata. + ExportFunctionMetadata::new(logic as *mut _ as *mut _, None, |ptr| ptr, |_| {}) + }; + Wasmer2Imports { memory, vmlogic: logic, metadata: Arc::new(metadata), engine } +} + +#[cfg(test)] +mod tests { + #[test] + fn test_memory_like() { + crate::logic::test_utils::test_memory_like(|| { + Box::new(super::Wasmer2Memory::new(1, 1).unwrap()) + }); + } } diff --git a/runtime/near-vm-runner/src/wasmer_runner.rs b/runtime/near-vm-runner/src/wasmer_runner.rs index 0c620f51f54..57e7697c58e 100644 --- a/runtime/near-vm-runner/src/wasmer_runner.rs +++ b/runtime/near-vm-runner/src/wasmer_runner.rs @@ -12,6 +12,7 @@ use crate::{get_contract_cache_key, imports, ContractCode}; use near_parameters::vm::{Config, VMKind}; use near_parameters::RuntimeFeesConfig; use near_primitives_core::hash::CryptoHash; +use std::ffi::c_void; use wasmer_runtime::{ImportObject, Module}; fn check_method(module: &Module, method_name: &str) -> Result<(), FunctionCallError> { @@ -417,7 +418,7 @@ impl crate::runner::VM for Wasmer0VM { return Ok(VMOutcome::abort(logic, e)); } - let import_object = imports::wasmer::build(memory_copy, &mut logic); + let import_object = build_imports(memory_copy, &mut logic); if let Err(e) = check_method(&module, method_name) { return Ok(VMOutcome::abort_but_nop_outcome_in_old_protocol(logic, e)); @@ -442,3 +443,51 @@ impl crate::runner::VM for Wasmer0VM { .map(|_| ContractPrecompilatonResult::ContractCompiled)) } } + +#[derive(Clone, Copy)] +struct ImportReference(pub *mut c_void); +unsafe impl Send for ImportReference {} +unsafe impl Sync for ImportReference {} + +pub(crate) fn build_imports( + memory: wasmer_runtime::memory::Memory, + logic: &mut VMLogic<'_>, +) -> wasmer_runtime::ImportObject { + let raw_ptr = logic as *mut _ as *mut c_void; + let import_reference = ImportReference(raw_ptr); + let mut import_object = wasmer_runtime::ImportObject::new_with_data(move || { + let dtor = (|_: *mut c_void| {}) as fn(*mut c_void); + ({ import_reference }.0, dtor) + }); + + let mut ns_internal = wasmer_runtime_core::import::Namespace::new(); + let mut ns_env = wasmer_runtime_core::import::Namespace::new(); + ns_env.insert("memory", memory); + + macro_rules! add_import { + ( + $mod:ident / $name:ident : $func:ident < [ $( $arg_name:ident : $arg_type:ident ),* ] -> [ $( $returns:ident ),* ] > + ) => { + #[allow(unused_parens)] + fn $name( ctx: &mut wasmer_runtime::Ctx, $( $arg_name: $arg_type ),* ) -> Result<($( $returns ),*), VMLogicError> { + const TRACE: bool = $crate::imports::should_trace_host_function(stringify!($name)); + let _span = TRACE.then(|| { + tracing::trace_span!(target: "vm::host_function", stringify!($name)).entered() + }); + let logic: &mut VMLogic<'_> = unsafe { &mut *(ctx.data as *mut VMLogic<'_>) }; + logic.$func( $( $arg_name, )* ) + } + + match stringify!($mod) { + "env" => ns_env.insert(stringify!($name), wasmer_runtime::func!($name)), + "internal" => ns_internal.insert(stringify!($name), wasmer_runtime::func!($name)), + _ => unimplemented!(), + } + }; + } + imports::for_each_available_import!(logic.config, add_import); + + import_object.register("env", ns_env); + import_object.register("internal", ns_internal); + import_object +} diff --git a/runtime/near-vm-runner/src/wasmtime_runner.rs b/runtime/near-vm-runner/src/wasmtime_runner.rs index 8814eab0c7b..901c5502048 100644 --- a/runtime/near-vm-runner/src/wasmtime_runner.rs +++ b/runtime/near-vm-runner/src/wasmtime_runner.rs @@ -11,7 +11,8 @@ use near_parameters::vm::VMKind; use near_parameters::RuntimeFeesConfig; use near_primitives_core::hash::CryptoHash; use std::borrow::Cow; -use std::cell::RefCell; +use std::cell::{RefCell, UnsafeCell}; +use std::ffi::c_void; use wasmtime::ExternType::Func; use wasmtime::{Engine, Linker, Memory, MemoryType, Module, Store}; @@ -83,7 +84,7 @@ trait IntoVMError { impl IntoVMError for anyhow::Error { fn into_vm_error(self) -> Result { let cause = self.root_cause(); - if let Some(container) = cause.downcast_ref::() { + if let Some(container) = cause.downcast_ref::() { use {VMLogicError as LE, VMRunnerError as RE}; return match container.take() { Some(LE::HostError(h)) => Ok(FunctionCallError::HostError(h)), @@ -206,8 +207,7 @@ impl crate::runner::VM for WasmtimeVM { if let Err(e) = result { return Ok(VMOutcome::abort(logic, e)); } - - imports::wasmtime::link(&mut linker, memory_copy, &store, &mut logic); + link(&mut linker, memory_copy, &store, &mut logic); match module.get_export(method_name) { Some(export) => match export { Func(func_type) => { @@ -263,3 +263,76 @@ impl crate::runner::VM for WasmtimeVM { Ok(Ok(ContractPrecompilatonResult::CacheNotAvailable)) } } + +/// This is a container from which an error can be taken out by value. This is necessary as +/// `anyhow` does not really give any opportunity to grab causes by value and the VM Logic +/// errors end up a couple layers deep in a causal chain. +#[derive(Debug)] +pub(crate) struct ErrorContainer(std::sync::Mutex>); +impl ErrorContainer { + pub(crate) fn take(&self) -> Option { + let mut guard = self.0.lock().unwrap_or_else(|e| e.into_inner()); + guard.take() + } +} +impl std::error::Error for ErrorContainer {} +impl std::fmt::Display for ErrorContainer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("VMLogic error occurred and is now stored in an opaque storage container") + } +} + +thread_local! { + static CALLER_CONTEXT: UnsafeCell<*mut c_void> = const { UnsafeCell::new(core::ptr::null_mut()) }; +} + +fn link<'a, 'b>( + linker: &mut wasmtime::Linker<()>, + memory: wasmtime::Memory, + store: &wasmtime::Store<()>, + logic: &'a mut VMLogic<'b>, +) { + // Unfortunately, due to the Wasmtime implementation we have to do tricks with the + // lifetimes of the logic instance and pass raw pointers here. + // FIXME(nagisa): I believe this is no longer required, we just need to look at this code + // again. + let raw_logic = logic as *mut _ as *mut c_void; + CALLER_CONTEXT.with(|caller_context| unsafe { *caller_context.get() = raw_logic }); + linker.define(store, "env", "memory", memory).expect("cannot define memory"); + + macro_rules! add_import { + ( + $mod:ident / $name:ident : $func:ident < [ $( $arg_name:ident : $arg_type:ident ),* ] -> [ $( $returns:ident ),* ] > + ) => { + #[allow(unused_parens)] + fn $name(caller: wasmtime::Caller<'_, ()>, $( $arg_name: $arg_type ),* ) -> anyhow::Result<($( $returns ),*)> { + const TRACE: bool = imports::should_trace_host_function(stringify!($name)); + let _span = TRACE.then(|| { + tracing::trace_span!(target: "vm::host_function", stringify!($name)).entered() + }); + // the below is bad. don't do this at home. it probably works thanks to the exact way the system is setup. + // Thanksfully, this doesn't run in production, and hopefully should be possible to remove before we even + // consider doing so. + let data = CALLER_CONTEXT.with(|caller_context| { + unsafe { + *caller_context.get() + } + }); + unsafe { + // Transmute the lifetime of caller so it's possible to put it in a thread-local. + crate::wasmtime_runner::CALLER.with(|runner_caller| *runner_caller.borrow_mut() = std::mem::transmute(caller)); + } + let logic: &mut VMLogic<'_> = unsafe { &mut *(data as *mut VMLogic<'_>) }; + match logic.$func( $( $arg_name as $arg_type, )* ) { + Ok(result) => Ok(result as ($( $returns ),* ) ), + Err(err) => { + Err(ErrorContainer(std::sync::Mutex::new(Some(err))).into()) + } + } + } + + linker.func_wrap(stringify!($mod), stringify!($name), $name).expect("cannot link external"); + }; + } + imports::for_each_available_import!(logic.config, add_import); +}