soroban_env_host/
vm.rs

1//! This module primarily provides the [Vm] type and the necessary name-lookup
2//! and runtime-dispatch mechanisms needed to allow WASM modules to call into
3//! the [Env](crate::Env) interface implemented by [Host].
4//!
5//! It also contains helper methods to look up and call into contract functions
6//! in terms of [ScVal] and [Val] arguments.
7//!
8//! The implementation of WASM types and the WASM bytecode interpreter come from
9//! the [wasmi](https://github.com/paritytech/wasmi) project.
10
11mod dispatch;
12mod fuel_refillable;
13mod func_info;
14mod module_cache;
15mod parsed_module;
16
17#[cfg(feature = "bench")]
18pub(crate) use dispatch::dummy0;
19#[cfg(test)]
20pub(crate) use dispatch::protocol_gated_dummy;
21
22use crate::{
23    budget::{get_wasmi_config, AsBudget, Budget},
24    host::{
25        error::TryBorrowOrErr,
26        metered_clone::MeteredContainer,
27        metered_hash::{CountingHasher, MeteredHash},
28    },
29    xdr::{ContractCostType, ContractId, ScErrorCode, ScErrorType},
30    ConversionError, ErrorHandler, Host, HostError, Symbol, SymbolStr, TryIntoVal, Val,
31    WasmiMarshal,
32};
33use std::{cell::RefCell, collections::BTreeSet, rc::Rc, sync::Arc};
34
35use fuel_refillable::FuelRefillable;
36use func_info::HOST_FUNCTIONS;
37
38pub use module_cache::ModuleCache;
39pub use parsed_module::{
40    wasm_module_memory_cost, CompilationContext, ParsedModule, VersionedContractCodeCostInputs,
41};
42
43use crate::VmCaller;
44use wasmi::{Caller, StoreContextMut};
45
46impl wasmi::core::HostError for HostError {}
47
48const WASM_STD_MEM_PAGE_SIZE_IN_BYTES: u32 = 0x10000;
49
50struct VmInstantiationTimer {
51    #[cfg(not(target_family = "wasm"))]
52    host: Host,
53    #[cfg(not(target_family = "wasm"))]
54    start: std::time::Instant,
55}
56impl VmInstantiationTimer {
57    fn new(_host: Host) -> Self {
58        VmInstantiationTimer {
59            #[cfg(not(target_family = "wasm"))]
60            host: _host,
61            #[cfg(not(target_family = "wasm"))]
62            start: std::time::Instant::now(),
63        }
64    }
65}
66#[cfg(not(target_family = "wasm"))]
67impl Drop for VmInstantiationTimer {
68    fn drop(&mut self) {
69        let _ = self.host.as_budget().track_time(
70            ContractCostType::VmInstantiation,
71            self.start.elapsed().as_nanos() as u64,
72        );
73    }
74}
75
76/// A [Vm] is a thin wrapper around an instance of [wasmi::Module]. Multiple
77/// [Vm]s may be held in a single [Host], and each contains a single WASM module
78/// instantiation.
79///
80/// [Vm] rejects modules with either floating point or start functions.
81///
82/// [Vm] is configured to use its [Host] as a source of WASM imports.
83/// Specifically [Host] implements [wasmi::ImportResolver] by resolving all and
84/// only the functions declared in [Env](crate::Env) as imports, if requested by the
85/// WASM module. Any other lookups on any tables other than import functions
86/// will fail.
87pub struct Vm {
88    pub(crate) contract_id: ContractId,
89    #[allow(dead_code)]
90    pub(crate) module: Arc<ParsedModule>,
91    wasmi_store: RefCell<wasmi::Store<Host>>,
92    wasmi_instance: wasmi::Instance,
93    pub(crate) wasmi_memory: Option<wasmi::Memory>,
94}
95
96impl std::hash::Hash for Vm {
97    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
98        self.contract_id.hash(state);
99    }
100}
101
102impl Host {
103    // Make a wasmi linker restricted to _only_ importing the symbols
104    // mentioned in `symbols`.
105    pub(crate) fn make_minimal_wasmi_linker_for_symbols<Ctx: ErrorHandler>(
106        context: &Ctx,
107        engine: &wasmi::Engine,
108        symbols: &BTreeSet<(&str, &str)>,
109    ) -> Result<wasmi::Linker<Host>, HostError> {
110        let mut linker = wasmi::Linker::new(&engine);
111        for hf in HOST_FUNCTIONS {
112            if symbols.contains(&(hf.mod_str, hf.fn_str)) {
113                context.map_err((hf.wrap)(&mut linker).map_err(|le| wasmi::Error::Linker(le)))?;
114            }
115        }
116        Ok(linker)
117    }
118
119    // Make a wasmi linker that imports all the symbols.
120    pub(crate) fn make_maximal_wasmi_linker<Ctx: ErrorHandler>(
121        context: &Ctx,
122        engine: &wasmi::Engine,
123    ) -> Result<wasmi::Linker<Host>, HostError> {
124        let mut linker = wasmi::Linker::new(&engine);
125        for hf in HOST_FUNCTIONS {
126            context.map_err((hf.wrap)(&mut linker).map_err(|le| wasmi::Error::Linker(le)))?;
127        }
128        Ok(linker)
129    }
130}
131
132impl Vm {
133    /// The maximum number of arguments that can be passed to a VM function.
134    pub const MAX_VM_ARGS: usize = 32;
135
136    #[cfg(feature = "testutils")]
137    pub fn get_all_host_functions() -> Vec<(&'static str, &'static str, u32)> {
138        HOST_FUNCTIONS
139            .iter()
140            .map(|hf| (hf.mod_str, hf.fn_str, hf.arity))
141            .collect()
142    }
143
144    #[cfg(feature = "testutils")]
145    #[allow(clippy::type_complexity)]
146    pub fn get_all_host_functions_with_supported_protocol_range(
147    ) -> Vec<(&'static str, &'static str, u32, Option<u32>, Option<u32>)> {
148        HOST_FUNCTIONS
149            .iter()
150            .map(|hf| (hf.mod_str, hf.fn_str, hf.arity, hf.min_proto, hf.max_proto))
151            .collect()
152    }
153
154    /// Instantiate wasmi components specifically (vs. any other future backend).
155    fn instantiate_wasmi(
156        host: &Host,
157        parsed_module: &Arc<ParsedModule>,
158        wasmi_linker: &wasmi::Linker<Host>,
159    ) -> Result<(wasmi::Store<Host>, wasmi::Instance, Option<wasmi::Memory>), HostError> {
160        let _span = tracy_span!("Vm::instantiate_wasmi");
161
162        let wasmi_engine = parsed_module.wasmi_module.engine();
163        let mut store = {
164            let _span = tracy_span!("Vm::instantiate_wasmi - store");
165            wasmi::Store::new(wasmi_engine, host.clone())
166        };
167        parsed_module.cost_inputs.charge_for_instantiation(host)?;
168        store.limiter(|host| host);
169        parsed_module.check_contract_imports_match_host_protocol(host)?;
170        let not_started_instance = {
171            let _span = tracy_span!("Vm::instantiate_wasmi - instantiate");
172            host.map_err(wasmi_linker.instantiate(&mut store, &parsed_module.wasmi_module))?
173        };
174
175        let instance = host.map_err(
176            not_started_instance
177                .ensure_no_start(&mut store)
178                .map_err(|ie| wasmi::Error::Instantiation(ie)),
179        )?;
180
181        let memory = if let Some(ext) = instance.get_export(&mut store, "memory") {
182            ext.into_memory()
183        } else {
184            None
185        };
186        Ok((store, instance, memory))
187    }
188
189    /// Instantiates a VM given more concrete inputs, called by
190    /// [Vm::new] via [Vm::new_with_cost_inputs].
191    pub fn from_parsed_module_and_wasmi_linker(
192        host: &Host,
193        contract_id: ContractId,
194        parsed_module: Arc<ParsedModule>,
195        wasmi_linker: &wasmi::Linker<Host>,
196    ) -> Result<Rc<Self>, HostError> {
197        let _span = tracy_span!("Vm::instantiate");
198
199        // The host really never should have made it past construction on an old
200        // protocol version, but it doesn't hurt to double check here before we
201        // instantiate a VM, which is the place old-protocol replay will
202        // diverge.
203        host.check_ledger_protocol_supported()?;
204
205        let (wasmi_store, wasmi_instance, wasmi_memory) =
206            Self::instantiate_wasmi(host, &parsed_module, wasmi_linker)?;
207
208        // Here we do _not_ supply the store with any fuel. Fuel is supplied
209        // right before the VM is being run, i.e., before crossing the host->VM
210        // boundary.
211        Ok(Rc::new(Self {
212            contract_id,
213            module: parsed_module,
214            wasmi_store: RefCell::new(wasmi_store),
215            wasmi_instance,
216            wasmi_memory,
217        }))
218    }
219
220    /// Constructs a new instance of a [Vm] within the provided [Host],
221    /// establishing a new execution context for a contract identified by
222    /// `contract_id` with Wasm bytecode provided in `module_wasm_code`.
223    ///
224    /// This function performs several steps:
225    ///
226    ///   - Parses and performs Wasm validation on the module.
227    ///   - Checks that the module contains an [meta::INTERFACE_VERSION] that
228    ///     matches the host.
229    ///   - Checks that the module has no floating point code or `start`
230    ///     function, or post-MVP wasm extensions.
231    ///   - Instantiates the module, leaving it ready to accept function
232    ///     invocations.
233    ///   - Looks up and caches its linear memory export named `memory`
234    ///     if it exists.
235    ///
236    /// With the introduction of the granular cost inputs this method
237    /// should only be used for the one-off full parses of the new Wasms
238    /// during the initial upload verification.
239    pub fn new(host: &Host, contract_id: ContractId, wasm: &[u8]) -> Result<Rc<Self>, HostError> {
240        let cost_inputs = VersionedContractCodeCostInputs::V0 {
241            wasm_bytes: wasm.len(),
242        };
243        Self::new_with_cost_inputs(host, contract_id, wasm, cost_inputs)
244    }
245
246    pub(crate) fn new_with_cost_inputs(
247        host: &Host,
248        contract_id: ContractId,
249        wasm: &[u8],
250        cost_inputs: VersionedContractCodeCostInputs,
251    ) -> Result<Rc<Self>, HostError> {
252        let _span = tracy_span!("Vm::new");
253        VmInstantiationTimer::new(host.clone());
254        let parsed_module = ParsedModule::new_with_isolated_engine(host, wasm, cost_inputs)?;
255        let wasmi_linker = parsed_module.make_wasmi_linker(host)?;
256        Self::from_parsed_module_and_wasmi_linker(host, contract_id, parsed_module, &wasmi_linker)
257    }
258
259    pub(crate) fn get_memory(&self, host: &Host) -> Result<wasmi::Memory, HostError> {
260        match self.wasmi_memory {
261            Some(mem) => Ok(mem),
262            None => Err(host.err(
263                ScErrorType::WasmVm,
264                ScErrorCode::MissingValue,
265                "no linear memory named `memory`",
266                &[],
267            )),
268        }
269    }
270
271    // Wrapper for the [`Func`] call which is metered as a component.
272    // Resolves the function entity, and takes care the conversion between and
273    // tranfering of the host budget / VM fuel. This is where the host->VM->host
274    // boundaries are crossed.
275    pub(crate) fn metered_func_call(
276        self: &Rc<Self>,
277        host: &Host,
278        func_sym: &Symbol,
279        inputs: &[wasmi::Value],
280        treat_missing_function_as_noop: bool,
281    ) -> Result<Val, HostError> {
282        host.charge_budget(ContractCostType::InvokeVmFunction, None)?;
283
284        // resolve the function entity to be called
285        let func_ss: SymbolStr = func_sym.try_into_val(host)?;
286        let ext = match self
287            .wasmi_instance
288            .get_export(&*self.wasmi_store.try_borrow_or_err()?, func_ss.as_ref())
289        {
290            None => {
291                if treat_missing_function_as_noop {
292                    return Ok(Val::VOID.into());
293                } else {
294                    return Err(host.err(
295                        ScErrorType::WasmVm,
296                        ScErrorCode::MissingValue,
297                        "trying to invoke non-existent contract function",
298                        &[func_sym.to_val()],
299                    ));
300                }
301            }
302            Some(e) => e,
303        };
304        let func = match ext.into_func() {
305            None => {
306                return Err(host.err(
307                    ScErrorType::WasmVm,
308                    ScErrorCode::UnexpectedType,
309                    "trying to invoke Wasm export that is not a function",
310                    &[func_sym.to_val()],
311                ))
312            }
313            Some(e) => e,
314        };
315
316        if inputs.len() > Vm::MAX_VM_ARGS {
317            return Err(host.err(
318                ScErrorType::WasmVm,
319                ScErrorCode::InvalidInput,
320                "Too many arguments in Wasm invocation",
321                &[func_sym.to_val()],
322            ));
323        }
324
325        // call the function
326        let mut wasm_ret: [wasmi::Value; 1] = [wasmi::Value::I64(0)];
327        self.wasmi_store
328            .try_borrow_mut_or_err()?
329            .add_fuel_to_vm(host)?;
330        // Metering: the `func.call` will trigger `wasmi::Call` (or `CallIndirect`) instruction,
331        // which is technically covered by wasmi fuel metering. So we are double charging a bit
332        // here (by a few 100s cpu insns). It is better to be safe.
333        let res = func.call(
334            &mut *self.wasmi_store.try_borrow_mut_or_err()?,
335            inputs,
336            &mut wasm_ret,
337        );
338        // Due to the way wasmi's fuel metering works (it does `remaining.checked_sub(delta).ok_or(Trap)`),
339        // there may be a small amount of fuel (less than delta -- the fuel cost of that failing
340        // wasmi instruction) remaining when the `OutOfFuel` trap occurs. This is only observable
341        // if the contract traps with `OutOfFuel`, which may appear confusing if they look closely
342        // at the budget amount consumed. So it should be fine.
343        self.wasmi_store
344            .try_borrow_mut_or_err()?
345            .return_fuel_to_host(host)?;
346
347        if let Err(e) = res {
348            use std::borrow::Cow;
349
350            // When a call fails with a wasmi::Error::Trap that carries a HostError
351            // we propagate that HostError as is, rather than producing something new.
352
353            match e {
354                wasmi::Error::Trap(trap) => {
355                    if let Some(code) = trap.trap_code() {
356                        let err = code.into();
357                        let mut msg = Cow::Borrowed("VM call trapped");
358                        host.with_debug_mode(|| {
359                            msg = Cow::Owned(format!("VM call trapped: {:?}", &code));
360                            Ok(())
361                        });
362                        return Err(host.error(err, &msg, &[func_sym.to_val()]));
363                    }
364                    if let Some(he) = trap.downcast::<HostError>() {
365                        host.log_diagnostics(
366                            "VM call trapped with HostError",
367                            &[func_sym.to_val(), he.error.to_val()],
368                        );
369                        return Err(he);
370                    }
371                    return Err(host.err(
372                        ScErrorType::WasmVm,
373                        ScErrorCode::InternalError,
374                        "VM trapped but propagation failed",
375                        &[],
376                    ));
377                }
378                e => {
379                    let mut msg = Cow::Borrowed("VM call failed");
380                    host.with_debug_mode(|| {
381                        msg = Cow::Owned(format!("VM call failed: {:?}", &e));
382                        Ok(())
383                    });
384                    return Err(host.error(e.into(), &msg, &[func_sym.to_val()]));
385                }
386            }
387        }
388        host.relative_to_absolute(
389            Val::try_marshal_from_value(wasm_ret[0].clone()).ok_or(ConversionError)?,
390        )
391    }
392
393    pub(crate) fn invoke_function_raw(
394        self: &Rc<Self>,
395        host: &Host,
396        func_sym: &Symbol,
397        args: &[Val],
398        treat_missing_function_as_noop: bool,
399    ) -> Result<Val, HostError> {
400        let _span = tracy_span!("Vm::invoke_function_raw");
401        Vec::<wasmi::Value>::charge_bulk_init_cpy(args.len() as u64, host.as_budget())?;
402        let wasm_args: Vec<wasmi::Value> = args
403            .iter()
404            .map(|i| host.absolute_to_relative(*i).map(|v| v.marshal_from_self()))
405            .collect::<Result<Vec<wasmi::Value>, HostError>>()?;
406        self.metered_func_call(
407            host,
408            func_sym,
409            wasm_args.as_slice(),
410            treat_missing_function_as_noop,
411        )
412    }
413
414    /// Returns the raw bytes content of a named custom section from the WASM
415    /// module loaded into the [Vm], or `None` if no such custom section exists.
416    pub fn custom_section(&self, name: impl AsRef<str>) -> Option<&[u8]> {
417        self.module.custom_section(name)
418    }
419
420    /// Utility function that synthesizes a `VmCaller<Host>` configured to point
421    /// to this VM's `Store` and `Instance`, and calls the provided function
422    /// back with it. Mainly used for testing.
423    pub(crate) fn with_vmcaller<F, T>(&self, f: F) -> Result<T, HostError>
424    where
425        F: FnOnce(&mut VmCaller<Host>) -> Result<T, HostError>,
426    {
427        let store: &mut wasmi::Store<Host> = &mut *self.wasmi_store.try_borrow_mut_or_err()?;
428        let mut ctx: StoreContextMut<Host> = store.into();
429        let caller: Caller<Host> = Caller::new(&mut ctx, Some(&self.wasmi_instance));
430        let mut vmcaller: VmCaller<Host> = VmCaller(Some(caller));
431        f(&mut vmcaller)
432    }
433
434    #[cfg(feature = "bench")]
435    pub(crate) fn with_caller<F, T>(&self, f: F) -> Result<T, HostError>
436    where
437        F: FnOnce(Caller<Host>) -> Result<T, HostError>,
438    {
439        let store: &mut wasmi::Store<Host> = &mut *self.wasmi_store.try_borrow_mut_or_err()?;
440        let mut ctx: StoreContextMut<Host> = store.into();
441        let caller: Caller<Host> = Caller::new(&mut ctx, Some(&self.wasmi_instance));
442        f(caller)
443    }
444
445    pub(crate) fn memory_hash_and_size(&self, budget: &Budget) -> Result<(u64, usize), HostError> {
446        use std::hash::Hasher;
447        if let Some(mem) = self.wasmi_memory {
448            self.with_vmcaller(|vmcaller| {
449                let mut state = CountingHasher::default();
450                let data = mem.data(vmcaller.try_ref()?);
451                data.metered_hash(&mut state, budget)?;
452                Ok((state.finish(), data.len()))
453            })
454        } else {
455            Ok((0, 0))
456        }
457    }
458
459    // This is pretty weak: we just observe the state that wasmi exposes through
460    // wasm _exports_. There might be tables or globals a wasm doesn't export
461    // but there's no obvious way to observe them.
462    pub(crate) fn exports_hash_and_size(&self, budget: &Budget) -> Result<(u64, usize), HostError> {
463        use std::hash::Hasher;
464        use wasmi::{Extern, StoreContext};
465        self.with_vmcaller(|vmcaller| {
466            let ctx: StoreContext<'_, _> = vmcaller.try_ref()?.into();
467            let mut size: usize = 0;
468            let mut state = CountingHasher::default();
469            for export in self.wasmi_instance.exports(vmcaller.try_ref()?) {
470                size = size.saturating_add(1);
471                export.name().metered_hash(&mut state, budget)?;
472
473                match export.into_extern() {
474                    // Funcs are immutable, memory we hash separately above.
475                    Extern::Func(_) | Extern::Memory(_) => (),
476
477                    Extern::Table(t) => {
478                        let sz = t.size(&ctx);
479                        sz.metered_hash(&mut state, budget)?;
480                        size = size.saturating_add(sz as usize);
481                        for i in 0..sz {
482                            if let Some(elem) = t.get(&ctx, i) {
483                                // This is a slight fudge to avoid having to
484                                // define a ton of additional MeteredHash impls
485                                // for wasmi substructures, since there is a
486                                // bounded size on the string representation of
487                                // a value, we're comfortable going temporarily
488                                // over budget here.
489                                let s = format!("{:?}", elem);
490                                budget.charge(ContractCostType::MemAlloc, Some(s.len() as u64))?;
491                                s.metered_hash(&mut state, budget)?;
492                            }
493                        }
494                    }
495                    Extern::Global(g) => {
496                        let s = format!("{:?}", g.get(&ctx));
497                        budget.charge(ContractCostType::MemAlloc, Some(s.len() as u64))?;
498                        s.metered_hash(&mut state, budget)?;
499                    }
500                }
501            }
502            Ok((state.finish(), size))
503        })
504    }
505}