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, Hash, ScErrorCode, ScErrorType},
30    ConversionError, Host, HostError, Symbol, SymbolStr, TryIntoVal, Val, WasmiMarshal,
31};
32use std::{cell::RefCell, collections::BTreeSet, rc::Rc};
33
34use fuel_refillable::FuelRefillable;
35use func_info::HOST_FUNCTIONS;
36
37pub use module_cache::ModuleCache;
38pub use parsed_module::{ParsedModule, VersionedContractCodeCostInputs};
39
40use wasmi::{Instance, Linker, Memory, Store, Value};
41
42use crate::VmCaller;
43use wasmi::{Caller, StoreContextMut};
44
45impl wasmi::core::HostError for HostError {}
46
47const WASM_STD_MEM_PAGE_SIZE_IN_BYTES: u32 = 0x10000;
48
49struct VmInstantiationTimer {
50    #[cfg(not(target_family = "wasm"))]
51    host: Host,
52    #[cfg(not(target_family = "wasm"))]
53    start: std::time::Instant,
54}
55impl VmInstantiationTimer {
56    fn new(_host: Host) -> Self {
57        VmInstantiationTimer {
58            #[cfg(not(target_family = "wasm"))]
59            host: _host,
60            #[cfg(not(target_family = "wasm"))]
61            start: std::time::Instant::now(),
62        }
63    }
64}
65#[cfg(not(target_family = "wasm"))]
66impl Drop for VmInstantiationTimer {
67    fn drop(&mut self) {
68        let _ = self.host.as_budget().track_time(
69            ContractCostType::VmInstantiation,
70            self.start.elapsed().as_nanos() as u64,
71        );
72    }
73}
74
75/// A [Vm] is a thin wrapper around an instance of [wasmi::Module]. Multiple
76/// [Vm]s may be held in a single [Host], and each contains a single WASM module
77/// instantiation.
78///
79/// [Vm] rejects modules with either floating point or start functions.
80///
81/// [Vm] is configured to use its [Host] as a source of WASM imports.
82/// Specifically [Host] implements [wasmi::ImportResolver] by resolving all and
83/// only the functions declared in [Env](crate::Env) as imports, if requested by the
84/// WASM module. Any other lookups on any tables other than import functions
85/// will fail.
86pub struct Vm {
87    pub(crate) contract_id: Hash,
88    #[allow(dead_code)]
89    pub(crate) module: Rc<ParsedModule>,
90    store: RefCell<Store<Host>>,
91    instance: Instance,
92    pub(crate) memory: Option<Memory>,
93}
94
95impl std::hash::Hash for Vm {
96    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
97        self.contract_id.hash(state);
98    }
99}
100
101impl Host {
102    pub(crate) fn make_linker(
103        engine: &wasmi::Engine,
104        symbols: &BTreeSet<(&str, &str)>,
105    ) -> Result<Linker<Host>, HostError> {
106        let mut linker = Linker::new(&engine);
107        for hf in HOST_FUNCTIONS {
108            if symbols.contains(&(hf.mod_str, hf.fn_str)) {
109                (hf.wrap)(&mut linker).map_err(|le| wasmi::Error::Linker(le))?;
110            }
111        }
112        Ok(linker)
113    }
114}
115
116// In one very narrow context -- when recording, and with a module cache -- we
117// defer the cost of parsing a module until we pop a control frame.
118// Unfortunately we have to thread this information from the call site to here.
119// See comment below where this type is used.
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub(crate) enum ModuleParseCostMode {
122    Normal,
123    #[cfg(any(test, feature = "recording_mode"))]
124    PossiblyDeferredIfRecording,
125}
126
127impl Vm {
128    /// The maximum number of arguments that can be passed to a VM function.
129    pub const MAX_VM_ARGS: usize = 32;
130
131    #[cfg(feature = "testutils")]
132    pub fn get_all_host_functions() -> Vec<(&'static str, &'static str, u32)> {
133        HOST_FUNCTIONS
134            .iter()
135            .map(|hf| (hf.mod_str, hf.fn_str, hf.arity))
136            .collect()
137    }
138
139    #[cfg(feature = "testutils")]
140    #[allow(clippy::type_complexity)]
141    pub fn get_all_host_functions_with_supported_protocol_range(
142    ) -> Vec<(&'static str, &'static str, u32, Option<u32>, Option<u32>)> {
143        HOST_FUNCTIONS
144            .iter()
145            .map(|hf| (hf.mod_str, hf.fn_str, hf.arity, hf.min_proto, hf.max_proto))
146            .collect()
147    }
148
149    /// Instantiates a VM given the arguments provided in [`Self::new`],
150    /// or [`Self::new_from_module_cache`]
151    fn instantiate(
152        host: &Host,
153        contract_id: Hash,
154        parsed_module: Rc<ParsedModule>,
155        linker: &Linker<Host>,
156    ) -> Result<Rc<Self>, HostError> {
157        let _span = tracy_span!("Vm::instantiate");
158
159        // The host really never should have made it past construction on an old
160        // protocol version, but it doesn't hurt to double check here before we
161        // instantiate a VM, which is the place old-protocol replay will
162        // diverge.
163        host.check_ledger_protocol_supported()?;
164
165        let engine = parsed_module.module.engine();
166        let mut store = Store::new(engine, host.clone());
167
168        parsed_module.cost_inputs.charge_for_instantiation(host)?;
169
170        store.limiter(|host| host);
171
172        {
173            // We perform instantiation-time protocol version gating of
174            // all module-imported symbols here.
175            // Reasons for doing link-time instead of run-time check:
176            // 1. VM instantiation is performed in both contract upload and
177            //    execution, thus any errorous contract will be rejected at
178            //    upload time.
179            // 2. If a contract contains a call to an outdated host function,
180            //    i.e. `contract_protocol > hf.max_supported_protocol`, failing
181            //    early is preferred from resource usage perspective.
182            // 3. If a contract contains a call to an non-existent host
183            //    function, the current (correct) behavior is to return
184            //    `Wasmi::errors::LinkerError::MissingDefinition` error (which gets
185            //    converted to a `(WasmVm, InvalidAction)`). If that host
186            //    function is defined in a later protocol, and we replay that
187            //    contract (in the earlier protocol where it belongs), we need
188            //    to return the same error.
189            let _span0 = tracy_span!("define host functions");
190            let ledger_proto = host.with_ledger_info(|li| Ok(li.protocol_version))?;
191            parsed_module.with_import_symbols(host, |module_symbols| {
192                for hf in HOST_FUNCTIONS {
193                    if !module_symbols.contains(&(hf.mod_str, hf.fn_str)) {
194                        continue;
195                    }
196                    if let Some(min_proto) = hf.min_proto {
197                        if parsed_module.proto_version < min_proto || ledger_proto < min_proto {
198                            return Err(host.err(
199                                ScErrorType::WasmVm,
200                                ScErrorCode::InvalidAction,
201                                "contract calls a host function not yet supported by current protocol",
202                                &[],
203                            ));
204                        }
205                    }
206                    if let Some(max_proto) = hf.max_proto {
207                        if parsed_module.proto_version > max_proto || ledger_proto > max_proto {
208                            return Err(host.err(
209                                ScErrorType::WasmVm,
210                                ScErrorCode::InvalidAction,
211                                "contract calls a host function no longer supported in the current protocol",
212                                &[],
213                            ));
214                        }
215                    }
216                }
217                Ok(())
218            })?;
219        }
220
221        let not_started_instance = {
222            let _span0 = tracy_span!("instantiate module");
223            host.map_err(linker.instantiate(&mut store, &parsed_module.module))?
224        };
225
226        let instance = host.map_err(
227            not_started_instance
228                .ensure_no_start(&mut store)
229                .map_err(|ie| wasmi::Error::Instantiation(ie)),
230        )?;
231
232        let memory = if let Some(ext) = instance.get_export(&mut store, "memory") {
233            ext.into_memory()
234        } else {
235            None
236        };
237
238        // Here we do _not_ supply the store with any fuel. Fuel is supplied
239        // right before the VM is being run, i.e., before crossing the host->VM
240        // boundary.
241        Ok(Rc::new(Self {
242            contract_id,
243            module: parsed_module,
244            store: RefCell::new(store),
245            instance,
246            memory,
247        }))
248    }
249
250    pub fn from_parsed_module(
251        host: &Host,
252        contract_id: Hash,
253        parsed_module: Rc<ParsedModule>,
254    ) -> Result<Rc<Self>, HostError> {
255        let _span = tracy_span!("Vm::from_parsed_module");
256        VmInstantiationTimer::new(host.clone());
257        if let Some(linker) = &*host.try_borrow_linker()? {
258            Self::instantiate(host, contract_id, parsed_module, linker)
259        } else {
260            let linker = parsed_module.make_linker(host)?;
261            Self::instantiate(host, contract_id, parsed_module, &linker)
262        }
263    }
264
265    /// Constructs a new instance of a [Vm] within the provided [Host],
266    /// establishing a new execution context for a contract identified by
267    /// `contract_id` with Wasm bytecode provided in `module_wasm_code`.
268    ///
269    /// This function performs several steps:
270    ///
271    ///   - Parses and performs Wasm validation on the module.
272    ///   - Checks that the module contains an [meta::INTERFACE_VERSION] that
273    ///     matches the host.
274    ///   - Checks that the module has no floating point code or `start`
275    ///     function, or post-MVP wasm extensions.
276    ///   - Instantiates the module, leaving it ready to accept function
277    ///     invocations.
278    ///   - Looks up and caches its linear memory export named `memory`
279    ///     if it exists.
280    ///
281    /// With the introduction of the granular cost inputs this method
282    /// should only be used for the one-off full parses of the new Wasms
283    /// during the initial upload verification.
284    pub fn new(host: &Host, contract_id: Hash, wasm: &[u8]) -> Result<Rc<Self>, HostError> {
285        let cost_inputs = VersionedContractCodeCostInputs::V0 {
286            wasm_bytes: wasm.len(),
287        };
288        Self::new_with_cost_inputs(
289            host,
290            contract_id,
291            wasm,
292            cost_inputs,
293            ModuleParseCostMode::Normal,
294        )
295    }
296
297    pub(crate) fn new_with_cost_inputs(
298        host: &Host,
299        contract_id: Hash,
300        wasm: &[u8],
301        cost_inputs: VersionedContractCodeCostInputs,
302        cost_mode: ModuleParseCostMode,
303    ) -> Result<Rc<Self>, HostError> {
304        let _span = tracy_span!("Vm::new");
305        VmInstantiationTimer::new(host.clone());
306        let parsed_module = Self::parse_module(host, wasm, cost_inputs, cost_mode)?;
307        let linker = parsed_module.make_linker(host)?;
308        Self::instantiate(host, contract_id, parsed_module, &linker)
309    }
310
311    #[cfg(not(any(test, feature = "recording_mode")))]
312    fn parse_module(
313        host: &Host,
314        wasm: &[u8],
315        cost_inputs: VersionedContractCodeCostInputs,
316        _cost_mode: ModuleParseCostMode,
317    ) -> Result<Rc<ParsedModule>, HostError> {
318        ParsedModule::new_with_isolated_engine(host, wasm, cost_inputs)
319    }
320
321    /// This method exists to support [crate::storage::FootprintMode::Recording]
322    /// when running in protocol versions that feature the [ModuleCache].
323    ///
324    /// There are two ways we can get to here:
325    ///
326    ///   1. When we're running in a protocol that doesn't support the
327    ///   [ModuleCache] at all. In this case, we just parse the module and
328    ///   charge for it as normal.
329    ///
330    ///   2. When we're in a protocol that _does_ support the [ModuleCache] but
331    ///   are _also_ in [crate::storage::FootprintMode::Recording] mode and
332    ///   _also_ being instantiated from [Host::call_contract_fn]. Then the
333    ///   [ModuleCache] _did not get built_ during host setup (because we had
334    ///   no footprint yet to buid the cache from), so our caller
335    ///   [Host::call_contract_fn] sees no module cache, and so each call winds
336    ///   up calling us here, reparsing each module as it's called, and then
337    ///   throwing it away.
338    ///
339    /// When we are in case 2, we don't want to charge for all those reparses:
340    /// we want to charge only for the post-parse instantiations _as if_ we had
341    /// had the cache. The cache will actually be added in [Host::pop_context]
342    /// _after_ a top-level recording-mode invocation completes, by reading the
343    /// storage and parsing all the modules in it, in order to charge for
344    /// parsing each used module _once_ and thereby produce a mostly-correct
345    /// total cost.
346    ///
347    /// We still charge the reparses to the shadow budget, to avoid a DoS risk,
348    /// and we still charge the instantiations to the real budget, to behave the
349    /// same as if we had a cache.
350    ///
351    /// Finally, for those scratching their head about the overall structure:
352    /// all of this happens as a result of the "module cache" not being
353    /// especially cache-like (i.e. not being populated lazily, on-access). It's
354    /// populated all at once, up front, because wasmi does not allow adding
355    /// modules to an engine that's currently running.
356    #[cfg(any(test, feature = "recording_mode"))]
357    fn parse_module(
358        host: &Host,
359        wasm: &[u8],
360        cost_inputs: VersionedContractCodeCostInputs,
361        cost_mode: ModuleParseCostMode,
362    ) -> Result<Rc<ParsedModule>, HostError> {
363        if cost_mode == ModuleParseCostMode::PossiblyDeferredIfRecording {
364            if host.in_storage_recording_mode()? {
365                return host.budget_ref().with_observable_shadow_mode(|| {
366                    ParsedModule::new_with_isolated_engine(host, wasm, cost_inputs)
367                });
368            }
369        }
370        ParsedModule::new_with_isolated_engine(host, wasm, cost_inputs)
371    }
372
373    pub(crate) fn get_memory(&self, host: &Host) -> Result<Memory, HostError> {
374        match self.memory {
375            Some(mem) => Ok(mem),
376            None => Err(host.err(
377                ScErrorType::WasmVm,
378                ScErrorCode::MissingValue,
379                "no linear memory named `memory`",
380                &[],
381            )),
382        }
383    }
384
385    // Wrapper for the [`Func`] call which is metered as a component.
386    // Resolves the function entity, and takes care the conversion between and
387    // tranfering of the host budget / VM fuel. This is where the host->VM->host
388    // boundaries are crossed.
389    pub(crate) fn metered_func_call(
390        self: &Rc<Self>,
391        host: &Host,
392        func_sym: &Symbol,
393        inputs: &[Value],
394        treat_missing_function_as_noop: bool,
395    ) -> Result<Val, HostError> {
396        host.charge_budget(ContractCostType::InvokeVmFunction, None)?;
397
398        // resolve the function entity to be called
399        let func_ss: SymbolStr = func_sym.try_into_val(host)?;
400        let ext = match self
401            .instance
402            .get_export(&*self.store.try_borrow_or_err()?, func_ss.as_ref())
403        {
404            None => {
405                if treat_missing_function_as_noop {
406                    return Ok(Val::VOID.into());
407                } else {
408                    return Err(host.err(
409                        ScErrorType::WasmVm,
410                        ScErrorCode::MissingValue,
411                        "trying to invoke non-existent contract function",
412                        &[func_sym.to_val()],
413                    ));
414                }
415            }
416            Some(e) => e,
417        };
418        let func = match ext.into_func() {
419            None => {
420                return Err(host.err(
421                    ScErrorType::WasmVm,
422                    ScErrorCode::UnexpectedType,
423                    "trying to invoke Wasm export that is not a function",
424                    &[func_sym.to_val()],
425                ))
426            }
427            Some(e) => e,
428        };
429
430        if inputs.len() > Vm::MAX_VM_ARGS {
431            return Err(host.err(
432                ScErrorType::WasmVm,
433                ScErrorCode::InvalidInput,
434                "Too many arguments in Wasm invocation",
435                &[func_sym.to_val()],
436            ));
437        }
438
439        // call the function
440        let mut wasm_ret: [Value; 1] = [Value::I64(0)];
441        self.store.try_borrow_mut_or_err()?.add_fuel_to_vm(host)?;
442        // Metering: the `func.call` will trigger `wasmi::Call` (or `CallIndirect`) instruction,
443        // which is technically covered by wasmi fuel metering. So we are double charging a bit
444        // here (by a few 100s cpu insns). It is better to be safe.
445        let res = func.call(
446            &mut *self.store.try_borrow_mut_or_err()?,
447            inputs,
448            &mut wasm_ret,
449        );
450        // Due to the way wasmi's fuel metering works (it does `remaining.checked_sub(delta).ok_or(Trap)`),
451        // there may be a small amount of fuel (less than delta -- the fuel cost of that failing
452        // wasmi instruction) remaining when the `OutOfFuel` trap occurs. This is only observable
453        // if the contract traps with `OutOfFuel`, which may appear confusing if they look closely
454        // at the budget amount consumed. So it should be fine.
455        self.store
456            .try_borrow_mut_or_err()?
457            .return_fuel_to_host(host)?;
458
459        if let Err(e) = res {
460            use std::borrow::Cow;
461
462            // When a call fails with a wasmi::Error::Trap that carries a HostError
463            // we propagate that HostError as is, rather than producing something new.
464
465            match e {
466                wasmi::Error::Trap(trap) => {
467                    if let Some(code) = trap.trap_code() {
468                        let err = code.into();
469                        let mut msg = Cow::Borrowed("VM call trapped");
470                        host.with_debug_mode(|| {
471                            msg = Cow::Owned(format!("VM call trapped: {:?}", &code));
472                            Ok(())
473                        });
474                        return Err(host.error(err, &msg, &[func_sym.to_val()]));
475                    }
476                    if let Some(he) = trap.downcast::<HostError>() {
477                        host.log_diagnostics(
478                            "VM call trapped with HostError",
479                            &[func_sym.to_val(), he.error.to_val()],
480                        );
481                        return Err(he);
482                    }
483                    return Err(host.err(
484                        ScErrorType::WasmVm,
485                        ScErrorCode::InternalError,
486                        "VM trapped but propagation failed",
487                        &[],
488                    ));
489                }
490                e => {
491                    let mut msg = Cow::Borrowed("VM call failed");
492                    host.with_debug_mode(|| {
493                        msg = Cow::Owned(format!("VM call failed: {:?}", &e));
494                        Ok(())
495                    });
496                    return Err(host.error(e.into(), &msg, &[func_sym.to_val()]));
497                }
498            }
499        }
500        host.relative_to_absolute(
501            Val::try_marshal_from_value(wasm_ret[0].clone()).ok_or(ConversionError)?,
502        )
503    }
504
505    pub(crate) fn invoke_function_raw(
506        self: &Rc<Self>,
507        host: &Host,
508        func_sym: &Symbol,
509        args: &[Val],
510        treat_missing_function_as_noop: bool,
511    ) -> Result<Val, HostError> {
512        let _span = tracy_span!("Vm::invoke_function_raw");
513        Vec::<Value>::charge_bulk_init_cpy(args.len() as u64, host.as_budget())?;
514        let wasm_args: Vec<Value> = args
515            .iter()
516            .map(|i| host.absolute_to_relative(*i).map(|v| v.marshal_from_self()))
517            .collect::<Result<Vec<Value>, HostError>>()?;
518        self.metered_func_call(
519            host,
520            func_sym,
521            wasm_args.as_slice(),
522            treat_missing_function_as_noop,
523        )
524    }
525
526    /// Returns the raw bytes content of a named custom section from the WASM
527    /// module loaded into the [Vm], or `None` if no such custom section exists.
528    pub fn custom_section(&self, name: impl AsRef<str>) -> Option<&[u8]> {
529        self.module.custom_section(name)
530    }
531
532    /// Utility function that synthesizes a `VmCaller<Host>` configured to point
533    /// to this VM's `Store` and `Instance`, and calls the provided function
534    /// back with it. Mainly used for testing.
535    pub(crate) fn with_vmcaller<F, T>(&self, f: F) -> Result<T, HostError>
536    where
537        F: FnOnce(&mut VmCaller<Host>) -> Result<T, HostError>,
538    {
539        let store: &mut Store<Host> = &mut *self.store.try_borrow_mut_or_err()?;
540        let mut ctx: StoreContextMut<Host> = store.into();
541        let caller: Caller<Host> = Caller::new(&mut ctx, Some(&self.instance));
542        let mut vmcaller: VmCaller<Host> = VmCaller(Some(caller));
543        f(&mut vmcaller)
544    }
545
546    #[cfg(feature = "bench")]
547    pub(crate) fn with_caller<F, T>(&self, f: F) -> Result<T, HostError>
548    where
549        F: FnOnce(Caller<Host>) -> Result<T, HostError>,
550    {
551        let store: &mut Store<Host> = &mut *self.store.try_borrow_mut_or_err()?;
552        let mut ctx: StoreContextMut<Host> = store.into();
553        let caller: Caller<Host> = Caller::new(&mut ctx, Some(&self.instance));
554        f(caller)
555    }
556
557    pub(crate) fn memory_hash_and_size(&self, budget: &Budget) -> Result<(u64, usize), HostError> {
558        use std::hash::Hasher;
559        if let Some(mem) = self.memory {
560            self.with_vmcaller(|vmcaller| {
561                let mut state = CountingHasher::default();
562                let data = mem.data(vmcaller.try_ref()?);
563                data.metered_hash(&mut state, budget)?;
564                Ok((state.finish(), data.len()))
565            })
566        } else {
567            Ok((0, 0))
568        }
569    }
570
571    // This is pretty weak: we just observe the state that wasmi exposes through
572    // wasm _exports_. There might be tables or globals a wasm doesn't export
573    // but there's no obvious way to observe them.
574    pub(crate) fn exports_hash_and_size(&self, budget: &Budget) -> Result<(u64, usize), HostError> {
575        use std::hash::Hasher;
576        use wasmi::{Extern, StoreContext};
577        self.with_vmcaller(|vmcaller| {
578            let ctx: StoreContext<'_, _> = vmcaller.try_ref()?.into();
579            let mut size: usize = 0;
580            let mut state = CountingHasher::default();
581            for export in self.instance.exports(vmcaller.try_ref()?) {
582                size = size.saturating_add(1);
583                export.name().metered_hash(&mut state, budget)?;
584
585                match export.into_extern() {
586                    // Funcs are immutable, memory we hash separately above.
587                    Extern::Func(_) | Extern::Memory(_) => (),
588
589                    Extern::Table(t) => {
590                        let sz = t.size(&ctx);
591                        sz.metered_hash(&mut state, budget)?;
592                        size = size.saturating_add(sz as usize);
593                        for i in 0..sz {
594                            if let Some(elem) = t.get(&ctx, i) {
595                                // This is a slight fudge to avoid having to
596                                // define a ton of additional MeteredHash impls
597                                // for wasmi substructures, since there is a
598                                // bounded size on the string representation of
599                                // a value, we're comfortable going temporarily
600                                // over budget here.
601                                let s = format!("{:?}", elem);
602                                budget.charge(ContractCostType::MemAlloc, Some(s.len() as u64))?;
603                                s.metered_hash(&mut state, budget)?;
604                            }
605                        }
606                    }
607                    Extern::Global(g) => {
608                        let s = format!("{:?}", g.get(&ctx));
609                        budget.charge(ContractCostType::MemAlloc, Some(s.len() as u64))?;
610                        s.metered_hash(&mut state, budget)?;
611                    }
612                }
613            }
614            Ok((state.finish(), size))
615        })
616    }
617}