Repository: stellar/rs-soroban-env (rust)

Issue:
[test] over-large or otherwise invalid wasms
A variety of places in wasm allow over-large or otherwise invalid inputs. Many of these will be rejected before they make it to us, and many will be rejected by wasmi itself. But we should have a test that checks we reject as many as we can think of ourselves:

- [x] corrupt metadata custom section
- [x] floating point instructions
- [ ] SIMD instructions, other post-MVP extensions
  - [x] multi-value 
- [x] very large import table that imports the same host functions over and over
- [x] importing nonexistent host functions
- [x] multiple memories
- [x] exporting nonexistent functions
- [ ] invalid table entries
  - [x] FuncRef
  - [ ] ExternRef 
- [x] start functions (these are banned)
- [ ] invalid const-expressions and active table references in elements table
- [ ] invalid const-expressions in active data segment mode
- [x] multiple data-count sections, sections with an invalid count
- [ ] very large versions of every section type:
  - [x] `Custom`
  - [ ] `Type`
  - [x] `Import`
  - [ ] `Function`
  - [ ] `Table`
  - [x] `Memory`
  - [x] `Global`
  - [x] `Export`
  - [x] `Start`
  - [x] `Element`
  - [x] `Code`
  - [x] `Data`
  - [x] `DataCount`


Pull request:
Test bad WASMs
### What

Parts of #1145 

- [x]  corrupt metadata custom section
- [x]  floating point instructions
- [ ]  SIMD instructions,
- [ ]  other post-MVP extensions
    - [x]  multi-value
- [x]  very large import table that imports the same host functions over and over
- [x]  importing nonexistent host functions
- [x]  multiple memories
- [x]  exporting nonexistent functions
- [ ]  invalid table entries
    - [x]  FuncRef
    - [ ]  ExternRef
- [x]  start functions (these are banned)
- [ ]  invalid const-expressions and active table references in elements table
- [ ]  invalid const-expressions in active data segment mode
- [x]  multiple data-count sections, sections with an invalid count
- [ ]  very large versions of every section type:
    - [x]  `Custom` (not valid)
    - [ ]  `Type`
    - [x]  `Import`
    - [ ]  `Function`
    - [ ]  `Table`
    - [x]  `Memory`
    - [x]  `Global`
    - [x]  `Export`
    - [x]  `Start` (not valid)
    - [x]  `Element`
    - [x]  `Code`
    - [x]  `Data`
    - [x]  `DataCount`

Patch:
diff --git a/soroban-env-host/Cargo.toml b/soroban-env-host/Cargo.toml
index 4a2c5550..89c6551d 100644
--- a/soroban-env-host/Cargo.toml
+++ b/soroban-env-host/Cargo.toml
@@ -49,7 +49,7 @@ tabwriter = "=1.3.0"
 thousands = "=0.2.0"
 soroban-env-macros = { workspace = true }
 soroban-test-wasms = { package = "soroban-test-wasms", path = "../soroban-test-wasms" }
-soroban-synth-wasm = { package = "soroban-synth-wasm", path = "../soroban-synth-wasm" }
+soroban-synth-wasm = { package = "soroban-synth-wasm", path = "../soroban-synth-wasm", features = ["adversarial"]}
 soroban-bench-utils = { package = "soroban-bench-utils", path = "../soroban-bench-utils" }
 bytes-lit = "=0.0.5"
 textplots = "=0.8.4"
diff --git a/soroban-env-host/benches/common/cost_types/wasm_insn_exec.rs b/soroban-env-host/benches/common/cost_types/wasm_insn_exec.rs
index c3032de4..079426dd 100644
--- a/soroban-env-host/benches/common/cost_types/wasm_insn_exec.rs
+++ b/soroban-env-host/benches/common/cost_types/wasm_insn_exec.rs
@@ -235,7 +235,7 @@ fn call_indirect(n: u64, _rng: &mut StdRng) -> WasmModule {
     let (mut me, f2) = fe.finish();
     // store in table
     me.define_elems(&[f0, f1, f2]);
-    let ty = me.get_fn_type(Arity(0));
+    let ty = me.get_fn_type(Arity(0), Arity(1));
     // the caller
     fe = me.func(Arity(0), 0);
     for i in 0..n {
diff --git a/soroban-synth-wasm/Cargo.toml b/soroban-synth-wasm/Cargo.toml
index bb2bb15c..bd4cae2e 100644
--- a/soroban-synth-wasm/Cargo.toml
+++ b/soroban-synth-wasm/Cargo.toml
@@ -20,6 +20,7 @@ soroban-env-macros = { workspace = true }
 [features]
 next = ["soroban-env-common/next"]
 testutils = ["soroban-env-common/testutils"]
+adversarial = []
 
 [dev-dependencies]
 expect-test = "=1.4.1"
diff --git a/soroban-synth-wasm/src/func_emitter.rs b/soroban-synth-wasm/src/func_emitter.rs
index 3886583d..14b0228d 100644
--- a/soroban-synth-wasm/src/func_emitter.rs
+++ b/soroban-synth-wasm/src/func_emitter.rs
@@ -108,6 +108,7 @@ impl From<ScError> for Operand {
 pub struct FuncEmitter {
     pub(crate) mod_emit: ModEmitter,
     pub(crate) arity: Arity,
+    pub(crate) ret: Arity,
     pub(crate) func: Function,
 
     /// A vector of [`LocalRef`]s that cover the arguments to the function. If
@@ -143,7 +144,7 @@ impl FuncEmitter {
     /// [`FuncEmitter::args`] and [`FuncEmitter::locals`] of [`LocalRef`]s, each
     /// indexing into the arguments and locals. Access these member vectors
     /// directly to get the corresponding [`LocalRef`]s.
-    pub fn new(mod_emit: ModEmitter, arity: Arity, n_locals: u32) -> Self {
+    pub fn new(mod_emit: ModEmitter, arity: Arity, ret: Arity, n_locals: u32) -> Self {
         let func = Function::new([(n_locals, ValType::I64)]);
         let args = (0..arity.0).map(|n| (LocalRef(n), None)).collect();
         let locals = (arity.0..arity.0 + n_locals)
@@ -152,6 +153,7 @@ impl FuncEmitter {
         Self {
             mod_emit,
             arity,
+            ret,
             func,
             args,
             locals,
@@ -250,15 +252,6 @@ impl FuncEmitter {
         self.insn(&insn)
     }
 
-    /// Emit an [`Instruction::I64Const`]
-    pub fn i64_const(&mut self, i: i64) -> &mut Self {
-        self.insn(&Instruction::I64Const(i))
-    }
-    /// Emit an [`Instruction::I32Const`]
-    pub fn i32_const(&mut self, i: i32) -> &mut Self {
-        self.insn(&Instruction::I32Const(i))
-    }
-
     /// Emit an [`Instruction::If`], call `t(self)`, then emit
     /// an [`Instruction::End`].
     pub fn if_then<THEN>(&mut self, t: THEN) -> &mut Self
@@ -335,7 +328,7 @@ impl FuncEmitter {
     /// function.
     pub fn finish(mut self) -> (ModEmitter, FuncRef) {
         self.insn(&Instruction::End);
-        let fid = self.mod_emit.define_func(self.arity, &self.func);
+        let fid = self.mod_emit.define_func(self.arity, self.ret, &self.func);
         (self.mod_emit, fid)
     }
 
@@ -389,7 +382,28 @@ variable_insn!(
     (global_set, GlobalSet, GlobalRef)
 );
 
-macro_rules! i64_mem_insn {
+macro_rules! consts {
+    ( $(($func_name: ident, $insn: ident, $ty: ty)),* )
+    =>
+    {
+        impl FuncEmitter {
+        $(
+            pub fn $func_name(&mut self, i: $ty,
+            ) -> &mut Self {
+                self.insn(&Instruction::$insn(i))
+            }
+        )*
+        }
+    }
+}
+consts!(
+    (i64_const, I64Const, i64),
+    (i32_const, I32Const, i32),
+    // forbidden instructions
+    (f64_const, F64Const, f64)
+);
+
+macro_rules! store_load {
     ( $(($func_name: ident, $insn: ident)),* )
     =>
     {
@@ -408,7 +422,7 @@ macro_rules! i64_mem_insn {
         }
     }
 }
-i64_mem_insn!(
+store_load!(
     (i64_store, I64Store),
     (i64_load, I64Load),
     (i64_load8_s, I64Load8S),
@@ -416,7 +430,10 @@ i64_mem_insn!(
     (i64_load32_s, I64Load32S),
     (i64_store8, I64Store8),
     (i64_store16, I64Store16),
-    (i64_store32, I64Store32)
+    (i64_store32, I64Store32),
+    (f64_load, F64Load),
+    // forbidden instructions
+    (f64_store, F64Store)
 );
 
 macro_rules! numeric_insn {
@@ -464,5 +481,10 @@ numeric_insn!(
     (i64_rotl, I64Rotl),
     (i64_rotr, I64Rotr),
     // post-MVP instructions
-    (i64_extend32s, I64Extend32S)
+    (i64_extend32s, I64Extend32S),
+    // forbidden instructions
+    (f64_add, F64Add),
+    (f64_sub, F64Sub),
+    (f64_mul, F64Mul),
+    (f64_div, F64Div)
 );
diff --git a/soroban-synth-wasm/src/mod_emitter.rs b/soroban-synth-wasm/src/mod_emitter.rs
index 56ccaeda..088154b8 100644
--- a/soroban-synth-wasm/src/mod_emitter.rs
+++ b/soroban-synth-wasm/src/mod_emitter.rs
@@ -1,9 +1,12 @@
 use crate::FuncEmitter;
 use std::{borrow::Cow, collections::BTreeMap};
+#[cfg(feature = "adversarial")]
+use wasm_encoder::StartSection;
 use wasm_encoder::{
-    CodeSection, ConstExpr, CustomSection, DataSection, ElementSection, Elements, EntityType,
-    ExportKind, ExportSection, Function, FunctionSection, GlobalSection, GlobalType, ImportSection,
-    MemorySection, MemoryType, Module, RefType, TableSection, TableType, TypeSection, ValType,
+    CodeSection, ConstExpr, CustomSection, DataCountSection, DataSection, ElementSection, Elements,
+    EntityType, ExportKind, ExportSection, Function, FunctionSection, GlobalSection, GlobalType,
+    ImportSection, MemorySection, MemoryType, Module, RefType, TableSection, TableType,
+    TypeSection, ValType,
 };
 
 /// Wrapper for a u32 that defines the arity of a function -- that is, the number of
@@ -49,55 +52,63 @@ pub struct ModEmitter {
     memories: MemorySection,
     globals: GlobalSection,
     exports: ExportSection,
+    #[cfg(feature = "adversarial")]
+    start: Option<StartSection>,
     elements: ElementSection,
     codes: CodeSection,
     data: DataSection,
+    // The data count section is used to simplify single-pass validation. It is optional.
+    data_count: Option<DataCountSection>,
 
-    type_refs: BTreeMap<Arity, TypeRef>,
+    // key is (args arity, return arity)
+    type_refs: BTreeMap<(Arity, Arity), TypeRef>,
+    // import functions cannot have return arity != 1
     import_refs: BTreeMap<(String, String, Arity), FuncRef>,
 }
 
+impl Default for ModEmitter {
+    fn default() -> Self {
+        let mut me = Self::new();
+        me.custom_section(
+            soroban_env_common::meta::ENV_META_V0_SECTION_NAME,
+            &soroban_env_common::meta::XDR,
+        );
+        me.table(RefType::FUNCREF, 128, None);
+        me.memory(1, None, false, false);
+        me.global(ValType::I64, true, &ConstExpr::i64_const(42));
+        me.export("memory", wasm_encoder::ExportKind::Memory, 0);
+        me
+    }
+}
+
 impl ModEmitter {
     pub fn from_configs(mem_pages: u32, elem_count: u32) -> Self {
-        let mut module = Module::new();
-
-        let metasection = CustomSection {
-            name: Cow::Borrowed(soroban_env_common::meta::ENV_META_V0_SECTION_NAME),
-            data: Cow::Borrowed(&soroban_env_common::meta::XDR),
-        };
-        module.section(&metasection);
+        let mut me = Self::new();
+        me.custom_section(
+            soroban_env_common::meta::ENV_META_V0_SECTION_NAME,
+            &soroban_env_common::meta::XDR,
+        );
+        me.table(RefType::FUNCREF, elem_count, None);
+        me.memory(mem_pages as u64, None, false, false);
+        me.global(ValType::I64, true, &ConstExpr::i64_const(42));
+        me.export("memory", wasm_encoder::ExportKind::Memory, 0);
+        me
+    }
 
+    pub fn new() -> Self {
+        let module = Module::new();
         let types = TypeSection::new();
         let imports = ImportSection::new();
         let funcs = FunctionSection::new();
-        let mut tables = TableSection::new();
-        tables.table(TableType {
-            element_type: RefType::FUNCREF,
-            minimum: elem_count,
-            maximum: None,
-        });
-        let mut memories = MemorySection::new();
-        memories.memory(MemoryType {
-            minimum: mem_pages as u64,
-            maximum: None,
-            memory64: false,
-            shared: false,
-        });
-        let mut globals = GlobalSection::new();
-        globals.global(
-            GlobalType {
-                val_type: ValType::I64,
-                mutable: true,
-            },
-            &ConstExpr::i64_const(42),
-        );
-        let mut exports = ExportSection::new();
-        exports.export("memory", wasm_encoder::ExportKind::Memory, 0);
+        let tables = TableSection::new();
+        let memories = MemorySection::new();
+        let globals = GlobalSection::new();
+        let exports = ExportSection::new();
         let elements = ElementSection::new();
         let codes = CodeSection::new();
         let data = DataSection::new();
-        let typerefs = BTreeMap::new();
-        let importrefs = BTreeMap::new();
+        let type_refs = BTreeMap::new();
+        let import_refs = BTreeMap::new();
         Self {
             module,
             types,
@@ -107,35 +118,109 @@ impl ModEmitter {
             memories,
             globals,
             exports,
+            #[cfg(feature = "adversarial")]
+            start: None,
             elements,
             codes,
             data,
-            type_refs: typerefs,
-            import_refs: importrefs,
+            data_count: None,
+            type_refs,
+            import_refs,
         }
     }
 
+    pub fn custom_section(&mut self, name: &str, data: &[u8]) -> &mut Self {
+        self.module.section(&CustomSection {
+            name: Cow::Borrowed(name),
+            data: Cow::Borrowed(data),
+        });
+        self
+    }
+
+    pub fn table(
+        &mut self,
+        element_type: RefType,
+        minimum: u32,
+        maximum: Option<u32>,
+    ) -> &mut Self {
+        self.tables.table(TableType {
+            element_type,
+            minimum,
+            maximum,
+        });
+        self
+    }
+
+    pub fn memory(
+        &mut self,
+        minimum: u64,
+        maximum: Option<u64>,
+        memory64: bool,
+        shared: bool,
+    ) -> &mut Self {
+        self.memories.memory(MemoryType {
+            minimum,
+            maximum,
+            memory64,
+            shared,
+        });
+        self
+    }
+
+    pub fn global(&mut self, val_type: ValType, mutable: bool, init_expr: &ConstExpr) -> &mut Self {
+        self.globals
+            .global(GlobalType { val_type, mutable }, init_expr);
+        self
+    }
+
+    pub fn export(&mut self, name: &str, kind: ExportKind, index: u32) -> &mut Self {
+        self.exports.export(name, kind, index);
+        self
+    }
+
+    #[cfg(feature = "adversarial")]
+    pub fn start(&mut self, fid: FuncRef) -> &mut Self {
+        self.start = Some(StartSection {
+            function_index: fid.0,
+        });
+        self
+    }
+
+    pub fn data_count(&mut self, count: u32) -> &mut Self {
+        self.data_count = Some(DataCountSection { count });
+        self
+    }
+
     /// Create a new [`FuncEmitter`] with the given [`Arity`] and locals count.
     /// Transfers ownership of `self` to the [`FuncEmitter`], which can be
     /// recovered by calling [`FuncEmitter::finish`].
     pub fn func(self, arity: Arity, n_locals: u32) -> FuncEmitter {
-        FuncEmitter::new(self, arity, n_locals)
+        FuncEmitter::new(self, arity, Arity(1), n_locals)
     }
 
-    /// Return the unique [`TypeRef`] for a function with a given [`Arity`],
-    /// creating such a type in the `type` section of the module if such a type
-    /// does not already exist.
+    #[cfg(feature = "adversarial")]
+    pub fn func_with_arity_and_ret(self, arity: Arity, ret: Arity, n_locals: u32) -> FuncEmitter {
+        FuncEmitter::new(self, arity, ret, n_locals)
+    }
+
+    /// Return the unique [`TypeRef`] for a function with a given args [`Arity`]
+    /// and return [`Arity`], creating such a type in the `type` section of the
+    /// module if such a type does not already exist.
     #[allow(clippy::map_entry)]
-    pub fn get_fn_type(&mut self, arity: Arity) -> TypeRef {
-        if self.type_refs.contains_key(&arity) {
-            self.type_refs[&arity]
+    pub fn get_fn_type(&mut self, arity: Arity, ret: Arity) -> TypeRef {
+        let key = (arity, ret);
+        if self.type_refs.contains_key(&key) {
+            self.type_refs[&key]
         } else {
             let params: Vec<_> = std::iter::repeat(ValType::I64)
-                .take(arity.0 as usize)
+                .take(key.0 .0 as usize)
+                .collect();
+            let rets: Vec<_> = std::iter::repeat(ValType::I64)
+                .take(key.1 .0 as usize)
                 .collect();
             let ty_id = TypeRef(self.types.len());
-            self.types.function(params, vec![ValType::I64]);
-            self.type_refs.insert(arity, ty_id);
+            self.types.function(params, rets);
+            self.type_refs.insert(key, ty_id);
             ty_id
         }
     }
@@ -154,7 +239,8 @@ impl ModEmitter {
             self.import_refs[&key]
         } else {
             let import_id = FuncRef(self.imports.len());
-            let ty_id = self.get_fn_type(arity);
+            // import func must have return arity == 1
+            let ty_id = self.get_fn_type(arity, Arity(1));
             self.imports
                 .import(module, fname, EntityType::Function(ty_id.0));
             self.import_refs.insert(key, import_id);
@@ -162,11 +248,21 @@ impl ModEmitter {
         }
     }
 
+    #[cfg(feature = "adversarial")]
+    pub fn import_func_no_check(&mut self, module: &str, fname: &str, arity: Arity) -> FuncRef {
+        let import_id = FuncRef(self.imports.len());
+        // import func must have return arity == 1
+        let ty_id = self.get_fn_type(arity, Arity(1));
+        self.imports
+            .import(module, fname, EntityType::Function(ty_id.0));
+        import_id
+    }
+
     /// Define a function in the module with a given arity, adding its code to
     /// the `code` section of the module and declaring it in the `function`
     /// section of the module, and returning a new [`FuncRef`] denoting it.
-    pub fn define_func(&mut self, arity: Arity, func: &Function) -> FuncRef {
-        let ty = self.get_fn_type(arity);
+    pub fn define_func(&mut self, arity: Arity, ret: Arity, func: &Function) -> FuncRef {
+        let ty = self.get_fn_type(arity, ret);
         assert!(self.funcs.len() == self.codes.len());
         let fid = self.imports.len() + self.funcs.len();
         self.funcs.function(ty.0);
@@ -191,13 +287,7 @@ impl ModEmitter {
 
     pub fn define_global_i64(&mut self, val: i64, mutable: bool, export: Option<&str>) {
         let idx = self.globals.len();
-        self.globals.global(
-            GlobalType {
-                val_type: ValType::I64,
-                mutable,
-            },
-            &ConstExpr::i64_const(val),
-        );
+        self.global(ValType::I64, mutable, &ConstExpr::i64_const(val));
         if let Some(name) = export {
             self.exports.export(name, ExportKind::Global, idx);
         }
@@ -237,6 +327,9 @@ impl ModEmitter {
         if !self.elements.is_empty() {
             self.module.section(&self.elements);
         }
+        if let Some(data_count) = self.data_count {
+            self.module.section(&data_count);
+        }
         if !self.codes.is_empty() {
             self.module.section(&self.codes);
         }
@@ -249,63 +342,47 @@ impl ModEmitter {
             Err(ty) => panic!("invalid WASM module: {:?}", ty.message()),
         }
     }
-}
-
-impl Default for ModEmitter {
-    fn default() -> Self {
-        let mut module = Module::new();
-
-        let metasection = CustomSection {
-            name: Cow::Borrowed(soroban_env_common::meta::ENV_META_V0_SECTION_NAME),
-            data: Cow::Borrowed(&soroban_env_common::meta::XDR),
-        };
-        module.section(&metasection);
 
-        let types = TypeSection::new();
-        let imports = ImportSection::new();
-        let funcs = FunctionSection::new();
-        let mut tables = TableSection::new();
-        tables.table(TableType {
-            element_type: RefType::FUNCREF,
-            minimum: 128,
-            maximum: None,
-        });
-        let mut memories = MemorySection::new();
-        memories.memory(MemoryType {
-            minimum: 1,
-            maximum: None,
-            memory64: false,
-            shared: false,
-        });
-        let mut globals = GlobalSection::new();
-        globals.global(
-            GlobalType {
-                val_type: ValType::I64,
-                mutable: true,
-            },
-            &ConstExpr::i64_const(42),
-        );
-        let mut exports = ExportSection::new();
-        exports.export("memory", wasm_encoder::ExportKind::Memory, 0);
-        let elements = ElementSection::new();
-        let codes = CodeSection::new();
-        let data = DataSection::new();
-        let typerefs = BTreeMap::new();
-        let importrefs = BTreeMap::new();
-        Self {
-            module,
-            types,
-            imports,
-            funcs,
-            tables,
-            memories,
-            globals,
-            exports,
-            elements,
-            codes,
-            data,
-            type_refs: typerefs,
-            import_refs: importrefs,
+    #[cfg(feature = "adversarial")]
+    pub fn finish_no_validate(mut self) -> Vec<u8> {
+        // NB: these sections must be emitted in this order, by spec.
+        if !self.types.is_empty() {
+            self.module.section(&self.types);
+        }
+        if !self.imports.is_empty() {
+            self.module.section(&self.imports);
+        }
+        if !self.funcs.is_empty() {
+            self.module.section(&self.funcs);
         }
+        if !self.tables.is_empty() {
+            self.module.section(&self.tables);
+        }
+        if !self.memories.is_empty() {
+            self.module.section(&self.memories);
+        }
+        if !self.globals.is_empty() {
+            self.module.section(&self.globals);
+        }
+        if !self.exports.is_empty() {
+            self.module.section(&self.exports);
+        }
+        if let Some(start) = self.start {
+            self.module.section(&start);
+        }
+        if !self.elements.is_empty() {
+            self.module.section(&self.elements);
+        }
+        if let Some(data_count) = self.data_count {
+            self.module.section(&data_count);
+        }
+        if !self.codes.is_empty() {
+            self.module.section(&self.codes);
+        }
+        if !self.data.is_empty() {
+            self.module.section(&self.data);
+        }
+
+        self.module.finish()
     }
 }


Test patch:
diff --git a/soroban-env-host/src/test/hostile.rs b/soroban-env-host/src/test/hostile.rs
index f42c6d1a..3f8411e7 100644
--- a/soroban-env-host/src/test/hostile.rs
+++ b/soroban-env-host/src/test/hostile.rs
@@ -4,9 +4,13 @@ use soroban_test_wasms::HOSTILE;
 use crate::{
     budget::{AsBudget, Budget},
     host_object::HostVec,
+    meta,
     storage::Storage,
-    testutils::wasm as wasm_util,
-    xdr::{AccountId, ContractCostType, PublicKey, ScErrorCode, ScErrorType, Uint256},
+    testutils::{generate_account_id, generate_bytes_array, wasm as wasm_util},
+    xdr::{
+        AccountId, ContractCostType, Limited, Limits, PublicKey, ScEnvMetaEntry, ScErrorCode,
+        ScErrorType, Uint256, WriteXdr,
+    },
     DiagnosticLevel, Env, EnvBase, Error, Host, HostError, Symbol, SymbolSmall, Tag, Val,
     VecObject,
 };
@@ -739,3 +743,409 @@ fn test_integer_overflow() -> Result<(), HostError> {
     ));
     Ok(())
 }
+
+#[test]
+fn test_corrupt_custom_section() -> Result<(), HostError> {
+    // let wasm = wasm_util::wasm_module_with_custom_section("custom", vec![24; 7].as_slice());
+    let host = Host::test_host_with_recording_footprint();
+    host.enable_debug()?;
+    host.as_budget().reset_unlimited()?;
+
+    // empty custom section
+    let res = host.register_test_contract_wasm_from_source_account(
+        &wasm_util::empty_wasm_module().as_slice(),
+        generate_account_id(&host),
+        generate_bytes_array(&host),
+    );
+    assert!(HostError::result_matches_err(
+        res,
+        (ScErrorType::WasmVm, ScErrorCode::InvalidInput)
+    ));
+
+    // some random custom section
+    let res = host.register_test_contract_wasm_from_source_account(
+        wasm_util::wasm_module_with_custom_section("custom", vec![24; 7].as_slice()).as_slice(),
+        generate_account_id(&host),
+        generate_bytes_array(&host),
+    );
+    assert!(HostError::result_matches_err(
+        res,
+        (ScErrorType::WasmVm, ScErrorCode::InvalidInput)
+    ));
+
+    // invalid section name
+    let res = host.register_test_contract_wasm_from_source_account(
+        wasm_util::wasm_module_with_custom_section(
+            "contractenvmetav1",
+            &soroban_env_common::meta::XDR,
+        )
+        .as_slice(),
+        generate_account_id(&host),
+        generate_bytes_array(&host),
+    );
+    assert!(HostError::result_matches_err(
+        res,
+        (ScErrorType::WasmVm, ScErrorCode::InvalidInput)
+    ));
+
+    let ledger_protocol = meta::get_ledger_protocol_version(meta::INTERFACE_VERSION);
+    let ledger_pre = meta::get_ledger_protocol_version(meta::INTERFACE_VERSION);
+
+    let interface_meta = |proto: u32, pre: u32| -> Vec<u8> {
+        let iv = (proto as u64) << 32 | pre as u64;
+        let entry = ScEnvMetaEntry::ScEnvMetaKindInterfaceVersion(iv);
+        let bytes = Vec::<u8>::new();
+        let mut w = Limited::new(bytes, Limits::none());
+        entry.write_xdr(&mut w).unwrap();
+        w.inner
+    };
+
+    // invalid: protocol is future
+    let res = host.register_test_contract_wasm_from_source_account(
+        wasm_util::wasm_module_with_custom_section(
+            "contractenvmetav0",
+            interface_meta(ledger_protocol + 1, ledger_pre).as_slice(),
+        )
+        .as_slice(),
+        generate_account_id(&host),
+        generate_bytes_array(&host),
+    );
+    assert!(HostError::result_matches_err(
+        res,
+        (ScErrorType::WasmVm, ScErrorCode::InvalidInput)
+    ));
+
+    if cfg!(not(feature = "next")) {
+        // invalid: protocol is old but pre-release version is non-zero
+        let res = host.register_test_contract_wasm_from_source_account(
+            wasm_util::wasm_module_with_custom_section(
+                "contractenvmetav0",
+                interface_meta(ledger_protocol - 1, 1).as_slice(),
+            )
+            .as_slice(),
+            generate_account_id(&host),
+            generate_bytes_array(&host),
+        );
+        assert!(HostError::result_matches_err(
+            res,
+            (ScErrorType::WasmVm, ScErrorCode::InvalidInput)
+        ));
+
+        // invalid: protocol is current but pre-release version doesn't match env's
+        let res = host.register_test_contract_wasm_from_source_account(
+            wasm_util::wasm_module_with_custom_section(
+                "contractenvmetav0",
+                interface_meta(ledger_protocol, ledger_pre + 1).as_slice(),
+            )
+            .as_slice(),
+            generate_account_id(&host),
+            generate_bytes_array(&host),
+        );
+        assert!(HostError::result_matches_err(
+            res,
+            (ScErrorType::WasmVm, ScErrorCode::InvalidInput)
+        ));
+    }
+
+    Ok(())
+}
+
+#[test]
+fn test_floating_point() -> Result<(), HostError> {
+    let wasm = wasm_util::wasm_module_with_floating_point_ops();
+    let host = Host::test_host_with_recording_footprint();
+    host.enable_debug()?;
+    let res = host.register_test_contract_wasm_from_source_account(
+        wasm.as_slice(),
+        generate_account_id(&host),
+        generate_bytes_array(&host),
+    );
+    assert!(HostError::result_matches_err(
+        res,
+        (ScErrorType::WasmVm, ScErrorCode::InvalidAction)
+    ));
+    Ok(())
+}
+
+#[test]
+fn test_multiple_memory() -> Result<(), HostError> {
+    let wasm = wasm_util::wasm_module_with_multiple_memories();
+    let host = Host::test_host_with_recording_footprint();
+    host.enable_debug()?;
+    let res = host.register_test_contract_wasm_from_source_account(
+        wasm.as_slice(),
+        generate_account_id(&host),
+        generate_bytes_array(&host),
+    );
+    assert!(HostError::result_matches_err(
+        res,
+        (ScErrorType::WasmVm, ScErrorCode::InvalidAction)
+    ));
+    Ok(())
+}
+
+#[test]
+fn test_function_import_with_wrong_type() -> Result<(), HostError> {
+    let wasm = wasm_util::wasm_module_lying_about_import_function_type();
+    let host = Host::test_host_with_recording_footprint();
+    host.enable_debug()?;
+    let res = host.register_test_contract_wasm_from_source_account(
+        wasm.as_slice(),
+        generate_account_id(&host),
+        generate_bytes_array(&host),
+    );
+    assert!(HostError::result_matches_err(
+        res,
+        (ScErrorType::WasmVm, ScErrorCode::InvalidAction)
+    ));
+    Ok(())
+}
+
+#[test]
+fn test_import_nonexistent_function() -> Result<(), HostError> {
+    let wasm = wasm_util::wasm_module_importing_nonexistent_function();
+    let host = Host::test_host_with_recording_footprint();
+    host.enable_debug()?;
+    let res = host.register_test_contract_wasm_from_source_account(
+        wasm.as_slice(),
+        generate_account_id(&host),
+        generate_bytes_array(&host),
+    );
+    assert!(HostError::result_matches_err(
+        res,
+        (ScErrorType::WasmVm, ScErrorCode::InvalidAction)
+    ));
+    Ok(())
+}
+
+#[test]
+fn test_duplicate_function_import() -> Result<(), HostError> {
+    // repeating importing the same function is actually okay
+    let wasm = wasm_util::wasm_module_with_duplicate_function_import(5);
+    let host = Host::test_host_with_recording_footprint();
+    host.enable_debug()?;
+    let res = host.register_test_contract_wasm_from_source_account(
+        wasm.as_slice(),
+        generate_account_id(&host),
+        generate_bytes_array(&host),
+    );
+    assert!(res.is_ok());
+
+    // excessive importing leads to large wasm and eventually runs out of budget
+    let wasm2 = wasm_util::wasm_module_with_duplicate_function_import(50000);
+    let res = host.register_test_contract_wasm_from_source_account(
+        wasm2.as_slice(),
+        generate_account_id(&host),
+        generate_bytes_array(&host),
+    );
+    assert!(HostError::result_matches_err(
+        res,
+        (ScErrorType::Budget, ScErrorCode::ExceededLimit)
+    ));
+    Ok(())
+}
+
+#[test]
+fn test_export_nonexistent_function() -> Result<(), HostError> {
+    let wasm = wasm_util::wasm_module_with_nonexistent_function_export();
+    let host = Host::test_host_with_recording_footprint();
+    host.enable_debug()?;
+    let res = host.register_test_contract_wasm_from_source_account(
+        wasm.as_slice(),
+        generate_account_id(&host),
+        generate_bytes_array(&host),
+    );
+    assert!(HostError::result_matches_err(
+        res,
+        (ScErrorType::WasmVm, ScErrorCode::InvalidAction)
+    ));
+    Ok(())
+}
+
+#[test]
+fn test_nonexistent_func_element() -> Result<(), HostError> {
+    let wasm = wasm_util::wasm_module_with_nonexistent_func_element();
+    let host = Host::test_host_with_recording_footprint();
+    host.enable_debug()?;
+    let res = host.register_test_contract_wasm_from_source_account(
+        wasm.as_slice(),
+        generate_account_id(&host),
+        generate_bytes_array(&host),
+    );
+    assert!(HostError::result_matches_err(
+        res,
+        (ScErrorType::WasmVm, ScErrorCode::InvalidAction)
+    ));
+    Ok(())
+}
+
+#[test]
+fn test_no_start() -> Result<(), HostError> {
+    let wasm = wasm_util::wasm_module_with_start_function();
+    let host = Host::test_host_with_recording_footprint();
+    host.enable_debug()?;
+    let res = host.register_test_contract_wasm_from_source_account(
+        wasm.as_slice(),
+        generate_account_id(&host),
+        generate_bytes_array(&host),
+    );
+    assert!(HostError::result_matches_err(
+        res,
+        (ScErrorType::WasmVm, ScErrorCode::InvalidAction)
+    ));
+    Ok(())
+}
+
+#[test]
+fn test_too_large_data_count() -> Result<(), HostError> {
+    let host = Host::test_host_with_recording_footprint();
+    host.as_budget().reset_unlimited()?;
+    host.enable_debug()?;
+
+    // the segment limit in wasmparser is 100000, although that doesn't appear
+    // in the WASM spec (or I couldn't find it)
+    let wasm = wasm_util::wasm_module_with_data_count(100_001, 8, 100_001);
+    let res = host.register_test_contract_wasm_from_source_account(
+        wasm.as_slice(),
+        generate_account_id(&host),
+        generate_bytes_array(&host),
+    );
+    assert!(HostError::result_matches_err(
+        res,
+        (ScErrorType::WasmVm, ScErrorCode::InvalidAction)
+    ));
+
+    Ok(())
+}
+
+#[test]
+fn test_lying_about_data_count() -> Result<(), HostError> {
+    let host = Host::test_host_with_recording_footprint();
+    host.enable_debug()?;
+
+    // sanity check: truthful data count passes validation
+    let wasm = wasm_util::wasm_module_with_data_count(10, 0x100, 10);
+    let res = host.register_test_contract_wasm_from_source_account(
+        wasm.as_slice(),
+        generate_account_id(&host),
+        generate_bytes_array(&host),
+    );
+    assert!(res.is_ok());
+
+    // lying about the count
+    let wasm_bad = wasm_util::wasm_module_with_data_count(10, 0x100, 11);
+    let res = host.register_test_contract_wasm_from_source_account(
+        wasm_bad.as_slice(),
+        generate_account_id(&host),
+        generate_bytes_array(&host),
+    );
+    assert!(HostError::result_matches_err(
+        res,
+        (ScErrorType::WasmVm, ScErrorCode::InvalidAction)
+    ));
+    Ok(())
+}
+
+#[test]
+fn test_multi_value() -> Result<(), HostError> {
+    let host = Host::test_host_with_recording_footprint();
+    host.enable_debug()?;
+
+    // lying about the count
+    let wasm_bad = wasm_util::wasm_module_with_multi_value();
+    let res = host.register_test_contract_wasm_from_source_account(
+        wasm_bad.as_slice(),
+        generate_account_id(&host),
+        generate_bytes_array(&host),
+    );
+    assert!(HostError::result_matches_err(
+        res,
+        (ScErrorType::WasmVm, ScErrorCode::InvalidAction)
+    ));
+    Ok(())
+}
+
+#[test]
+fn test_large_wasm_code() -> Result<(), HostError> {
+    let host = Host::test_host_with_recording_footprint();
+
+    let wasm = wasm_util::wasm_module_with_4n_insns(100000);
+    let res = host.register_test_contract_wasm_from_source_account(
+        wasm.as_slice(),
+        generate_account_id(&host),
+        generate_bytes_array(&host),
+    );
+    assert!(HostError::result_matches_err(
+        res,
+        (ScErrorType::Budget, ScErrorCode::ExceededLimit)
+    ));
+    Ok(())
+}
+
+#[test]
+fn test_large_number_of_internal_funcs() -> Result<(), HostError> {
+    let host = Host::test_host_with_recording_footprint();
+
+    let wasm = wasm_util::wasm_module_with_n_funcs_no_export(100000);
+    let res = host.register_test_contract_wasm_from_source_account(
+        wasm.as_slice(),
+        generate_account_id(&host),
+        generate_bytes_array(&host),
+    );
+    assert!(HostError::result_matches_err(
+        res,
+        (ScErrorType::Budget, ScErrorCode::ExceededLimit)
+    ));
+    Ok(())
+}
+
+#[test]
+fn test_repeated_export_same_func() -> Result<(), HostError> {
+    let host = Host::test_host_with_recording_footprint();
+
+    // the export limit in wasmparser is 100000, although that doesn't appear in
+    // the WASM spec (or I couldn't find it), and wasmi doesn't have that limit either
+    let wasm = wasm_util::wasm_module_with_repeated_exporting_the_same_func(100001);
+    let res = host.register_test_contract_wasm_from_source_account(
+        wasm.as_slice(),
+        generate_account_id(&host),
+        generate_bytes_array(&host),
+    );
+    assert!(HostError::result_matches_err(
+        res,
+        (ScErrorType::Budget, ScErrorCode::ExceededLimit)
+    ));
+    Ok(())
+}
+
+#[test]
+fn test_large_elements() -> Result<(), HostError> {
+    let host = Host::test_host_with_recording_footprint();
+    let wasm = wasm_util::wasm_module_large_elements(100001);
+    let res = host.register_test_contract_wasm_from_source_account(
+        wasm.as_slice(),
+        generate_account_id(&host),
+        generate_bytes_array(&host),
+    );
+    assert!(HostError::result_matches_err(
+        res,
+        (ScErrorType::Budget, ScErrorCode::ExceededLimit)
+    ));
+    Ok(())
+}
+
+#[test]
+fn test_large_globals() -> Result<(), HostError> {
+    let host = Host::test_host_with_recording_footprint();
+    let wasm = wasm_util::wasm_module_large_globals(100001);
+    let res = host.register_test_contract_wasm_from_source_account(
+        wasm.as_slice(),
+        generate_account_id(&host),
+        generate_bytes_array(&host),
+    );
+    assert!(HostError::result_matches_err(
+        res,
+        (ScErrorType::Budget, ScErrorCode::ExceededLimit)
+    ));
+    Ok(())
+}
diff --git a/soroban-env-host/src/test/post_mvp.rs b/soroban-env-host/src/test/post_mvp.rs
index 045344de..87f5fbe7 100644
--- a/soroban-env-host/src/test/post_mvp.rs
+++ b/soroban-env-host/src/test/post_mvp.rs
@@ -1,32 +1,8 @@
-use crate::{Env, EnvBase, Host, HostError, Symbol, Tag, TryFromVal};
-use soroban_synth_wasm::{Arity, ModEmitter};
-
-// Emit a wasm module that uses post-MVP WASM features. Specifically
-// mutable-globals and sign-ext.
-fn post_mvp_wasm_module() -> Vec<u8> {
-    let mut me = ModEmitter::default();
-
-    // Emit an exported mutable global
-    me.define_global_i64(-100, true, Some("global"));
-
-    let mut fe = me.func(Arity(0), 0);
-    fe.i64_const(0x0000_0000_ffff_abcd_u64 as i64);
-
-    // Emit an op from the new sign-ext repertoire
-    fe.i64_extend32s();
-
-    // Turn this into a I64Small
-    fe.i64_const(8);
-    fe.i64_shl();
-    fe.i64_const(Tag::I64Small as i64);
-    fe.i64_or();
-
-    fe.finish_and_export("test").finish()
-}
+use crate::{testutils::wasm, Env, EnvBase, Host, HostError, Symbol, TryFromVal};
 
 #[test]
 fn test_enabled_post_mvp_features() -> Result<(), HostError> {
-    let wasm = post_mvp_wasm_module();
+    let wasm = wasm::post_mvp_wasm_module();
     let host = observe_host!(Host::test_host_with_recording_footprint());
     host.enable_debug()?;
     let addr = host.register_test_contract_wasm(wasm.as_slice());
diff --git a/soroban-env-host/src/testutils.rs b/soroban-env-host/src/testutils.rs
index dc721b77..750167a9 100644
--- a/soroban-env-host/src/testutils.rs
+++ b/soroban-env-host/src/testutils.rs
@@ -1,11 +1,6 @@
-use std::cell::Cell;
-use std::panic::{catch_unwind, set_hook, take_hook, UnwindSafe};
-use std::sync::Once;
-
-use crate::{StorageType, SymbolSmall};
-use std::{collections::BTreeMap, rc::Rc};
-
 use rand::RngCore;
+use std::panic::{catch_unwind, set_hook, take_hook, UnwindSafe};
+use std::{cell::Cell, collections::BTreeMap, rc::Rc, sync::Once};
 
 use crate::{
     budget::Budget,
@@ -14,7 +9,8 @@ use crate::{
         AccountId, ContractCostType, LedgerEntry, LedgerKey, PublicKey, ScAddress, ScErrorCode,
         ScErrorType, ScVal, ScVec, Uint256,
     },
-    AddressObject, BytesObject, Env, EnvBase, Error, Host, HostError, LedgerInfo, Val, VecObject,
+    AddressObject, BytesObject, Env, EnvBase, Error, Host, HostError, LedgerInfo, StorageType,
+    SymbolSmall, Val, VecObject,
 };
 
 /// Catch panics while suppressing the default panic hook that prints to the
@@ -374,8 +370,8 @@ impl Host {
 
 #[cfg(test)]
 pub(crate) mod wasm {
-    use crate::{Symbol, U32Val, Val};
-    use soroban_synth_wasm::{Arity, LocalRef, ModEmitter, Operand};
+    use crate::{Symbol, Tag, U32Val, Val};
+    use soroban_synth_wasm::{Arity, FuncRef, LocalRef, ModEmitter, Operand};
 
     pub(crate) fn wasm_module_with_4n_insns(n: usize) -> Vec<u8> {
         let mut fe = ModEmitter::default().func(Arity(1), 0);
@@ -392,6 +388,27 @@ pub(crate) mod wasm {
         fe.finish_and_export("test").finish()
     }
 
+    pub(crate) fn wasm_module_with_n_funcs_no_export(n: usize) -> Vec<u8> {
+        let mut me = ModEmitter::default();
+        for _i in 0..n {
+            let mut fe = me.func(Arity(0), 0);
+            fe.push(Symbol::try_from_small_str("pass").unwrap());
+            me = fe.finish().0;
+        }
+        me.finish()
+    }
+
+    pub(crate) fn wasm_module_with_repeated_exporting_the_same_func(n: usize) -> Vec<u8> {
+        let me = ModEmitter::default();
+        let mut fe = me.func(Arity(0), 0);
+        fe.push(Symbol::try_from_small_str("pass").unwrap());
+        let (mut me, fid) = fe.finish();
+        for i in 0..n {
+            me.export_func(fid, format!("test{}", i).as_str());
+        }
+        me.finish_no_validate()
+    }
+
     pub(crate) fn wasm_module_with_mem_grow(n_pages: usize) -> Vec<u8> {
         let mut fe = ModEmitter::default().func(Arity(0), 0);
         fe.push(Operand::Const32(n_pages as i32));
@@ -438,7 +455,7 @@ pub(crate) mod wasm {
         let (mut me, f2) = fe.finish();
         // store in table
         me.define_elems(&[f0, f1, f2]);
-        let ty = me.get_fn_type(Arity(0));
+        let ty = me.get_fn_type(Arity(0), Arity(1));
         // the caller
         fe = me.func(Arity(1), 0);
         fe.local_get(LocalRef(0));
@@ -500,6 +517,8 @@ pub(crate) mod wasm {
     ) -> Vec<u8> {
         let mut me = ModEmitter::from_configs(num_pages, 128);
         let mem_len = num_pages * 0x10_000;
+        // we just make sure the total memory can fit one segments the segments
+        // will just cycle through the space and possibly override earlier ones
         let max_segments = (mem_len / seg_size.max(1)).max(1);
         for _i in 0..num_sgmts % max_segments {
             me.define_data_segment(0, vec![0; seg_size as usize]);
@@ -580,6 +599,165 @@ pub(crate) mod wasm {
         let vals_pos = U32Val::from(num_vals * 8);
         let keys_pos = U32Val::from(num_vals * 16);
         fe.map_new_from_linear_memory(keys_pos, vals_pos, U32Val::from(num_vals));
+
+        fe.finish_and_export("test").finish()
+    }
+
+    pub(crate) fn wasm_module_with_data_count(
+        num_sgmts: u32,
+        seg_size: u32,
+        data_count: u32,
+    ) -> Vec<u8> {
+        // first calculate the number of memory pages needed to fit all the data
+        // segments non-overlapping
+        let pages = num_sgmts * seg_size / 0x10_000 + 1;
+        let mut me = ModEmitter::from_configs(pages, 128);
+        for i in 0..num_sgmts {
+            me.define_data_segment(i * seg_size, vec![7; seg_size as usize]);
+        }
+        // define the data count, if count != num_sgmts, then the validator is
+        // supposed to catch it
+        me.data_count(data_count);
+        me.finish_no_validate()
+    }
+
+    // Emit a wasm module that uses post-MVP WASM features. Specifically
+    // mutable-globals and sign-ext.
+    pub fn post_mvp_wasm_module() -> Vec<u8> {
+        let mut me = ModEmitter::default();
+
+        // Emit an exported mutable global
+        me.define_global_i64(-100, true, Some("global"));
+
+        let mut fe = me.func(Arity(0), 0);
+        fe.i64_const(0x0000_0000_ffff_abcd_u64 as i64);
+
+        // Emit an op from the new sign-ext repertoire
+        fe.i64_extend32s();
+
+        // Turn this into a I64Small
+        fe.i64_const(8);
+        fe.i64_shl();
+        fe.i64_const(Tag::I64Small as i64);
+        fe.i64_or();
+
+        fe.finish_and_export("test").finish()
+    }
+
+    pub fn empty_wasm_module() -> Vec<u8> {
+        ModEmitter::new().finish()
+    }
+
+    pub fn wasm_module_with_custom_section(name: &str, data: &[u8]) -> Vec<u8> {
+        let mut me = ModEmitter::new();
+        me.custom_section(name, data);
+        me.finish()
+    }
+
+    pub fn wasm_module_with_floating_point_ops() -> Vec<u8> {
+        let me = ModEmitter::default();
+        let mut fe = me.func(Arity(0), 0);
+        fe.f64_const(1.1f64);
+        fe.f64_const(2.2f64);
+        fe.f64_add();
+        fe.drop();
+        fe.push(Symbol::try_from_small_str("pass").unwrap());
+        fe.finish_and_export("test").finish()
+    }
+
+    pub fn wasm_module_with_multiple_memories() -> Vec<u8> {
+        let mut me = ModEmitter::new();
+        me.memory(1, None, false, false);
+        me.memory(1, None, false, false);
+        me.finish()
+    }
+
+    pub fn wasm_module_lying_about_import_function_type() -> Vec<u8> {
+        let mut me = ModEmitter::default();
+        // function {"t", "_"} is "dummy0", taking 0 arguments
+        // this will not pass the wasmi linker
+        me.import_func("t", "_", Arity(1));
+        me.finish()
+    }
+
+    pub fn wasm_module_importing_nonexistent_function() -> Vec<u8> {
+        let mut me = ModEmitter::default();
+        me.import_func("t", "z", Arity(1));
+        me.finish()
+    }
+
+    pub fn wasm_module_with_duplicate_function_import(n: u32) -> Vec<u8> {
+        let mut me = ModEmitter::default();
+        for _ in 0..n {
+            me.import_func_no_check("t", "_", Arity(0));
+        }
+        me.finish()
+    }
+
+    pub fn wasm_module_with_nonexistent_function_export() -> Vec<u8> {
+        let mut me = ModEmitter::default();
+        me.import_func("t", "_", Arity(0));
+        let mut fe = me.func(Arity(0), 0);
+        fe.push(Symbol::try_from_small_str("pass").unwrap());
+        let (mut me, fid) = fe.finish();
+        println!("{}", fid.0);
+        // exporting a function I defined is okay
+        me.export_func(fid, "test");
+        // exporting an imported function is also okay, although weird
+        me.export_func(FuncRef(0), "test0");
+        // importing an non-existent function will not pass validation
+        me.export_func(FuncRef(100), "test100");
+        me.finish_no_validate()
+    }
+
+    pub(crate) fn wasm_module_with_nonexistent_func_element() -> Vec<u8> {
+        let mut me = ModEmitter::default();
+        // an imported function
+        let f0 = me.import_func("t", "_", Arity(0));
+        // a local wasm function
+        let mut fe = me.func(Arity(0), 0);
+        fe.push(Symbol::try_from_small_str("pass").unwrap());
+        let (me, f1) = fe.finish();
+        // another local wasm function
+        let mut fe = me.func(Arity(0), 0);
+        fe.push(Symbol::try_from_small_str("pass2").unwrap());
+        let (mut me, f2) = fe.finish();
+        // store in table, FuncRef(100) is invalid
+        me.define_elems(&[f0, f1, f2, FuncRef(100)]);
+        me.finish_no_validate()
+    }
+
+    pub(crate) fn wasm_module_with_start_function() -> Vec<u8> {
+        let me = ModEmitter::default();
+        let fe = me.func_with_arity_and_ret(Arity(0), Arity(0), 0);
+        let (mut me, fid) = fe.finish();
+        me.export_func(fid.clone(), "start");
+        me.start(fid);
+        me.finish_no_validate()
+    }
+
+    pub(crate) fn wasm_module_with_multi_value() -> Vec<u8> {
+        let me = ModEmitter::default();
+        let mut fe = me.func_with_arity_and_ret(Arity(0), Arity(2), 0);
+        fe.push(Symbol::try_from_small_str("pass1").unwrap());
+        fe.push(Symbol::try_from_small_str("pass2").unwrap());
         fe.finish_and_export("test").finish()
     }
+
+    pub(crate) fn wasm_module_large_elements(n: u32) -> Vec<u8> {
+        let mut me = ModEmitter::from_configs(1, n);
+        // an imported function
+        let f0 = me.import_func("t", "_", Arity(0));
+        // store in table, FuncRef(100) is invalid
+        me.define_elems(vec![f0; n as usize].as_slice());
+        me.finish()
+    }
+
+    pub(crate) fn wasm_module_large_globals(n: u32) -> Vec<u8> {
+        let mut me = ModEmitter::default();
+        for i in 0..n {
+            me.define_global_i64(i as i64, true, None);
+        }
+        me.finish()
+    }
 }
diff --git a/soroban-synth-wasm/src/test.rs b/soroban-synth-wasm/src/test.rs
index 129c64c4..2eae66a0 100644
--- a/soroban-synth-wasm/src/test.rs
+++ b/soroban-synth-wasm/src/test.rs
@@ -136,7 +136,7 @@ fn call_indirect() {
     let (mut me, f2) = fe.finish();
     // store in table
     me.define_elems(&[f0, f1, f2]);
-    let ty = me.get_fn_type(Arity(0));
+    let ty = me.get_fn_type(Arity(0), Arity(1));
     // the caller
     fe = me.func(Arity(1), 0);
     fe.push(Operand::Const32(1));