Repository: stellar/rs-soroban-env
Pull request #1298 (2023-12-14 19:25:55)

Bad wasm tests
### What

Resolves https://github.com/stellar/rs-soroban-env/issues/1145

Add rest of the bad wasm cases:
- SIMD
- invalid const-expressions in
    - global
    - active elements table
    - active data segment mode
- very large sections
    - tables
    - types

### Why

[TODO: Why this change is being made. Include any context required to understand the why.]

### Known limitations

[TODO or N/A]



Patch:
diff --git a/Cargo.lock b/Cargo.lock
index a246acda..760e91c4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1418,6 +1418,7 @@ dependencies = [
  "textplots",
  "thousands",
  "tracy-client",
+ "wasm-encoder",
  "wasmprinter",
 ]
 
diff --git a/soroban-env-host/Cargo.toml b/soroban-env-host/Cargo.toml
index 48fbb507..aad1dbc9 100644
--- a/soroban-env-host/Cargo.toml
+++ b/soroban-env-host/Cargo.toml
@@ -62,6 +62,7 @@ serde_json = "=1.0.108"
 arbitrary = "=1.3.2"
 lstsq = "=0.5.0"
 nalgebra = { version = "=0.32.3", default-features = false, features = ["std"]}
+wasm-encoder = "=0.36.2"
 
 [dev-dependencies.stellar-xdr]
 version = "=20.0.0"
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 079426dd..4453462b 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
@@ -234,7 +234,7 @@ fn call_indirect(n: u64, _rng: &mut StdRng) -> WasmModule {
     fe.push(Symbol::try_from_small_str("fail").unwrap());
     let (mut me, f2) = fe.finish();
     // store in table
-    me.define_elems(&[f0, f1, f2]);
+    me.define_elem_funcs(&[f0, f1, f2]);
     let ty = me.get_fn_type(Arity(0), Arity(1));
     // the caller
     fe = me.func(Arity(0), 0);
diff --git a/soroban-env-host/src/budget/wasmi_helper.rs b/soroban-env-host/src/budget/wasmi_helper.rs
index adb75e12..465b4055 100644
--- a/soroban-env-host/src/budget/wasmi_helper.rs
+++ b/soroban-env-host/src/budget/wasmi_helper.rs
@@ -54,7 +54,10 @@ impl FuelConfig {
 pub(crate) struct WasmiLimits {
     pub table_elements: u32,
     pub instances: usize,
+    // The `tables` limit is only relevant if `wasmi_reference_type` is enabled
     pub tables: usize,
+    // The `memories` limit is irrelevant. At the current version of WASM, at
+    // most one memory may be defined or imported in a single module
     pub memories: usize,
 }
 
diff --git a/soroban-synth-wasm/src/func_emitter.rs b/soroban-synth-wasm/src/func_emitter.rs
index 14b0228d..ee51f66e 100644
--- a/soroban-synth-wasm/src/func_emitter.rs
+++ b/soroban-synth-wasm/src/func_emitter.rs
@@ -396,12 +396,9 @@ macro_rules! consts {
         }
     }
 }
-consts!(
-    (i64_const, I64Const, i64),
-    (i32_const, I32Const, i32),
-    // forbidden instructions
-    (f64_const, F64Const, f64)
-);
+consts!((i64_const, I64Const, i64), (i32_const, I32Const, i32));
+#[cfg(feature = "adversarial")]
+consts!((f64_const, F64Const, f64));
 
 macro_rules! store_load {
     ( $(($func_name: ident, $insn: ident)),* )
@@ -431,9 +428,13 @@ store_load!(
     (i64_store8, I64Store8),
     (i64_store16, I64Store16),
     (i64_store32, I64Store32),
-    (f64_load, F64Load),
-    // forbidden instructions
-    (f64_store, F64Store)
+    (f64_load, F64Load)
+);
+#[cfg(feature = "adversarial")]
+store_load!(
+    (f64_store, F64Store),
+    (v128_load, V128Load),
+    (v128_store, V128Store)
 );
 
 macro_rules! numeric_insn {
@@ -481,10 +482,13 @@ numeric_insn!(
     (i64_rotl, I64Rotl),
     (i64_rotr, I64Rotr),
     // post-MVP instructions
-    (i64_extend32s, I64Extend32S),
-    // forbidden instructions
+    (i64_extend32s, I64Extend32S)
+);
+#[cfg(feature = "adversarial")]
+numeric_insn!(
     (f64_add, F64Add),
     (f64_sub, F64Sub),
     (f64_mul, F64Mul),
-    (f64_div, F64Div)
+    (f64_div, F64Div),
+    (i32x4_add, I32x4Add)
 );
diff --git a/soroban-synth-wasm/src/mod_emitter.rs b/soroban-synth-wasm/src/mod_emitter.rs
index 088154b8..76ae0aa0 100644
--- a/soroban-synth-wasm/src/mod_emitter.rs
+++ b/soroban-synth-wasm/src/mod_emitter.rs
@@ -225,6 +225,19 @@ impl ModEmitter {
         }
     }
 
+    #[cfg(feature = "adversarial")]
+    pub fn add_fn_type_no_check(&mut self, arity: Arity, ret: Arity) -> TypeRef {
+        let params: Vec<_> = std::iter::repeat(ValType::I64)
+            .take(arity.0 as usize)
+            .collect();
+        let rets: Vec<_> = std::iter::repeat(ValType::I64)
+            .take(ret.0 as usize)
+            .collect();
+        let ty_id = TypeRef(self.types.len());
+        self.types.function(params, rets);
+        ty_id
+    }
+
     /// Return the unique [`FuncRef`] for a function import with a given module
     /// name, function name, and arity, creating such an import in the `import`
     /// section of the module if it does not already exist.
@@ -277,7 +290,7 @@ impl ModEmitter {
             .export(name, wasm_encoder::ExportKind::Func, fid.0);
     }
 
-    pub fn define_elems(&mut self, funcs: &[FuncRef]) {
+    pub fn define_elem_funcs(&mut self, funcs: &[FuncRef]) {
         let table_index = 0;
         let offset = ConstExpr::i32_const(0);
         let ids: Vec<u32> = funcs.iter().map(|r| r.0).collect();
@@ -298,6 +311,26 @@ impl ModEmitter {
             .active(0, &ConstExpr::i32_const(mem_offset as i32), data);
     }
 
+    #[cfg(feature = "adversarial")]
+    pub fn define_active_elements(
+        &mut self,
+        table_index: Option<u32>,
+        offset: &ConstExpr,
+        elements: Elements<'_>,
+    ) {
+        self.elements.active(table_index, offset, elements);
+    }
+
+    #[cfg(feature = "adversarial")]
+    pub fn define_global(&mut self, val_type: ValType, mutable: bool, init_expr: &ConstExpr) {
+        self.global(val_type, mutable, init_expr);
+    }
+
+    #[cfg(feature = "adversarial")]
+    pub fn define_active_data(&mut self, memory_index: u32, offset: &ConstExpr, data: Vec<u8>) {
+        self.data.active(memory_index, offset, data);
+    }
+
     /// Finish emitting code, consuming the `self`, serializing a WASM binary
     /// blob, validating and returning it. Panics the resulting blob fails
     /// validation.


Test patch:
diff --git a/soroban-env-host/src/test/hostile.rs b/soroban-env-host/src/test/hostile.rs
index 3f8411e7..f4bd6b63 100644
--- a/soroban-env-host/src/test/hostile.rs
+++ b/soroban-env-host/src/test/hostile.rs
@@ -1121,7 +1121,7 @@ fn test_repeated_export_same_func() -> Result<(), HostError> {
 #[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 wasm = wasm_util::wasm_module_large_elements(100001, 100001);
     let res = host.register_test_contract_wasm_from_source_account(
         wasm.as_slice(),
         generate_account_id(&host),
@@ -1134,6 +1134,22 @@ fn test_large_elements() -> Result<(), HostError> {
     Ok(())
 }
 
+#[test]
+fn test_oob_elements() -> Result<(), HostError> {
+    let host = Host::test_host_with_recording_footprint();
+    let wasm = wasm_util::wasm_module_large_elements(128, 129);
+    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_large_globals() -> Result<(), HostError> {
     let host = Host::test_host_with_recording_footprint();
@@ -1149,3 +1165,113 @@ fn test_large_globals() -> Result<(), HostError> {
     ));
     Ok(())
 }
+
+#[test]
+fn test_large_number_of_tables() -> Result<(), HostError> {
+    let host = Host::test_host_with_recording_footprint();
+    host.enable_debug()?;
+    // even though we have enabled wasmi_reference_type, which makes multiple
+    // tables possible, we have explicitly set our table count limit to 1, in
+    // `WASMI_LIMITS_CONFIG`. Thus we essentially not allow multiple tables.
+    let wasm = wasm_util::wasm_module_with_many_tables(2);
+    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_large_number_of_func_types() -> Result<(), HostError> {
+    let host = Host::test_host_with_recording_footprint();
+    let wasm = wasm_util::wasm_module_with_many_func_types(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_simd() -> Result<(), HostError> {
+    let host = Host::test_host_with_recording_footprint();
+    host.enable_debug()?;
+    let wasm = wasm_util::wasm_module_with_simd_add_i32x4();
+    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_invalid_expr_in_global() -> Result<(), HostError> {
+    let host = Host::test_host_with_recording_footprint();
+    host.enable_debug()?;
+    for i in 0..3 {
+        let wasm = wasm_util::wasm_module_various_constexr_in_global(i);
+        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_invalid_expr_in_elements() -> Result<(), HostError> {
+    let host = Host::test_host_with_recording_footprint();
+    host.enable_debug()?;
+    for i in 0..4 {
+        let wasm = wasm_util::wasm_module_various_constexpr_in_elements(i);
+        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_invalid_expr_in_segments() -> Result<(), HostError> {
+    let host = Host::test_host_with_recording_footprint();
+    host.enable_debug()?;
+    for i in 0..4 {
+        let wasm = wasm_util::wasm_module_various_constexr_in_data_segment(i);
+        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(())
+}
diff --git a/soroban-env-host/src/testutils.rs b/soroban-env-host/src/testutils.rs
index 750167a9..a7eb3fe9 100644
--- a/soroban-env-host/src/testutils.rs
+++ b/soroban-env-host/src/testutils.rs
@@ -1,7 +1,3 @@
-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,
     storage::{SnapshotSource, Storage},
@@ -12,6 +8,9 @@ use crate::{
     AddressObject, BytesObject, Env, EnvBase, Error, Host, HostError, LedgerInfo, StorageType,
     SymbolSmall, Val, VecObject,
 };
+use rand::RngCore;
+use std::panic::{catch_unwind, set_hook, take_hook, UnwindSafe};
+use std::{cell::Cell, collections::BTreeMap, rc::Rc, sync::Once};
 
 /// Catch panics while suppressing the default panic hook that prints to the
 /// console.
@@ -372,6 +371,7 @@ impl Host {
 pub(crate) mod wasm {
     use crate::{Symbol, Tag, U32Val, Val};
     use soroban_synth_wasm::{Arity, FuncRef, LocalRef, ModEmitter, Operand};
+    use wasm_encoder::{ConstExpr, Elements, RefType};
 
     pub(crate) fn wasm_module_with_4n_insns(n: usize) -> Vec<u8> {
         let mut fe = ModEmitter::default().func(Arity(1), 0);
@@ -454,7 +454,7 @@ pub(crate) mod wasm {
         fe.push(Symbol::try_from_small_str("pass2").unwrap());
         let (mut me, f2) = fe.finish();
         // store in table
-        me.define_elems(&[f0, f1, f2]);
+        me.define_elem_funcs(&[f0, f1, f2]);
         let ty = me.get_fn_type(Arity(0), Arity(1));
         // the caller
         fe = me.func(Arity(1), 0);
@@ -723,7 +723,7 @@ pub(crate) mod wasm {
         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.define_elem_funcs(&[f0, f1, f2, FuncRef(100)]);
         me.finish_no_validate()
     }
 
@@ -744,15 +744,53 @@ pub(crate) mod wasm {
         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);
+    // if n > m, we have oob elements
+    pub(crate) fn wasm_module_large_elements(m: u32, n: u32) -> Vec<u8> {
+        let mut me = ModEmitter::from_configs(1, m);
         // 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.define_elem_funcs(vec![f0; n as usize].as_slice());
         me.finish()
     }
 
+    pub(crate) fn wasm_module_various_constexpr_in_elements(case: u32) -> Vec<u8> {
+        let mut me = ModEmitter::default();
+        // an imported function
+        let f0 = me.import_func("t", "_", Arity(0));
+
+        match case {
+            0 =>
+            // err: wrong type, expect i32, passing i64
+            {
+                me.define_active_elements(
+                    None,
+                    &ConstExpr::i64_const(0),
+                    Elements::Functions(&[f0.0]),
+                )
+            }
+            // err: fp
+            1 => me.define_active_elements(
+                None,
+                &ConstExpr::f64_const(1.0),
+                Elements::Functions(&[f0.0]),
+            ),
+            // err: simd
+            2 => me.define_active_elements(
+                None,
+                &ConstExpr::v128_const(1),
+                Elements::Functions(&[f0.0]),
+            ),
+            // err: wrong type, expect i32, passing funcref
+            3 => me.define_active_elements(
+                None,
+                &ConstExpr::ref_func(f0.0),
+                Elements::Functions(&[f0.0]),
+            ),
+            _ => panic!("not a valid option"),
+        }
+        me.finish_no_validate()
+    }
+
     pub(crate) fn wasm_module_large_globals(n: u32) -> Vec<u8> {
         let mut me = ModEmitter::default();
         for i in 0..n {
@@ -760,4 +798,105 @@ pub(crate) mod wasm {
         }
         me.finish()
     }
+
+    pub(crate) fn wasm_module_various_constexr_in_global(case: u32) -> Vec<u8> {
+        let mut me = ModEmitter::default();
+        match case {
+            0 =>
+            // err: mismatch
+            {
+                me.define_global(wasm_encoder::ValType::I32, true, &ConstExpr::i64_const(1))
+            }
+            1 =>
+            // err: fp
+            {
+                me.define_global(wasm_encoder::ValType::F32, true, &ConstExpr::f32_const(1.0))
+            }
+            2 =>
+            // err: simd
+            {
+                me.define_global(wasm_encoder::ValType::V128, true, &ConstExpr::v128_const(1))
+            }
+            3 =>
+            // okay: func
+            {
+                let fr = me.import_func("t", "_", Arity(0));
+                me.define_global(
+                    wasm_encoder::ValType::Ref(RefType::FUNCREF),
+                    true,
+                    &ConstExpr::ref_func(fr.0),
+                )
+            }
+            _ => panic!("not a valid option"),
+        }
+        me.finish_no_validate()
+    }
+
+    pub(crate) fn wasm_module_various_constexr_in_data_segment(case: u32) -> Vec<u8> {
+        let mut me = ModEmitter::default();
+        // an imported function
+        let f0 = me.import_func("t", "_", Arity(0));
+
+        match case {
+            0 =>
+            // err: wrong type, expect i32, passing i64
+            {
+                me.define_active_data(0, &ConstExpr::i64_const(0), vec![0; 8]);
+            }
+            // err: fp
+            1 => {
+                me.define_active_data(0, &ConstExpr::f64_const(0.0), vec![0; 8]);
+            }
+            // err: simd
+            2 => {
+                me.define_active_data(0, &ConstExpr::v128_const(0), vec![0; 8]);
+            }
+            // err: wrong type, expect i32, passing funcref
+            3 => {
+                me.define_active_data(0, &ConstExpr::ref_func(f0.0), vec![0; 8]);
+            }
+            _ => panic!("not a valid option"),
+        }
+        me.finish_no_validate()
+    }
+
+    pub(crate) fn wasm_module_with_many_tables(n: u32) -> Vec<u8> {
+        let mut me = ModEmitter::default();
+        for i in 0..n {
+            let rt = match i % 2 == 0 {
+                true => RefType::FUNCREF,
+                false => RefType::EXTERNREF,
+            };
+            me.table(rt, 2, None);
+        }
+        // wasmparser has an limit of 100 tables. wasmi does not have such a limit
+        me.finish_no_validate()
+    }
+
+    // The only type allowed in the MVP is the function type. There are more
+    // composite types defined by the GC proposal, which should not be allowed
+    // and we will verify that in another test.
+    pub(crate) fn wasm_module_with_many_func_types(n: u32) -> Vec<u8> {
+        let mut me = ModEmitter::default();
+        for _i in 0..n {
+            // it is allowed to define the same types over and over
+            me.add_fn_type_no_check(Arity(0), Arity(0));
+        }
+        me.finish()
+    }
+
+    pub(crate) fn wasm_module_with_simd_add_i32x4() -> Vec<u8> {
+        let me = ModEmitter::default();
+        let mut fe = me.func(Arity(0), 0);
+        // we load [u32, u32, u32, u32] x 2, add them and store back
+        fe.i32_const(32); // ptr for storing the result
+        fe.i32_const(0); // ptr for the first 4xi32
+        fe.v128_load(0, 0);
+        fe.i32_const(16); // ptr for the second 4xi32
+        fe.v128_load(0, 0);
+        fe.i32x4_add();
+        fe.v128_store(0, 0);
+        fe.push(Symbol::try_from_small_str("pass").unwrap());
+        fe.finish_and_export("test").finish()
+    }
 }
diff --git a/soroban-synth-wasm/src/test.rs b/soroban-synth-wasm/src/test.rs
index 2eae66a0..76976aeb 100644
--- a/soroban-synth-wasm/src/test.rs
+++ b/soroban-synth-wasm/src/test.rs
@@ -135,7 +135,7 @@ fn call_indirect() {
     fe.push(Symbol::try_from_small_str("fail").unwrap());
     let (mut me, f2) = fe.finish();
     // store in table
-    me.define_elems(&[f0, f1, f2]);
+    me.define_elem_funcs(&[f0, f1, f2]);
     let ty = me.get_fn_type(Arity(0), Arity(1));
     // the caller
     fe = me.func(Arity(1), 0);