soroban_env_host_zephyr/vm/
parsed_module.rs

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