soroban_env_host/vm/
parsed_module.rs

1use crate::{
2    err,
3    host::metered_clone::MeteredContainer,
4    meta,
5    xdr::{
6        ContractCostType, Limited, ReadXdr, ScEnvMetaEntry, ScEnvMetaEntryInterfaceVersion,
7        ScErrorCode, ScErrorType,
8    },
9    Host, HostError, DEFAULT_XDR_RW_LIMITS,
10};
11
12use wasmi::{Engine, Module};
13
14use super::Vm;
15use std::{collections::BTreeSet, io::Cursor, rc::Rc};
16
17#[derive(Debug, Clone)]
18pub enum VersionedContractCodeCostInputs {
19    V0 { wasm_bytes: usize },
20    V1(crate::xdr::ContractCodeCostInputs),
21}
22
23impl VersionedContractCodeCostInputs {
24    pub fn is_v0(&self) -> bool {
25        match self {
26            Self::V0 { .. } => true,
27            Self::V1(_) => false,
28        }
29    }
30    pub fn charge_for_parsing(&self, host: &Host) -> Result<(), HostError> {
31        match self {
32            Self::V0 { wasm_bytes } => {
33                host.charge_budget(ContractCostType::VmInstantiation, Some(*wasm_bytes as u64))?;
34            }
35            Self::V1(inputs) => {
36                host.charge_budget(
37                    ContractCostType::ParseWasmInstructions,
38                    Some(inputs.n_instructions as u64),
39                )?;
40                host.charge_budget(
41                    ContractCostType::ParseWasmFunctions,
42                    Some(inputs.n_functions as u64),
43                )?;
44                host.charge_budget(
45                    ContractCostType::ParseWasmGlobals,
46                    Some(inputs.n_globals as u64),
47                )?;
48                host.charge_budget(
49                    ContractCostType::ParseWasmTableEntries,
50                    Some(inputs.n_table_entries as u64),
51                )?;
52                host.charge_budget(
53                    ContractCostType::ParseWasmTypes,
54                    Some(inputs.n_types as u64),
55                )?;
56                host.charge_budget(
57                    ContractCostType::ParseWasmDataSegments,
58                    Some(inputs.n_data_segments as u64),
59                )?;
60                host.charge_budget(
61                    ContractCostType::ParseWasmElemSegments,
62                    Some(inputs.n_elem_segments as u64),
63                )?;
64                host.charge_budget(
65                    ContractCostType::ParseWasmImports,
66                    Some(inputs.n_imports as u64),
67                )?;
68                host.charge_budget(
69                    ContractCostType::ParseWasmExports,
70                    Some(inputs.n_exports as u64),
71                )?;
72                host.charge_budget(
73                    ContractCostType::ParseWasmDataSegmentBytes,
74                    Some(inputs.n_data_segment_bytes as u64),
75                )?;
76            }
77        }
78        Ok(())
79    }
80    pub fn charge_for_instantiation(&self, _host: &Host) -> Result<(), HostError> {
81        match self {
82            Self::V0 { wasm_bytes } => {
83                // Before soroban supported cached instantiation, the full cost
84                // of parsing-and-instantiation was charged to the
85                // VmInstantiation cost type and we already charged it by the
86                // time we got here, in `charge_for_parsing` above. At-and-after
87                // the protocol that enabled cached instantiation, the
88                // VmInstantiation cost type was repurposed to only cover the
89                // cost of parsing, so we have to charge the "second half" cost
90                // of instantiation separately here.
91                _host.charge_budget(
92                    ContractCostType::VmCachedInstantiation,
93                    Some(*wasm_bytes as u64),
94                )?;
95            }
96            Self::V1(inputs) => {
97                _host.charge_budget(ContractCostType::InstantiateWasmInstructions, None)?;
98                _host.charge_budget(
99                    ContractCostType::InstantiateWasmFunctions,
100                    Some(inputs.n_functions as u64),
101                )?;
102                _host.charge_budget(
103                    ContractCostType::InstantiateWasmGlobals,
104                    Some(inputs.n_globals as u64),
105                )?;
106                _host.charge_budget(
107                    ContractCostType::InstantiateWasmTableEntries,
108                    Some(inputs.n_table_entries as u64),
109                )?;
110                _host.charge_budget(ContractCostType::InstantiateWasmTypes, None)?;
111                _host.charge_budget(
112                    ContractCostType::InstantiateWasmDataSegments,
113                    Some(inputs.n_data_segments as u64),
114                )?;
115                _host.charge_budget(
116                    ContractCostType::InstantiateWasmElemSegments,
117                    Some(inputs.n_elem_segments as u64),
118                )?;
119                _host.charge_budget(
120                    ContractCostType::InstantiateWasmImports,
121                    Some(inputs.n_imports as u64),
122                )?;
123                _host.charge_budget(
124                    ContractCostType::InstantiateWasmExports,
125                    Some(inputs.n_exports as u64),
126                )?;
127                _host.charge_budget(
128                    ContractCostType::InstantiateWasmDataSegmentBytes,
129                    Some(inputs.n_data_segment_bytes as u64),
130                )?;
131            }
132        }
133        Ok(())
134    }
135}
136
137/// A [ParsedModule] contains the parsed [wasmi::Module] for a given Wasm blob,
138/// as well as a protocol number and set of [ContractCodeCostInputs] extracted
139/// from the module when it was parsed.
140pub struct ParsedModule {
141    pub module: Module,
142    pub proto_version: u32,
143    pub cost_inputs: VersionedContractCodeCostInputs,
144}
145
146impl ParsedModule {
147    pub fn new(
148        host: &Host,
149        engine: &Engine,
150        wasm: &[u8],
151        cost_inputs: VersionedContractCodeCostInputs,
152    ) -> Result<Rc<Self>, HostError> {
153        cost_inputs.charge_for_parsing(host)?;
154        let (module, proto_version) = Self::parse_wasm(host, engine, wasm)?;
155        Ok(Rc::new(Self {
156            module,
157            proto_version,
158            cost_inputs,
159        }))
160    }
161
162    pub fn with_import_symbols<T>(
163        &self,
164        host: &Host,
165        callback: impl FnOnce(&BTreeSet<(&str, &str)>) -> Result<T, HostError>,
166    ) -> Result<T, HostError> {
167        // Cap symbols we're willing to import at 10 characters for each of
168        // module and function name. in practice they are all 1-2 chars, but
169        // we'll leave some future-proofing room here. The important point
170        // is to not be introducing a DoS vector.
171        const SYM_LEN_LIMIT: usize = 10;
172        let symbols: BTreeSet<(&str, &str)> = self
173            .module
174            .imports()
175            .filter_map(|i| {
176                if i.ty().func().is_some() {
177                    let mod_str = i.module();
178                    let fn_str = i.name();
179                    if mod_str.len() < SYM_LEN_LIMIT && fn_str.len() < SYM_LEN_LIMIT {
180                        return Some((mod_str, fn_str));
181                    }
182                }
183                None
184            })
185            .collect();
186        // We approximate the cost of `BTreeSet` with the cost of initializng a
187        // `Vec` with the same elements, and we are doing it after the set has
188        // been created. The element count has been limited/charged during the
189        // parsing phase, so there is no DOS factor. We don't charge for
190        // insertion/lookups, since they should be cheap and number of
191        // operations on the set is limited.
192        Vec::<(&str, &str)>::charge_bulk_init_cpy(symbols.len() as u64, host)?;
193        callback(&symbols)
194    }
195
196    pub fn make_linker(&self, host: &Host) -> Result<wasmi::Linker<Host>, HostError> {
197        self.with_import_symbols(host, |symbols| {
198            Host::make_linker(self.module.engine(), symbols)
199        })
200    }
201
202    pub fn new_with_isolated_engine(
203        host: &Host,
204        wasm: &[u8],
205        cost_inputs: VersionedContractCodeCostInputs,
206    ) -> Result<Rc<Self>, HostError> {
207        use crate::budget::AsBudget;
208        let config = crate::vm::get_wasmi_config(host.as_budget())?;
209        let engine = Engine::new(&config);
210        Self::new(host, &engine, wasm, cost_inputs)
211    }
212
213    /// Parse the Wasm blob into a [Module] and its protocol number, checking its interface version
214    fn parse_wasm(host: &Host, engine: &Engine, wasm: &[u8]) -> Result<(Module, u32), HostError> {
215        let module = {
216            let _span0 = tracy_span!("parse module");
217            host.map_err(Module::new(&engine, wasm))?
218        };
219
220        Self::check_max_args(host, &module)?;
221        let interface_version = Self::check_meta_section(host, &module)?;
222        let contract_proto = interface_version.protocol;
223
224        Ok((module, contract_proto))
225    }
226
227    fn check_contract_interface_version(
228        host: &Host,
229        interface_version: &ScEnvMetaEntryInterfaceVersion,
230    ) -> Result<(), HostError> {
231        let want_proto = {
232            let ledger_proto = host.get_ledger_protocol_version()?;
233            let env_proto = meta::INTERFACE_VERSION.protocol;
234            if ledger_proto <= env_proto {
235                // ledger proto should be before or equal to env proto
236                ledger_proto
237            } else {
238                return Err(err!(
239                    host,
240                    (ScErrorType::Context, ScErrorCode::InternalError),
241                    "ledger protocol number is ahead of supported env protocol number",
242                    ledger_proto,
243                    env_proto
244                ));
245            }
246        };
247
248        // Not used when "next" is enabled
249        #[cfg(not(feature = "next"))]
250        let got_pre = interface_version.pre_release;
251
252        let got_proto = interface_version.protocol;
253
254        if got_proto < want_proto {
255            // Old protocols are finalized, we only support contracts
256            // with similarly finalized (zero) prerelease numbers.
257            //
258            // Note that we only enable this check if the "next" feature isn't enabled
259            // because a "next" stellar-core can still run a "curr" test using non-finalized
260            // test Wasms. The "next" feature isn't safe for production and is meant to
261            // simulate the protocol version after the one currently supported in
262            // stellar-core, so bypassing this check for "next" is safe.
263            #[cfg(not(feature = "next"))]
264            if got_pre != 0 {
265                return Err(err!(
266                    host,
267                    (ScErrorType::WasmVm, ScErrorCode::InvalidInput),
268                    "contract pre-release number for old protocol is nonzero",
269                    got_pre
270                ));
271            }
272        } else if got_proto == want_proto {
273            // Relax this check as well for the "next" feature to allow for flexibility while testing.
274            // stellar-core can pass in an older protocol version, in which case the pre-release version
275            // will not match up with the "next" feature (The "next" pre-release version is always 1).
276            #[cfg(not(feature = "next"))]
277            {
278                // Current protocol might have a nonzero prerelease number; we will
279                // allow it only if it matches the current prerelease exactly.
280                let want_pre = meta::INTERFACE_VERSION.pre_release;
281                if want_pre != got_pre {
282                    return Err(err!(
283                        host,
284                        (ScErrorType::WasmVm, ScErrorCode::InvalidInput),
285                        "contract pre-release number for current protocol does not match host",
286                        got_pre,
287                        want_pre
288                    ));
289                }
290            }
291        } else {
292            // Future protocols we don't allow. It might be nice (in the sense
293            // of "allowing uploads of a future-protocol contract that will go
294            // live as soon as the network upgrades to it") but there's a risk
295            // that the "future" protocol semantics baked in to a contract
296            // differ from the final semantics chosen by the network, so to be
297            // conservative we avoid even allowing this.
298            return Err(err!(
299                host,
300                (ScErrorType::WasmVm, ScErrorCode::InvalidInput),
301                "contract protocol number is newer than host",
302                got_proto
303            ));
304        }
305        Ok(())
306    }
307
308    fn module_custom_section(m: &Module, name: impl AsRef<str>) -> Option<&[u8]> {
309        m.custom_sections().iter().find_map(|s| {
310            if &*s.name == name.as_ref() {
311                Some(&*s.data)
312            } else {
313                None
314            }
315        })
316    }
317
318    /// Returns the raw bytes content of a named custom section from the Wasm
319    /// module loaded into the [Vm], or `None` if no such custom section exists.
320    pub fn custom_section(&self, name: impl AsRef<str>) -> Option<&[u8]> {
321        Self::module_custom_section(&self.module, name)
322    }
323
324    fn check_meta_section(
325        host: &Host,
326        m: &Module,
327    ) -> Result<ScEnvMetaEntryInterfaceVersion, HostError> {
328        if let Some(env_meta) = Self::module_custom_section(m, meta::ENV_META_V0_SECTION_NAME) {
329            let mut limits = DEFAULT_XDR_RW_LIMITS;
330            limits.len = env_meta.len();
331            let mut cursor = Limited::new(Cursor::new(env_meta), limits);
332            if let Some(env_meta_entry) = ScEnvMetaEntry::read_xdr_iter(&mut cursor).next() {
333                let ScEnvMetaEntry::ScEnvMetaKindInterfaceVersion(v) =
334                    host.map_err(env_meta_entry)?;
335                Self::check_contract_interface_version(host, &v)?;
336                Ok(v)
337            } else {
338                Err(host.err(
339                    ScErrorType::WasmVm,
340                    ScErrorCode::InvalidInput,
341                    "contract missing environment interface version",
342                    &[],
343                ))
344            }
345        } else {
346            Err(host.err(
347                ScErrorType::WasmVm,
348                ScErrorCode::InvalidInput,
349                "contract missing metadata section",
350                &[],
351            ))
352        }
353    }
354
355    fn check_max_args(host: &Host, m: &Module) -> Result<(), HostError> {
356        for e in m.exports() {
357            match e.ty() {
358                wasmi::ExternType::Func(f) => {
359                    if f.results().len() > Vm::MAX_VM_ARGS {
360                        return Err(err!(
361                            host,
362                            (ScErrorType::WasmVm, ScErrorCode::InvalidInput),
363                            "Too many return values in Wasm export",
364                            f.results().len()
365                        ));
366                    }
367                    if f.params().len() > Vm::MAX_VM_ARGS {
368                        return Err(err!(
369                            host,
370                            (ScErrorType::WasmVm, ScErrorCode::InvalidInput),
371                            "Too many arguments Wasm export",
372                            f.params().len()
373                        ));
374                    }
375                }
376                _ => (),
377            }
378        }
379        Ok(())
380    }
381
382    // Do a second, manual parse of the Wasm blob to extract cost parameters we're
383    // interested in.
384    pub fn extract_refined_contract_cost_inputs(
385        host: &Host,
386        wasm: &[u8],
387    ) -> Result<crate::xdr::ContractCodeCostInputs, HostError> {
388        use wasmparser::{ElementItems, ElementKind, Parser, Payload::*, TableInit};
389
390        if !Parser::is_core_wasm(wasm) {
391            return Err(host.err(
392                ScErrorType::WasmVm,
393                ScErrorCode::InvalidInput,
394                "unsupported non-core wasm module",
395                &[],
396            ));
397        }
398
399        let mut costs = crate::xdr::ContractCodeCostInputs {
400            ext: crate::xdr::ExtensionPoint::V0,
401            n_instructions: 0,
402            n_functions: 0,
403            n_globals: 0,
404            n_table_entries: 0,
405            n_types: 0,
406            n_data_segments: 0,
407            n_elem_segments: 0,
408            n_imports: 0,
409            n_exports: 0,
410            n_data_segment_bytes: 0,
411        };
412
413        let parser = Parser::new(0);
414        let mut elements: u32 = 0;
415        let mut available_memory: u32 = 0;
416        for section in parser.parse_all(wasm) {
417            let section = host.map_err(section)?;
418            match section {
419                // Ignored sections.
420                Version { .. }
421                | DataCountSection { .. }
422                | CustomSection(_)
423                | CodeSectionStart { .. }
424                | End(_) => (),
425
426                // Component-model stuff or other unsupported sections. Error out.
427                StartSection { .. }
428                | ModuleSection { .. }
429                | InstanceSection(_)
430                | CoreTypeSection(_)
431                | ComponentSection { .. }
432                | ComponentInstanceSection(_)
433                | ComponentAliasSection(_)
434                | ComponentTypeSection(_)
435                | ComponentCanonicalSection(_)
436                | ComponentStartSection { .. }
437                | ComponentImportSection(_)
438                | ComponentExportSection(_)
439                | TagSection(_)
440                | UnknownSection { .. } => {
441                    return Err(host.err(
442                        ScErrorType::WasmVm,
443                        ScErrorCode::InvalidInput,
444                        "unsupported wasm section type",
445                        &[],
446                    ))
447                }
448
449                MemorySection(s) => {
450                    for mem in s {
451                        let mem = host.map_err(mem)?;
452                        if mem.memory64 {
453                            return Err(host.err(
454                                ScErrorType::WasmVm,
455                                ScErrorCode::InvalidInput,
456                                "unsupported 64-bit memory",
457                                &[],
458                            ));
459                        }
460                        if mem.shared {
461                            return Err(host.err(
462                                ScErrorType::WasmVm,
463                                ScErrorCode::InvalidInput,
464                                "unsupported shared memory",
465                                &[],
466                            ));
467                        }
468                        if mem
469                            .initial
470                            .saturating_mul(crate::vm::WASM_STD_MEM_PAGE_SIZE_IN_BYTES as u64)
471                            > u32::MAX as u64
472                        {
473                            return Err(host.err(
474                                ScErrorType::WasmVm,
475                                ScErrorCode::InvalidInput,
476                                "unsupported memory size",
477                                &[],
478                            ));
479                        }
480                        available_memory = available_memory.saturating_add(
481                            (mem.initial as u32)
482                                .saturating_mul(crate::vm::WASM_STD_MEM_PAGE_SIZE_IN_BYTES),
483                        );
484                    }
485                }
486
487                TypeSection(s) => costs.n_types = costs.n_types.saturating_add(s.count()),
488                ImportSection(s) => costs.n_imports = costs.n_imports.saturating_add(s.count()),
489                FunctionSection(s) => {
490                    costs.n_functions = costs.n_functions.saturating_add(s.count())
491                }
492                TableSection(s) => {
493                    for table in s {
494                        let table = host.map_err(table)?;
495                        costs.n_table_entries =
496                            costs.n_table_entries.saturating_add(table.ty.initial);
497                        match table.init {
498                            TableInit::RefNull => (),
499                            TableInit::Expr(ref expr) => {
500                                Self::check_const_expr_simple(&host, &expr)?;
501                            }
502                        }
503                    }
504                }
505                GlobalSection(s) => {
506                    costs.n_globals = costs.n_globals.saturating_add(s.count());
507                    for global in s {
508                        let global = host.map_err(global)?;
509                        Self::check_const_expr_simple(&host, &global.init_expr)?;
510                    }
511                }
512                ExportSection(s) => costs.n_exports = costs.n_exports.saturating_add(s.count()),
513                ElementSection(s) => {
514                    costs.n_elem_segments = costs.n_elem_segments.saturating_add(s.count());
515                    for elem in s {
516                        let elem = host.map_err(elem)?;
517                        match elem.kind {
518                            ElementKind::Declared | ElementKind::Passive => (),
519                            ElementKind::Active { offset_expr, .. } => {
520                                Self::check_const_expr_simple(&host, &offset_expr)?
521                            }
522                        }
523                        match elem.items {
524                            ElementItems::Functions(fs) => {
525                                elements = elements.saturating_add(fs.count());
526                            }
527                            ElementItems::Expressions(_, exprs) => {
528                                elements = elements.saturating_add(exprs.count());
529                                for expr in exprs {
530                                    let expr = host.map_err(expr)?;
531                                    Self::check_const_expr_simple(&host, &expr)?;
532                                }
533                            }
534                        }
535                    }
536                }
537                DataSection(s) => {
538                    costs.n_data_segments = costs.n_data_segments.saturating_add(s.count());
539                    for d in s {
540                        let d = host.map_err(d)?;
541                        if d.data.len() > u32::MAX as usize {
542                            return Err(host.err(
543                                ScErrorType::WasmVm,
544                                ScErrorCode::InvalidInput,
545                                "data segment exceeds u32::MAX",
546                                &[],
547                            ));
548                        }
549                        costs.n_data_segment_bytes = costs
550                            .n_data_segment_bytes
551                            .saturating_add(d.data.len() as u32);
552                        match d.kind {
553                            wasmparser::DataKind::Active { offset_expr, .. } => {
554                                Self::check_const_expr_simple(&host, &offset_expr)?
555                            }
556                            wasmparser::DataKind::Passive => (),
557                        }
558                    }
559                }
560                CodeSectionEntry(s) => {
561                    let ops = host.map_err(s.get_operators_reader())?;
562                    for _op in ops {
563                        costs.n_instructions = costs.n_instructions.saturating_add(1);
564                    }
565                }
566            }
567        }
568        if costs.n_data_segment_bytes > available_memory {
569            return Err(err!(
570                host,
571                (ScErrorType::WasmVm, ScErrorCode::InvalidInput),
572                "data segment(s) content exceeds memory size",
573                costs.n_data_segment_bytes,
574                available_memory
575            ));
576        }
577        if elements > costs.n_table_entries {
578            return Err(err!(
579                host,
580                (ScErrorType::WasmVm, ScErrorCode::InvalidInput),
581                "elem segments(s) content exceeds table size",
582                elements,
583                costs.n_table_entries
584            ));
585        }
586        Ok(costs)
587    }
588
589    fn check_const_expr_simple(host: &Host, expr: &wasmparser::ConstExpr) -> Result<(), HostError> {
590        use wasmparser::Operator::*;
591        let mut op = expr.get_operators_reader();
592        while !op.eof() {
593            match host.map_err(op.read())? {
594                I32Const { .. } | I64Const { .. } | RefFunc { .. } | RefNull { .. } | End => (),
595                _ => {
596                    return Err(host.err(
597                        ScErrorType::WasmVm,
598                        ScErrorCode::InvalidInput,
599                        "unsupported complex Wasm constant expression",
600                        &[],
601                    ))
602                }
603            }
604        }
605        Ok(())
606    }
607}