diff --git a/Cargo.lock b/Cargo.lock index 20ab00a5f..0dbd5ef97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1064,9 +1064,9 @@ dependencies = [ "int-enum", "serde", "soroban-env-macros", + "soroban-wasmi", "static_assertions", "stellar-xdr", - "wasmi", ] [[package]] @@ -1103,13 +1103,13 @@ dependencies = [ "soroban-native-sdk-macros", "soroban-synth-wasm", "soroban-test-wasms", + "soroban-wasmi", "static_assertions", "tabwriter", "textplots", "thousands", "tinyvec", "tracking-allocator", - "wasmi", "wasmprinter", ] @@ -1152,6 +1152,27 @@ dependencies = [ name = "soroban-test-wasms" version = "0.0.16" +[[package]] +name = "soroban-wasmi" +version = "0.29.0-soroban" +dependencies = [ + "smallvec", + "soroban-wasmi_core", + "spin", + "wasmi_arena", + "wasmparser-nostd", +] + +[[package]] +name = "soroban-wasmi_core" +version = "0.29.0-soroban" +dependencies = [ + "downcast-rs", + "libm", + "num-traits", + "paste", +] + [[package]] name = "spin" version = "0.9.8" @@ -1542,33 +1563,9 @@ dependencies = [ "leb128", ] -[[package]] -name = "wasmi" -version = "0.29.0" -source = "git+https://github.com/paritytech/wasmi?rev=23d8d8c684255f2d63526baa348ab3eb3d249de0#23d8d8c684255f2d63526baa348ab3eb3d249de0" -dependencies = [ - "smallvec", - "spin", - "wasmi_arena", - "wasmi_core", - "wasmparser-nostd", -] - [[package]] name = "wasmi_arena" version = "0.4.0" -source = "git+https://github.com/paritytech/wasmi?rev=23d8d8c684255f2d63526baa348ab3eb3d249de0#23d8d8c684255f2d63526baa348ab3eb3d249de0" - -[[package]] -name = "wasmi_core" -version = "0.12.0" -source = "git+https://github.com/paritytech/wasmi?rev=23d8d8c684255f2d63526baa348ab3eb3d249de0#23d8d8c684255f2d63526baa348ab3eb3d249de0" -dependencies = [ - "downcast-rs", - "libm", - "num-traits", - "paste", -] [[package]] name = "wasmparser" diff --git a/Cargo.toml b/Cargo.toml index c82bd2fb4..b8c5e679c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,17 +31,16 @@ rev = "b283ec0bcb791610013107b9eab7d7ff07a65db1" default-features = false [workspace.dependencies.wasmi] -package = "wasmi" -version = "0.29.0" -git = "https://github.com/paritytech/wasmi" -rev = "23d8d8c684255f2d63526baa348ab3eb3d249de0" +package = "soroban-wasmi" +# version = "0.16.0-soroban2" +git = "https://github.com/stellar/wasmi" +# rev = "862b32f5" # [patch."https://github.com/stellar/rs-stellar-xdr"] # stellar-xdr = { path = "../rs-stellar-xdr/" } - -# [patch."https://github.com/stellar/wasmi"] -# soroban-wasmi = { path = "../wasmi/wasmi_v1/" } -# soroban-wasmi_core = { path = "../wasmi/core/" } +[patch."https://github.com/stellar/wasmi"] +soroban-wasmi = { path = "../wasmi/crates/wasmi/" } +soroban-wasmi_core = { path = "../wasmi/crates/core/" } [profile.release] codegen-units = 1 diff --git a/soroban-env-host/src/budget.rs b/soroban-env-host/src/budget.rs index 3f0850abc..f47bcb9ab 100644 --- a/soroban-env-host/src/budget.rs +++ b/soroban-env-host/src/budget.rs @@ -151,6 +151,10 @@ impl BudgetDimension { self.limit } + pub fn get_remaining(&self) -> u64 { + self.limit.saturating_sub(self.total_count) + } + pub fn reset(&mut self, limit: u64) { self.limit = limit; self.total_count = 0; @@ -185,6 +189,17 @@ impl BudgetDimension { } } + pub fn apply_fuel(&mut self, amount: u64) -> Result<(), HostError> { + let ty = ContractCostType::WasmInsnExec; + self.counts[ty as usize] = self.counts[ty as usize].saturating_add(amount); + self.total_count = self.total_count.saturating_add(amount); + if self.is_over_budget() { + Err((ScErrorType::Budget, ScErrorCode::ExceededLimit).into()) + } else { + Ok(()) + } + } + // Resets all model parameters to zero (so that we can override and test individual ones later). #[cfg(test)] pub fn reset_models(&mut self) { @@ -450,6 +465,12 @@ impl Budget { self.charge_in_bulk(ty, 1, input) } + pub fn apply_fuel(&self, amount: u64) -> Result<(), HostError> { + self.mut_budget(|mut b| b.cpu_insns.apply_fuel(amount)) + // TODO: ignored mem bytes + // TODO: ignored tracker + } + /// Performs a bulk charge to the budget under the specified [`CostType`]. /// The `iterations` is the batch size. The caller needs to ensure: /// 1. the batched charges have identical costs (having the same @@ -496,14 +517,22 @@ impl Budget { f(&mut self.0.borrow_mut().tracker[ty as usize]) } - pub fn get_cpu_insns_count(&self) -> u64 { + pub fn get_cpu_insns_consumed(&self) -> u64 { self.0.borrow().cpu_insns.get_total_count() } - pub fn get_mem_bytes_count(&self) -> u64 { + pub fn get_mem_bytes_consumed(&self) -> u64 { self.0.borrow().mem_bytes.get_total_count() } + pub fn get_cpu_insns_remaining(&self) -> u64 { + self.0.borrow().cpu_insns.get_remaining() + } + + pub fn get_mem_bytes_remaining(&self) -> u64 { + self.0.borrow().mem_bytes.get_remaining() + } + pub fn reset_default(&self) { *self.0.borrow_mut() = BudgetImpl::default() } diff --git a/soroban-env-host/src/host.rs b/soroban-env-host/src/host.rs index 884353c17..ae0810f28 100644 --- a/soroban-env-host/src/host.rs +++ b/soroban-env-host/src/host.rs @@ -205,13 +205,17 @@ impl Host { /// Helper for mutating the [`Budget`] held in this [`Host`], either to /// allocate it on contract creation or to deplete it on callbacks from /// the VM or host functions. - pub fn with_budget(&self, f: F) -> T + pub(crate) fn with_budget(&self, f: F) -> Result where - F: FnOnce(Budget) -> T, + F: FnOnce(Budget) -> Result, { f(self.0.budget.clone()) } + // pub (crate) fn with_mut_budget(&self, mut f: F) -> Result + // where + // F: FnMut() + pub(crate) fn budget_ref(&self) -> &Budget { &self.0.budget } diff --git a/soroban-env-host/src/host/error.rs b/soroban-env-host/src/host/error.rs index d5eacc859..72a2f9fa6 100644 --- a/soroban-env-host/src/host/error.rs +++ b/soroban-env-host/src/host/error.rs @@ -218,6 +218,15 @@ impl Host { ) } + pub(crate) fn err_wasmi_fuel_metering_disabled(&self) -> HostError { + self.err( + ScErrorType::WasmVm, + ScErrorCode::InternalError, + "wasmi fuel metering is disabled", + &[], + ) + } + /// Given a result carrying some error type that can be converted to an /// [Error] and supports [core::fmt::Debug], calls [Host::error] with the /// error when there's an error, also passing the result of diff --git a/soroban-env-host/src/test/budget_metering.rs b/soroban-env-host/src/test/budget_metering.rs index 99fa57722..614766c9d 100644 --- a/soroban-env-host/src/test/budget_metering.rs +++ b/soroban-env-host/src/test/budget_metering.rs @@ -35,9 +35,10 @@ fn xdr_object_conversion() -> Result<(), HostError> { // we wind up double-counting the conversion of "objects". // Possibly this should be improved in the future. assert_eq!(budget.get_tracker(ContractCostType::ValXdrConv).0, 6); - assert_eq!(budget.get_cpu_insns_count(), 60); - assert_eq!(budget.get_mem_bytes_count(), 6); - }); + assert_eq!(budget.get_cpu_insns_consumed(), 60); + assert_eq!(budget.get_mem_bytes_consumed(), 6); + Ok(()) + })?; Ok(()) } @@ -63,9 +64,10 @@ fn vm_hostfn_invocation() -> Result<(), HostError> { budget.get_tracker(ContractCostType::InvokeHostFunction).0, 2 ); - assert_eq!(budget.get_cpu_insns_count(), 30); - assert_eq!(budget.get_mem_bytes_count(), 3); - }); + assert_eq!(budget.get_cpu_insns_consumed(), 30); + assert_eq!(budget.get_mem_bytes_consumed(), 3); + Ok(()) + })?; Ok(()) } @@ -96,7 +98,8 @@ fn metered_xdr() -> Result<(), HostError> { budget.get_tracker(ContractCostType::ValSer).1, Some(w.len() as u64) ); - }); + Ok(()) + })?; host.metered_from_xdr::(w.as_slice())?; host.with_budget(|budget| { @@ -104,7 +107,8 @@ fn metered_xdr() -> Result<(), HostError> { budget.get_tracker(ContractCostType::ValDeser).1, Some(w.len() as u64) ); - }); + Ok(()) + })?; Ok(()) } @@ -155,7 +159,8 @@ fn map_insert_key_vec_obj() -> Result<(), HostError> { assert_eq!(budget.get_tracker(ContractCostType::VisitObject).0, 4); // upper bound of number of map-accesses, counting both binary-search and point-access. assert_eq!(budget.get_tracker(ContractCostType::MapEntry).0, 5); - }); + Ok(()) + })?; Ok(()) } diff --git a/soroban-env-host/src/test/hostile.rs b/soroban-env-host/src/test/hostile.rs index fc1e41276..0e758f1f2 100644 --- a/soroban-env-host/src/test/hostile.rs +++ b/soroban-env-host/src/test/hostile.rs @@ -89,8 +89,8 @@ fn hostile_objs_traps() -> Result<(), HostError> { let args: ScVec = host.test_scvec::(&[])?; host.set_diagnostic_level(crate::DiagnosticLevel::Debug); - host.with_budget(|b| b.reset_default()); - host.with_budget(|b| b.reset_unlimited_cpu()); + host.with_budget(|b| Ok(b.reset_default()))?; + host.with_budget(|b| Ok(b.reset_unlimited_cpu()))?; // This one should just run out of memory let res = vm.invoke_function(&host, "objs", &args); diff --git a/soroban-env-host/src/test/util.rs b/soroban-env-host/src/test/util.rs index 65cc6509d..3ae1173c1 100644 --- a/soroban-env-host/src/test/util.rs +++ b/soroban-env-host/src/test/util.rs @@ -63,7 +63,9 @@ impl Host { self.with_budget(|budget| { budget.reset_limits(cpu, mem); // something big but finite that we may exceed budget.reset_models(); - }); + Ok(()) + }) + .unwrap(); self } @@ -101,7 +103,9 @@ impl Host { .mem_bytes .get_cost_model_mut(ty) .linear_term = lin_mem as i64; - }); + Ok(()) + }) + .unwrap(); self } diff --git a/soroban-env-host/src/vm.rs b/soroban-env-host/src/vm.rs index f2393278f..429bae51d 100644 --- a/soroban-env-host/src/vm.rs +++ b/soroban-env-host/src/vm.rs @@ -11,7 +11,7 @@ mod dispatch; mod func_info; -use crate::{err, host::Frame, xdr::ContractCostType, HostError}; +use crate::{budget::AsBudget, err, host::Frame, xdr::ContractCostType, HostError, VmCaller}; use std::{cell::RefCell, io::Cursor, rc::Rc}; use super::{xdr::Hash, Host, RawVal, Symbol}; @@ -22,13 +22,18 @@ use soroban_env_common::{ ConversionError, SymbolStr, TryIntoVal, WasmiMarshal, }; -use wasmi::{Engine, Instance, Linker, Memory, Module, Store, Value}; +use wasmi::{ + core::Trap, errors::FuelError, Engine, FuelConsumptionMode, Func, Instance, Linker, Memory, + Module, Store, Value, +}; #[cfg(any(test, feature = "testutils"))] use soroban_env_common::{ xdr::{ScVal, ScVec}, - TryFromVal, VmCaller, + TryFromVal, }; +#[cfg(any(test, feature = "testutils"))] +use wasmi::{Caller, StoreContextMut}; impl wasmi::core::HostError for HostError {} @@ -150,6 +155,8 @@ impl Vm { config.wasm_sign_extension(false); config.floats(false); config.consume_fuel(true); + // TODO: figure out the right fuel consumption mode + config.fuel_consumption_mode(FuelConsumptionMode::Eager); let engine = Engine::new(&config); let module = host.map_err(Module::new(&engine, module_wasm_code))?; @@ -182,11 +189,13 @@ impl Vm { None }; - let store = RefCell::new(store); + // Here we do _not_ supply the store with any fuel. Fuel is supplied + // right before the VM is being run, i.e., before crossing the host->VM + // boundary. Ok(Rc::new(Self { contract_id, module, - store, + store: RefCell::new(store), instance, memory, })) @@ -204,6 +213,130 @@ impl Vm { } } + // atomic transfer the cpu and memory budget from the vm to the host + fn supply_fuel_to_vm(self: &Rc) -> Result<(), HostError> { + // we check the invariant that fuel tank must be clean before we supply + // it with fuel. + assert!( + self.store.borrow().fuel_consumed() == Some(0) + && self.store.borrow().fuel_total() == Some(0) + ); + + let mut store = self.store.borrow_mut(); + let host = store.data(); + let amount = host.as_budget().get_cpu_insns_remaining(); + // TODO: ignore the memory budget for now + store + .add_fuel(amount) + .map_err(|fe| wasmi::Error::Store(fe).into()) + // TODO: we might consider "locking" the host fuel. Fuel can only be owned by one entity + // however, if we do that, the host will not have any fuel left, so any "leaked" + // host operation will result in a trap. + // maybe we need to in some way add an invariant that only one entity can own the fuel. + } + + fn supply_fuel_to_caller(vmcaller: &mut VmCaller) -> Result<(), Trap> { + // this routine can only be called if we are in the VM mode + assert!(vmcaller.0.is_some()); + let caller = vmcaller + .try_mut() + .map_err(|e| Trap::from(HostError::from(e)))?; + // we check the invariant that fuel tank must be clean before we supply + // it with fuel. + assert!(caller.fuel_consumed() == Some(0) && caller.fuel_total() == Some(0)); + let host = caller.data().clone(); + let amount = host.as_budget().get_cpu_insns_remaining(); + // TODO: ignore the memory budget for now + caller + .add_fuel(amount) + .map_err(|_| host.err_wasmi_fuel_metering_disabled().into()) + } + + // atomic transfer the cpu and memory budget from the host to the vm + fn apply_fuel_consumption_to_budget(self: &Rc) -> Result<(), HostError> { + let mut store = self.store.borrow_mut(); + let host = store.data(); + let fuel_consumed: Result = store + .fuel_consumed() + .ok_or(wasmi::Error::Store(FuelError::FuelMeteringDisabled).into()); + host.as_budget().apply_fuel(fuel_consumed?)?; + // we close the tab by clearing the fuel amounts in the VM + store + .reset_fuel() + .map_err(|fe| wasmi::Error::Store(fe).into()) + } + + fn apply_fuel_consumption_from_caller(vmcaller: &mut VmCaller) -> Result<(), Trap> { + // this routine can only be called if we are in the VM mode + assert!(vmcaller.0.is_some()); + let caller = vmcaller + .try_mut() + .map_err(|e| Trap::from(HostError::from(e)))?; + let host = caller.data().clone(); + let fuel_consumed: Result = caller + .fuel_consumed() + .ok_or(host.err_wasmi_fuel_metering_disabled().into()); + host.as_budget() + .apply_fuel(fuel_consumed?) + .map_err(|he| Trap::from(he))?; + // we close the tab by clearing the fuel amounts in the VM + caller + .reset_fuel() + .map_err(|_| host.err_wasmi_fuel_metering_disabled().into()) + } + + // Wrapper for the [`Func`] call, mostly taking care of fuel supply from + // host to the VM and applying VM fuel consumption back to the host budget. + // This is where the host->VM->host boundaries are crossed. + fn wrapped_func_call( + self: &Rc, + host: &Host, + func_sym: &Symbol, + func: &Func, + inputs: &[Value], + ) -> Result { + let mut wasm_ret: [Value; 1] = [Value::I64(0)]; + self.supply_fuel_to_vm()?; + let res = func.call(&mut *self.store.borrow_mut(), inputs, &mut wasm_ret); + self.apply_fuel_consumption_to_budget()?; + + if let Err(e) = res { + // When a call fails with a wasmi::Error::Trap that carries a HostError + // we propagate that HostError as is, rather than producing something new. + + match e { + wasmi::Error::Trap(trap) => { + if let Some(he) = trap.downcast::() { + host.debug_diagnostics( + "VM call trapped with HostError", + &[func_sym.to_raw(), he.error.to_raw()], + )?; + return Err(he); + } + return Err(host.err( + ScErrorType::WasmVm, + ScErrorCode::InternalError, + "VM trapped with HostError but propagation failed", + &[], + )); + } + e => { + return Err(if host.is_debug() { + // With diagnostics on: log as much detail as we can from wasmi. + let msg = format!("VM call failed: {:?}", &e); + host.error(e.into(), &msg, &[func_sym.to_raw()]) + } else { + host.error(e.into(), "VM call failed", &[func_sym.to_raw()]) + }); + } + } + } + Ok( + <_ as WasmiMarshal>::try_marshal_from_value(wasm_ret[0].clone()) + .ok_or(ConversionError)?, + ) + } + pub(crate) fn invoke_function_raw( self: &Rc, host: &Host, @@ -218,7 +351,6 @@ impl Vm { .iter() .map(|i| Value::I64(i.get_payload() as i64)) .collect(); - let mut wasm_ret: [Value; 1] = [Value::I64(0)]; let func_ss: SymbolStr = func_sym.try_into_val(host)?; let ext = match self .instance @@ -246,47 +378,7 @@ impl Vm { Some(e) => e, }; - let res = func.call( - &mut *self.store.borrow_mut(), - wasm_args.as_slice(), - &mut wasm_ret, - ); - - if let Err(e) = res { - // When a call fails with a wasmi::Error::Trap that carries a HostError - // we propagate that HostError as is, rather than producing something new. - - match e { - wasmi::Error::Trap(trap) => { - if let Some(he) = trap.downcast::() { - host.debug_diagnostics( - "VM call trapped with HostError", - &[func_sym.to_raw(), he.error.to_raw()], - )?; - return Err(he); - } - return Err(host.err( - ScErrorType::WasmVm, - ScErrorCode::InternalError, - "VM trapped with HostError but propagation failed", - &[], - )); - } - e => { - return Err(if host.is_debug() { - // With diagnostics on: log as much detail as we can from wasmi. - let msg = format!("VM call failed: {:?}", &e); - host.error(e.into(), &msg, &[func_sym.to_raw()]) - } else { - host.error(e.into(), "VM call failed", &[func_sym.to_raw()]) - }); - } - } - } - Ok( - <_ as WasmiMarshal>::try_marshal_from_value(wasm_ret[0].clone()) - .ok_or(ConversionError)?, - ) + self.wrapped_func_call(host, func_sym, &func, wasm_args.as_slice()) }, ) } @@ -332,9 +424,7 @@ impl Vm { res } - fn module_custom_section(_m: &Module, _name: impl AsRef) -> Option<&[u8]> { - todo!() - /* + fn module_custom_section(m: &Module, name: impl AsRef) -> Option<&[u8]> { m.custom_sections().iter().find_map(|s| { if &*s.name == name.as_ref() { Some(&*s.data) @@ -342,7 +432,6 @@ impl Vm { None } }) - */ } /// Returns the raw bytes content of a named custom section from the WASM @@ -351,20 +440,18 @@ impl Vm { Self::module_custom_section(&self.module, name) } - // Utility function that synthesizes a `VmCaller` configured to point - // to this VM's `Store` and `Instance`, and calls the provided function - // back with it. Mainly used for testing. + /// Utility function that synthesizes a `VmCaller` configured to point + /// to this VM's `Store` and `Instance`, and calls the provided function + /// back with it. Mainly used for testing. #[cfg(any(test, feature = "testutils"))] - pub fn with_vmcaller(&self, _f: F) -> T + pub fn with_vmcaller(&self, f: F) -> T where F: FnOnce(&mut VmCaller) -> T, { - // let store: &mut Store = &mut self.store.borrow_mut(); - // let mut ctx: StoreContextMut = store.into(); - // let caller: Caller = Caller::new(&mut ctx, Some(&self.instance)); - // let mut vmcaller: VmCaller = VmCaller(Some(caller)); - // f(&mut vmcaller) - // TODO: they have made the Caller::new private. Need fork. - todo!() + let store: &mut Store = &mut self.store.borrow_mut(); + let mut ctx: StoreContextMut = store.into(); + let caller: Caller = Caller::new(&mut ctx, Some(&self.instance)); + let mut vmcaller: VmCaller = VmCaller(Some(caller)); + f(&mut vmcaller) } } diff --git a/soroban-env-host/src/vm/dispatch.rs b/soroban-env-host/src/vm/dispatch.rs index 1a800ee8b..ca6f60273 100644 --- a/soroban-env-host/src/vm/dispatch.rs +++ b/soroban-env-host/src/vm/dispatch.rs @@ -1,4 +1,4 @@ -use crate::{xdr::ContractCostType, Host, HostError, VmCaller, VmCallerEnv}; +use crate::{xdr::ContractCostType, Host, HostError, Vm, VmCaller, VmCallerEnv}; use crate::{ AddressObject, BytesObject, Error, I128Object, I256Object, I64Object, MapObject, RawVal, StringObject, Symbol, SymbolObject, U128Object, U256Object, U32Val, U64Object, VecObject, @@ -71,8 +71,13 @@ macro_rules! generate_dispatch_functions { // This does not account for the actual work being done in those functions, // which are accounted for individually at the operation level. let host = caller.data().clone(); - host.charge_budget(ContractCostType::InvokeHostFunction, None)?; let mut vmcaller = VmCaller(Some(caller)); + + // This is where the VM -> Host boundary is crossed. + // We first transfer the budget accounting to the host. + Vm::apply_fuel_consumption_from_caller(&mut vmcaller)?; + + host.charge_budget(ContractCostType::InvokeHostFunction, None)?; // The odd / seemingly-redundant use of `wasmi::Value` here // as intermediates -- rather than just passing RawVals -- // has to do with the fact that some host functions are @@ -83,8 +88,16 @@ macro_rules! generate_dispatch_functions { // conversions to and from both RawVal and i64 / u64 for // wasmi::Value. let res: Result<_, HostError> = host.$fn_id(&mut vmcaller, $(<$type>::try_marshal_from_value(Value::I64($arg)).ok_or(BadSignature)?),*); - let res: Value = match res { - Ok(ok) => ok.marshal_from_self(), + + let res = match res { + Ok(ok) => { + let val: Value = ok.marshal_from_self(); + if let Value::I64(v) = val { + Ok((v,)) + } else { + Err(BadSignature.into()) + } + }, Err(hosterr) => { // We make a new HostError here to capture the escalation event itself. let escalation: HostError = @@ -92,14 +105,14 @@ macro_rules! generate_dispatch_functions { concat!("escalating error to VM trap from failed host function call: ", stringify!($fn_id)), &[]); let trap: Trap = escalation.into(); - return Err(trap) + Err(trap) } }; - if let Value::I64(v) = res { - Ok((v,)) - } else { - Err(BadSignature.into()) - } + + // This is where the Host->VM boundary is crossed. + // We supply the remaining host budget as fuel to the VM. + Vm::supply_fuel_to_caller(&mut vmcaller)?; + res } )* )*