1mod 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
75pub 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#[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 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 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 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 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 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 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 #[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 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 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 let mut wasm_ret: [Value; 1] = [Value::I64(0)];
441 self.store.try_borrow_mut_or_err()?.add_fuel_to_vm(host)?;
442 let res = func.call(
446 &mut *self.store.try_borrow_mut_or_err()?,
447 inputs,
448 &mut wasm_ret,
449 );
450 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 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 pub fn custom_section(&self, name: impl AsRef<str>) -> Option<&[u8]> {
529 self.module.custom_section(name)
530 }
531
532 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 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 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 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}