diff --git a/crates/fuzzing/src/oracles/diff_spec.rs b/crates/fuzzing/src/oracles/diff_spec.rs index 42e9fe9d41..2fd2855080 100644 --- a/crates/fuzzing/src/oracles/diff_spec.rs +++ b/crates/fuzzing/src/oracles/diff_spec.rs @@ -4,7 +4,7 @@ use crate::generators::{Config, DiffValue, DiffValueType}; use crate::oracles::engine::{DiffEngine, DiffInstance}; use anyhow::{anyhow, Error, Result}; -use wasm_spec_interpreter::Value; +use wasm_spec_interpreter::SpecValue; use wasmtime::Trap; /// A wrapper for `wasm-spec-interpreter` as a [`DiffEngine`]. @@ -14,24 +14,10 @@ impl SpecInterpreter { pub(crate) fn new(config: &mut Config) -> Self { let config = &mut config.module_config.config; - // TODO: right now the interpreter bindings only execute the first - // function in the module so if there's possibly more than one function - // it's not possible to run the other function. This should be fixed - // with improvements to the ocaml bindings to the interpreter. - config.min_funcs = 1; - config.max_funcs = 1; - - // TODO: right now the instantiation step for the interpreter does - // nothing and the evaluation step performs an instantiation followed by - // an execution. This means that instantiations which fail in other - // engines will "succeed" in the interpreter because the error is - // delayed to the execution. This should be fixed by making - // instantiation a first-class primitive in our interpreter bindings. - config.min_tables = 0; - config.max_tables = 0; - config.min_memories = config.min_memories.min(1); config.max_memories = config.max_memories.min(1); + config.min_tables = config.min_tables.min(1); + config.max_tables = config.max_tables.min(1); config.memory64_enabled = false; config.threads_enabled = false; @@ -48,10 +34,9 @@ impl DiffEngine for SpecInterpreter { } fn instantiate(&mut self, wasm: &[u8]) -> Result> { - // TODO: ideally we would avoid copying the module bytes here. - Ok(Box::new(SpecInstance { - wasm: wasm.to_vec(), - })) + let instance = wasm_spec_interpreter::instantiate(wasm) + .map_err(|e| anyhow!("failed to instantiate in spec interpreter: {}", e))?; + Ok(Box::new(SpecInstance { instance })) } fn assert_error_match(&self, trap: &Trap, err: &Error) { @@ -60,14 +45,12 @@ impl DiffEngine for SpecInterpreter { } fn is_stack_overflow(&self, err: &Error) -> bool { - // TODO: implement this for the spec interpreter - drop(err); - false + err.to_string().contains("(Isabelle) call stack exhausted") } } struct SpecInstance { - wasm: Vec, + instance: wasm_spec_interpreter::SpecInstance, } impl DiffInstance for SpecInstance { @@ -77,55 +60,57 @@ impl DiffInstance for SpecInstance { fn evaluate( &mut self, - _function_name: &str, + function_name: &str, arguments: &[DiffValue], _results: &[DiffValueType], ) -> Result>> { - // The spec interpreter needs some work before it can fully support this - // interface: - // - TODO adapt `wasm-spec-interpreter` to use function name to select - // function to run - // - TODO adapt `wasm-spec-interpreter` to expose an "instance" with - // so we can hash memory, globals, etc. - let arguments = arguments.iter().map(Value::from).collect(); - match wasm_spec_interpreter::interpret(&self.wasm, Some(arguments)) { - Ok(results) => Ok(Some(results.into_iter().map(Value::into).collect())), + let arguments = arguments.iter().map(SpecValue::from).collect(); + match wasm_spec_interpreter::interpret(&self.instance, function_name, Some(arguments)) { + Ok(results) => Ok(Some(results.into_iter().map(SpecValue::into).collect())), Err(err) => Err(anyhow!(err)), } } - fn get_global(&mut self, _name: &str, _ty: DiffValueType) -> Option { - // TODO: should implement this - None + fn get_global(&mut self, name: &str, _ty: DiffValueType) -> Option { + use wasm_spec_interpreter::{export, SpecExport::Global}; + if let Ok(Global(g)) = export(&self.instance, name) { + Some(g.into()) + } else { + panic!("expected an exported global value at name `{}`", name) + } } - fn get_memory(&mut self, _name: &str, _shared: bool) -> Option> { - // TODO: should implement this - None + fn get_memory(&mut self, name: &str, _shared: bool) -> Option> { + use wasm_spec_interpreter::{export, SpecExport::Memory}; + if let Ok(Memory(m)) = export(&self.instance, name) { + Some(m) + } else { + panic!("expected an exported memory at name `{}`", name) + } } } -impl From<&DiffValue> for Value { +impl From<&DiffValue> for SpecValue { fn from(v: &DiffValue) -> Self { match *v { - DiffValue::I32(n) => Value::I32(n), - DiffValue::I64(n) => Value::I64(n), - DiffValue::F32(n) => Value::F32(n as i32), - DiffValue::F64(n) => Value::F64(n as i64), - DiffValue::V128(n) => Value::V128(n.to_le_bytes().to_vec()), + DiffValue::I32(n) => SpecValue::I32(n), + DiffValue::I64(n) => SpecValue::I64(n), + DiffValue::F32(n) => SpecValue::F32(n as i32), + DiffValue::F64(n) => SpecValue::F64(n as i64), + DiffValue::V128(n) => SpecValue::V128(n.to_le_bytes().to_vec()), DiffValue::FuncRef { .. } | DiffValue::ExternRef { .. } => unimplemented!(), } } } -impl Into for Value { +impl Into for SpecValue { fn into(self) -> DiffValue { match self { - Value::I32(n) => DiffValue::I32(n), - Value::I64(n) => DiffValue::I64(n), - Value::F32(n) => DiffValue::F32(n as u32), - Value::F64(n) => DiffValue::F64(n as u64), - Value::V128(n) => { + SpecValue::I32(n) => DiffValue::I32(n), + SpecValue::I64(n) => DiffValue::I64(n), + SpecValue::F32(n) => DiffValue::F32(n as u32), + SpecValue::F64(n) => DiffValue::F64(n as u64), + SpecValue::V128(n) => { assert_eq!(n.len(), 16); DiffValue::V128(u128::from_le_bytes(n.as_slice().try_into().unwrap())) } diff --git a/crates/fuzzing/wasm-spec-interpreter/build.rs b/crates/fuzzing/wasm-spec-interpreter/build.rs index 8a248949f6..29aac49c60 100644 --- a/crates/fuzzing/wasm-spec-interpreter/build.rs +++ b/crates/fuzzing/wasm-spec-interpreter/build.rs @@ -12,7 +12,7 @@ const OCAML_DIR: &'static str = "ocaml"; const SPEC_DIR: &'static str = "ocaml/spec"; const SPEC_REPOSITORY: &'static str = "https://github.com/conrad-watt/spec"; const SPEC_REPOSITORY_BRANCH: &'static str = "wasmtime_fuzzing"; -const SPEC_REPOSITORY_REV: &'static str = "7208af3bdb33fbf357ca5755e4edf2b35147ae95"; +const SPEC_REPOSITORY_REV: &'static str = "c6bab4461e10229e557aae2e1027cadfce0161ce"; fn main() { if cfg!(feature = "build-libinterpret") { diff --git a/crates/fuzzing/wasm-spec-interpreter/ocaml/interpret.ml b/crates/fuzzing/wasm-spec-interpreter/ocaml/interpret.ml index 96afecead3..883bd5ec06 100644 --- a/crates/fuzzing/wasm-spec-interpreter/ocaml/interpret.ml +++ b/crates/fuzzing/wasm-spec-interpreter/ocaml/interpret.ml @@ -1,16 +1,14 @@ -(* This module exposes an [interpret] function to Rust. It wraps several different calls from the -WebAssembly specification interpreter in a way that we can access across the FFI boundary. To -understand this better, see: - - the OCaml manual documentation re: calling OCaml from C, https://ocaml.org/manual/intfc.html#s%3Ac-advexample - - the [ocaml-interop] example, https://github.com/tezedge/ocaml-interop/blob/master/testing/rust-caller/ocaml/callable.ml +(* This module exposes an [interpret] function to Rust. It wraps several +different calls from the WebAssembly specification interpreter in a way that we +can access across the FFI boundary. To understand this better, see: + - the OCaml manual documentation re: calling OCaml from C, + https://ocaml.org/manual/intfc.html#s%3Ac-advexample + - the [ocaml-interop] example, + https://github.com/tezedge/ocaml-interop/blob/master/testing/rust-caller/ocaml/callable.ml *) -(* Here we access the WebAssembly specification interpreter; this must be linked in. *) -open Wasm -open Wasm.WasmRef_Isa_m.WasmRef_Isa - -(** Enumerate the types of values we pass across the FFI boundary. This must match `Value` in -`src/lib.rs` *) +(** Enumerate the types of values we pass across the FFI boundary. This must +match `Value` in `src/lib.rs` *) type ffi_value = | I32 of int32 | I64 of int64 @@ -18,6 +16,18 @@ type ffi_value = | F64 of int64 | V128 of Bytes.t +(** Enumerate the kinds of exported values the interpreter can retrieve. *) +type ffi_export_value = + | Global of ffi_value + | Memory of Bytes.t + +(* Here we access the WebAssembly specification interpreter; this must be linked +in. *) +open Wasm +open Wasm.WasmRef_Isa_m.WasmRef_Isa + +type spec_instance = (unit module_export_ext list * ((unit s_m_ext) ref)) + (** Helper for converting the FFI values to their spec interpreter type. *) let convert_to_wasm (v: ffi_value) : v = match v with | I32 n -> V_num (ConstInt32 (I32_impl_abs n)) @@ -33,28 +43,48 @@ let convert_from_wasm (v: v) : ffi_value = match v with | V_num ((ConstFloat32 n)) -> F32 (F32.to_bits n) | V_num ((ConstFloat64 n)) -> F64 (F64.to_bits n) | V_vec ((ConstVec128 n)) -> V128 (Bytes.of_string (V128.to_bits n)) -| _ -> failwith "Unknown type" -(** Parse the given WebAssembly module binary into an Ast.module_. At some point in the future this -should also be able to parse the textual form (TODO). *) +(** Parse the given WebAssembly module binary into an Ast.module_. At some point +in the future this should also be able to parse the textual form (TODO). *) let parse bytes = (* Optionally, use Bytes.unsafe_to_string here to avoid the copy *) let bytes_as_str = Bytes.to_string bytes in (Decode.decode "default" bytes_as_str) -(** Return true if an export is a function. *) -let match_exported_func export = match export with -| Module_export_ext(_,Ext_func n,_) -> true -| _ -> false +(** Construct an instance from a sequence of WebAssembly bytes. This clears the +previous contents of the global store *) +let instantiate_exn module_bytes : spec_instance = + let s = (make_empty_store_m ()) in + let module_ = parse module_bytes in + let m_isa = Ast_convert.convert_module (module_.it) in + (match interp_instantiate_init_m s m_isa [] () with + | (s', (RI_res_m(inst,v_exps,_))) -> (v_exps, ref s') + | (s', (RI_trap_m str)) -> raise (Eval.Trap (Source.no_region, "(Isabelle) trap: " ^ str)) + | (s', (RI_crash_m (Error_exhaustion str))) -> raise (Eval.Exhaustion (Source.no_region, "(Isabelle) call stack exhausted")) + | (s', (RI_crash_m (Error_invalid str))) -> raise (Eval.Crash (Source.no_region, "(Isabelle) error: " ^ str)) + | (s', (RI_crash_m (Error_invariant str))) -> raise (Eval.Crash (Source.no_region, "(Isabelle) error: " ^ str)) + ) -(** Extract a function from its export or fail. *) -let extract_exported_func export = match export with -| Module_export_ext(_,Ext_func n,_) -> n -| _ -> failwith "" +let instantiate module_bytes = + try Ok(instantiate_exn module_bytes) with + | _ as e -> Error(Printexc.to_string e) + +(** Retrieve the value of an export by name from a WebAssembly instance. *) +let export_exn (inst_s : spec_instance) (name : string) : ffi_export_value = + let (inst, s_ref) = inst_s in + match (e_desc (List.find (fun exp -> String.equal (e_name exp) name) inst)) with + Ext_func _ -> raise Not_found + | Ext_tab _ -> raise Not_found + | Ext_mem i -> Memory (fst (Array.get (mems (!s_ref)) (Z.to_int (integer_of_nat i)))) + | Ext_glob i -> Global (convert_from_wasm (g_val (Array.get (globs (!s_ref)) (Z.to_int (integer_of_nat i))))) + +let export inst name = + try Ok(export_exn inst name) with + | _ as e -> Error(Printexc.to_string e) (** Interpret the first exported function and return the result. Use provided parameters if they exist, otherwise use default (zeroed) values. *) -let interpret_exn module_bytes opt_params = +let interpret_legacy_exn module_bytes opt_params = let opt_params_ = Option.map (List.rev_map convert_to_wasm) opt_params in let module_ = parse module_bytes in let m_isa = Ast_convert.convert_module (module_.it) in @@ -66,12 +96,45 @@ let interpret_exn module_bytes opt_params = | (s', (RCrash (Error_exhaustion str))) -> raise (Eval.Exhaustion (Source.no_region, "(Isabelle) call stack exhausted")) | (s', (RCrash (Error_invalid str))) -> raise (Eval.Crash (Source.no_region, "(Isabelle) error: " ^ str)) | (s', (RCrash (Error_invariant str))) -> raise (Eval.Crash (Source.no_region, "(Isabelle) error: " ^ str)) - (* TODO eventually we should hash the memory state and return the hash *) ) -let interpret module_bytes opt_params = - try Ok(interpret_exn module_bytes opt_params) with +let interpret_legacy module_bytes opt_params = + try Ok(interpret_legacy_exn module_bytes opt_params) with + | _ as e -> Error(Printexc.to_string e) + +(* process an optional list of params, generating default params if necessary *) +(* TODO: this should be done in the Isabelle model *) +let get_param_vs s_ref (vs_opt :(ffi_value list) option) i = + (match vs_opt with + | None -> (match cl_m_type ((array_nth heap_cl_m (funcs !s_ref) i) ()) with Tf (t1, _) -> map bitzero t1) + | Some vs -> List.map convert_to_wasm vs) + +(** Interpret the function exported at name. Use provided +parameters if they exist, otherwise use default (zeroed) values. *) +let interpret_exn (inst_s : spec_instance) (name : string) opt_params = + (let fuel = Z.of_string "4611686018427387904" in + let max_call_depth = Z.of_string "300" in + let (inst, s_ref) = inst_s in + match (e_desc (List.find (fun exp -> String.equal (e_name exp) name) inst)) with + | Ext_func i -> + (let params = get_param_vs s_ref opt_params i in + let (s', res) = run_invoke_v_m (nat_of_integer fuel) (nat_of_integer max_call_depth) ((!s_ref), (params, i)) () in + s_ref := s'; + (match res with + | RValue vs_isa' -> List.rev_map convert_from_wasm vs_isa' + | RTrap str -> raise (Eval.Trap (Source.no_region, "(Isabelle) trap: " ^ str)) + | (RCrash (Error_exhaustion str)) -> raise (Eval.Exhaustion (Source.no_region, "(Isabelle) call stack exhausted")) + | (RCrash (Error_invalid str)) -> raise (Eval.Crash (Source.no_region, "(Isabelle) error: " ^ str)) + | (RCrash (Error_invariant str)) -> raise (Eval.Crash (Source.no_region, "(Isabelle) error: " ^ str)) + )) + | _ -> raise Not_found) + +let interpret inst name opt_params = + try Ok(interpret_exn inst name opt_params) with | _ as e -> Error(Printexc.to_string e) let () = + Callback.register "instantiate" instantiate; + Callback.register "interpret_legacy" interpret_legacy; Callback.register "interpret" interpret; + Callback.register "export" export; diff --git a/crates/fuzzing/wasm-spec-interpreter/src/lib.rs b/crates/fuzzing/wasm-spec-interpreter/src/lib.rs index 1d8008c84d..82f0cf5d50 100644 --- a/crates/fuzzing/wasm-spec-interpreter/src/lib.rs +++ b/crates/fuzzing/wasm-spec-interpreter/src/lib.rs @@ -9,9 +9,9 @@ //! - when the tools are not available, this library will panic at runtime (see //! `without_library` module). -/// Enumerate the kinds of Wasm values. +/// Enumerate the kinds of Wasm values the OCaml interpreter can handle. #[derive(Clone, Debug, PartialEq)] -pub enum Value { +pub enum SpecValue { I32(i32), I64(i64), F32(i32), @@ -19,6 +19,19 @@ pub enum Value { V128(Vec), } +/// Represents a WebAssembly export from the OCaml interpreter side. +#[allow(dead_code)] +pub enum SpecExport { + Global(SpecValue), + Memory(Vec), +} + +/// Represents a WebAssembly instance from the OCaml interpreter side. +pub struct SpecInstance { + #[cfg(feature = "has-libinterpret")] + repr: ocaml_interop::BoxRoot, +} + #[cfg(feature = "has-libinterpret")] mod with_library; #[cfg(feature = "has-libinterpret")] @@ -33,6 +46,7 @@ pub use without_library::*; #[cfg(all(fuzzing, not(feature = "has-libinterpret")))] compile_error!("The OCaml library was not built."); +/// Check if the OCaml spec interpreter bindings will work. pub fn support_compiled_in() -> bool { cfg!(feature = "has-libinterpret") } diff --git a/crates/fuzzing/wasm-spec-interpreter/src/with_library.rs b/crates/fuzzing/wasm-spec-interpreter/src/with_library.rs index e28bee3b60..15be5b5abf 100644 --- a/crates/fuzzing/wasm-spec-interpreter/src/with_library.rs +++ b/crates/fuzzing/wasm-spec-interpreter/src/with_library.rs @@ -1,73 +1,171 @@ //! Interpret WebAssembly modules using the OCaml spec interpreter. +//! //! ``` -//! # use wasm_spec_interpreter::{Value, interpret}; +//! # use wasm_spec_interpreter::{SpecValue, interpret, instantiate}; //! let module = wat::parse_file("tests/add.wat").unwrap(); -//! let parameters = vec![Value::I32(42), Value::I32(1)]; -//! let results = interpret(&module, Some(parameters)).unwrap(); -//! assert_eq!(results, &[Value::I32(43)]); +//! let instance = instantiate(&module).unwrap(); +//! let parameters = vec![SpecValue::I32(42), SpecValue::I32(1)]; +//! let results = interpret(&instance, "add", Some(parameters)).unwrap(); +//! assert_eq!(results, &[SpecValue::I32(43)]); //! ``` -use crate::Value; -use ocaml_interop::{OCamlRuntime, ToOCaml}; +//! +//! ### Warning +//! +//! The OCaml runtime is [not re-entrant]. The code below must ensure that only +//! one Rust thread is executing at a time (using the `INTERPRET` lock) or we +//! may observe `SIGSEGV` failures, e.g., while running `cargo test`. +//! +//! [not re-entrant]: +//! https://ocaml.org/manual/intfc.html#ss:parallel-execution-long-running-c-code +//! +//! ### Warning +//! +//! This module uses an unsafe approach (`OCamlRuntime::init_persistent()` + +//! `OCamlRuntime::recover_handle()`) to initializing the `OCamlRuntime` based +//! on some [discussion] with `ocaml-interop` crate authors. This approach was +//! their recommendation to resolve seeing errors like `boxroot is not setup` +//! followed by a `SIGSEGV`; this is similar to the testing approach [they use]. +//! Use this approach with care and note that it is only as safe as the OCaml +//! code running underneath. +//! +//! [discussion]: https://github.com/tezedge/ocaml-interop/issues/35 +//! [they use]: +//! https://github.com/tezedge/ocaml-interop/blob/master/testing/rust-caller/src/lib.rs + +use crate::{SpecExport, SpecInstance, SpecValue}; +use ocaml_interop::{BoxRoot, OCamlRuntime, ToOCaml}; use once_cell::sync::Lazy; use std::sync::Mutex; static INTERPRET: Lazy> = Lazy::new(|| Mutex::new(())); +/// Instantiate the WebAssembly module in the spec interpreter. +pub fn instantiate(module: &[u8]) -> Result { + let _lock = INTERPRET.lock().unwrap(); + OCamlRuntime::init_persistent(); + let ocaml_runtime = unsafe { OCamlRuntime::recover_handle() }; + + let module = module.to_boxroot(ocaml_runtime); + let instance = ocaml_bindings::instantiate(ocaml_runtime, &module); + instance.to_rust(ocaml_runtime) +} + +/// Interpret the exported function `name` with the given `parameters`. +pub fn interpret( + instance: &SpecInstance, + name: &str, + parameters: Option>, +) -> Result, String> { + let _lock = INTERPRET.lock().unwrap(); + OCamlRuntime::init_persistent(); + let ocaml_runtime = unsafe { OCamlRuntime::recover_handle() }; + + // Prepare the box-rooted parameters. + let instance = instance.to_boxroot(ocaml_runtime); + let name = name.to_string().to_boxroot(ocaml_runtime); + let parameters = parameters.to_boxroot(ocaml_runtime); + + // Interpret the function. + let results = ocaml_bindings::interpret(ocaml_runtime, &instance, &name, ¶meters); + results.to_rust(&ocaml_runtime) +} + /// Interpret the first function in the passed WebAssembly module (in Wasm form, /// currently, not WAT), optionally with the given parameters. If no parameters /// are provided, the function is invoked with zeroed parameters. -pub fn interpret(module: &[u8], opt_parameters: Option>) -> Result, String> { - // The OCaml runtime is not re-entrant - // (https://ocaml.org/manual/intfc.html#ss:parallel-execution-long-running-c-code). - // We need to make sure that only one Rust thread is executing at a time - // (using this lock) or we can observe `SIGSEGV` failures while running - // `cargo test`. +pub fn interpret_legacy( + module: &[u8], + opt_parameters: Option>, +) -> Result, String> { let _lock = INTERPRET.lock().unwrap(); - // Here we use an unsafe approach to initializing the `OCamlRuntime` based - // on the discussion in https://github.com/tezedge/ocaml-interop/issues/35. - // This was the recommendation to resolve seeing errors like `boxroot is not - // setup` followed by a `SIGSEGV`; this is similar to the testing approach - // in - // https://github.com/tezedge/ocaml-interop/blob/master/testing/rust-caller/src/lib.rs - // and is only as safe as the OCaml code running underneath. OCamlRuntime::init_persistent(); let ocaml_runtime = unsafe { OCamlRuntime::recover_handle() }; + // Parse and execute, returning results converted to Rust. let module = module.to_boxroot(ocaml_runtime); - let opt_parameters = opt_parameters.to_boxroot(ocaml_runtime); - let results = ocaml_bindings::interpret(ocaml_runtime, &module, &opt_parameters); + let results = ocaml_bindings::interpret_legacy(ocaml_runtime, &module, &opt_parameters); results.to_rust(ocaml_runtime) } +/// Retrieve the export given by `name`. +pub fn export(instance: &SpecInstance, name: &str) -> Result { + let _lock = INTERPRET.lock().unwrap(); + OCamlRuntime::init_persistent(); + let ocaml_runtime = unsafe { OCamlRuntime::recover_handle() }; + + // Prepare the box-rooted parameters. + let instance = instance.to_boxroot(ocaml_runtime); + let name = name.to_string().to_boxroot(ocaml_runtime); + + // Export the value. + let results = ocaml_bindings::export(ocaml_runtime, &instance, &name); + results.to_rust(&ocaml_runtime) +} + // Here we declare which functions we will use from the OCaml library. See // https://docs.rs/ocaml-interop/0.8.4/ocaml_interop/index.html#example. mod ocaml_bindings { use super::*; use ocaml_interop::{ - impl_conv_ocaml_variant, ocaml, OCamlBytes, OCamlInt32, OCamlInt64, OCamlList, + impl_conv_ocaml_variant, ocaml, FromOCaml, OCaml, OCamlBytes, OCamlInt32, OCamlInt64, + OCamlList, }; // Using this macro converts the enum both ways: Rust to OCaml and OCaml to // Rust. See // https://docs.rs/ocaml-interop/0.8.4/ocaml_interop/macro.impl_conv_ocaml_variant.html. impl_conv_ocaml_variant! { - Value { - Value::I32(i: OCamlInt32), - Value::I64(i: OCamlInt64), - Value::F32(i: OCamlInt32), - Value::F64(i: OCamlInt64), - Value::V128(i: OCamlBytes), + SpecValue { + SpecValue::I32(i: OCamlInt32), + SpecValue::I64(i: OCamlInt64), + SpecValue::F32(i: OCamlInt32), + SpecValue::F64(i: OCamlInt64), + SpecValue::V128(i: OCamlBytes), + } + } + + // We need to also convert the `SpecExport` enum. + impl_conv_ocaml_variant! { + SpecExport { + SpecExport::Global(i: SpecValue), + SpecExport::Memory(i: OCamlBytes), + } + } + + // We manually show `SpecInstance` how to convert itself to and from OCaml. + unsafe impl FromOCaml for SpecInstance { + fn from_ocaml(v: OCaml) -> Self { + Self { + repr: BoxRoot::new(v), + } + } + } + unsafe impl ToOCaml for SpecInstance { + fn to_ocaml<'a>(&self, cr: &'a mut OCamlRuntime) -> OCaml<'a, SpecInstance> { + BoxRoot::get(&self.repr, cr) } } // These functions must be exposed from OCaml with: - // `Callback.register "interpret" interpret` + // `Callback.register "interpret" interpret` // - // In Rust, this function becomes: + // In Rust, these functions look like: // `pub fn interpret(_: &mut OCamlRuntime, ...: OCamlRef<...>) -> BoxRoot<...>;` + // + // The `ocaml!` macro does not understand documentation, so the + // documentation is included here: + // - `instantiate`: clear the global store and instantiate a new WebAssembly + // module from bytes + // - `interpret`: given an instance, call the function exported at `name` + // - `interpret_legacy`: starting from bytes, instantiate and execute the + // first exported function + // - `export`: given an instance, get the value of the export at `name` ocaml! { - pub fn interpret(module: OCamlBytes, params: Option>) -> Result, String>; + pub fn instantiate(module: OCamlBytes) -> Result; + pub fn interpret(instance: SpecInstance, name: String, params: Option>) -> Result, String>; + pub fn interpret_legacy(module: OCamlBytes, params: Option>) -> Result, String>; + pub fn export(instance: SpecInstance, name: String) -> Result; } } @@ -86,27 +184,75 @@ mod tests { use super::*; #[test] - fn multiple() { + fn invalid_function_name() { let module = wat::parse_file("tests/add.wat").unwrap(); + let instance = instantiate(&module).unwrap(); + let results = interpret( + &instance, + "not-the-right-name", + Some(vec![SpecValue::I32(0), SpecValue::I32(0)]), + ); + assert_eq!(results, Err("Not_found".to_string())); + } - let parameters1 = Some(vec![Value::I32(42), Value::I32(1)]); - let results1 = interpret(&module, parameters1.clone()).unwrap(); - - let parameters2 = Some(vec![Value::I32(1), Value::I32(42)]); - let results2 = interpret(&module, parameters2.clone()).unwrap(); + #[test] + fn multiple_invocation() { + let module = wat::parse_file("tests/add.wat").unwrap(); + let instance = instantiate(&module).unwrap(); + let results1 = interpret( + &instance, + "add", + Some(vec![SpecValue::I32(42), SpecValue::I32(1)]), + ) + .unwrap(); + let results2 = interpret( + &instance, + "add", + Some(vec![SpecValue::I32(1), SpecValue::I32(42)]), + ) + .unwrap(); assert_eq!(results1, results2); - let parameters3 = Some(vec![Value::I32(20), Value::I32(23)]); - let results3 = interpret(&module, parameters3.clone()).unwrap(); + let results3 = interpret( + &instance, + "add", + Some(vec![SpecValue::I32(20), SpecValue::I32(23)]), + ) + .unwrap(); + assert_eq!(results2, results3); + } + #[test] + fn multiple_invocation_legacy() { + let module = wat::parse_file("tests/add.wat").unwrap(); + + let results1 = + interpret_legacy(&module, Some(vec![SpecValue::I32(42), SpecValue::I32(1)])).unwrap(); + let results2 = + interpret_legacy(&module, Some(vec![SpecValue::I32(1), SpecValue::I32(42)])).unwrap(); + assert_eq!(results1, results2); + + let results3 = + interpret_legacy(&module, Some(vec![SpecValue::I32(20), SpecValue::I32(23)])).unwrap(); assert_eq!(results2, results3); } #[test] fn oob() { let module = wat::parse_file("tests/oob.wat").unwrap(); - let results = interpret(&module, None); + let instance = instantiate(&module).unwrap(); + let results = interpret(&instance, "oob", None); + assert_eq!( + results, + Err("Error(_, \"(Isabelle) trap: load\")".to_string()) + ); + } + + #[test] + fn oob_legacy() { + let module = wat::parse_file("tests/oob.wat").unwrap(); + let results = interpret_legacy(&module, None); assert_eq!( results, Err("Error(_, \"(Isabelle) trap: load\")".to_string()) @@ -116,15 +262,33 @@ mod tests { #[test] fn simd_not() { let module = wat::parse_file("tests/simd_not.wat").unwrap(); + let instance = instantiate(&module).unwrap(); - let parameters = Some(vec![Value::V128(vec![ + let parameters = Some(vec![SpecValue::V128(vec![ 0, 255, 0, 0, 255, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, ])]); - let results = interpret(&module, parameters.clone()).unwrap(); + let results = interpret(&instance, "simd_not", parameters).unwrap(); assert_eq!( results, - vec![Value::V128(vec![ + vec![SpecValue::V128(vec![ + 255, 0, 255, 255, 0, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 255 + ])] + ); + } + + #[test] + fn simd_not_legacy() { + let module = wat::parse_file("tests/simd_not.wat").unwrap(); + + let parameters = Some(vec![SpecValue::V128(vec![ + 0, 255, 0, 0, 255, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, + ])]); + let results = interpret_legacy(&module, parameters).unwrap(); + + assert_eq!( + results, + vec![SpecValue::V128(vec![ 255, 0, 255, 255, 0, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 255 ])] ); @@ -134,10 +298,57 @@ mod tests { #[test] fn order_of_params() { let module = wat::parse_file("tests/shr_s.wat").unwrap(); + let instance = instantiate(&module).unwrap(); - let parameters = Some(vec![Value::I32(1795123818), Value::I32(-2147483648)]); - let results = interpret(&module, parameters.clone()).unwrap(); + let parameters = Some(vec![ + SpecValue::I32(1795123818), + SpecValue::I32(-2147483648), + ]); + let results = interpret(&instance, "test", parameters).unwrap(); - assert_eq!(results, vec![Value::I32(1795123818)]); + assert_eq!(results, vec![SpecValue::I32(1795123818)]); + } + + // See issue https://github.com/bytecodealliance/wasmtime/issues/4671. + #[test] + fn order_of_params_legacy() { + let module = wat::parse_file("tests/shr_s.wat").unwrap(); + + let parameters = Some(vec![ + SpecValue::I32(1795123818), + SpecValue::I32(-2147483648), + ]); + let results = interpret_legacy(&module, parameters).unwrap(); + + assert_eq!(results, vec![SpecValue::I32(1795123818)]); + } + + #[test] + fn load_store_and_export() { + let module = wat::parse_file("tests/memory.wat").unwrap(); + let instance = instantiate(&module).unwrap(); + + // Store 42 at offset 4. + let _ = interpret( + &instance, + "store_i32", + Some(vec![SpecValue::I32(4), SpecValue::I32(42)]), + ); + + // Load an i32 from offset 4. + let loaded = interpret(&instance, "load_i32", Some(vec![SpecValue::I32(4)])); + + // Check stored value was retrieved. + assert_eq!(loaded.unwrap(), vec![SpecValue::I32(42)]); + + // Retrieve the memory exported with name "mem" and check that the + // 32-bit value at byte offset 4 of memory is 42. + let export = export(&instance, "mem"); + match export.unwrap() { + SpecExport::Global(_) => panic!("incorrect export"), + SpecExport::Memory(m) => { + assert_eq!(&m[0..10], [0, 0, 0, 0, 42, 0, 0, 0, 0, 0]); + } + } } } diff --git a/crates/fuzzing/wasm-spec-interpreter/src/without_library.rs b/crates/fuzzing/wasm-spec-interpreter/src/without_library.rs index d33bccfb46..48a8a7cdab 100644 --- a/crates/fuzzing/wasm-spec-interpreter/src/without_library.rs +++ b/crates/fuzzing/wasm-spec-interpreter/src/without_library.rs @@ -2,14 +2,39 @@ //! `lib.rs`. //! //! ```should_panic -//! # use wasm_spec_interpreter::interpret; -//! let _ = interpret(&[], Some(vec![])); +//! # use wasm_spec_interpreter::instantiate; +//! let _ = instantiate(&[]); //! ``` -use crate::Value; +use crate::{SpecExport, SpecInstance, SpecValue}; #[allow(dead_code)] -pub fn interpret(_module: &[u8], _parameters: Option>) -> Result, String> { +pub fn instantiate(_module: &[u8]) -> Result { + fail_at_runtime() +} + +#[allow(dead_code)] +pub fn interpret( + _instance: &SpecInstance, + _name: &str, + _parameters: Option>, +) -> Result, String> { + fail_at_runtime() +} + +#[allow(dead_code)] +pub fn interpret_legacy( + _module: &[u8], + _parameters: Option>, +) -> Result, String> { + fail_at_runtime() +} + +pub fn export(_instance: &SpecInstance, _name: &str) -> Result { + fail_at_runtime() +} + +fn fail_at_runtime() -> ! { panic!( "wasm-spec-interpreter was built without its Rust-to-OCaml shim \ library; re-compile with the dependencies listed in its README.md." diff --git a/crates/fuzzing/wasm-spec-interpreter/tests/memory.wat b/crates/fuzzing/wasm-spec-interpreter/tests/memory.wat new file mode 100644 index 0000000000..d0319aecbe --- /dev/null +++ b/crates/fuzzing/wasm-spec-interpreter/tests/memory.wat @@ -0,0 +1,12 @@ +(module + (memory (export "mem") 1 1) + + (func (export "load_i32") (param $a i32) (result i32) + local.get $a + i32.load) + + (func (export "store_i32") (param $a i32) (param $b i32) + local.get $a + local.get $b + i32.store) +) diff --git a/fuzz/fuzz_targets/differential.rs b/fuzz/fuzz_targets/differential.rs index 53c376b967..5ba5ea06d2 100644 --- a/fuzz/fuzz_targets/differential.rs +++ b/fuzz/fuzz_targets/differential.rs @@ -125,13 +125,6 @@ fn run(data: &[u8]) -> Result<()> { // One side succeeded and one side failed, that means a bug happened! (l, r) => { - // FIXME(#4852): the spec interpreter doesn't instantiate as part of - // the instantiate step so if wasmtime failed and the spec succeeded - // that's ok. This clause should be removed once that issue is - // fixed. - if l.is_ok() && lhs.name() == "spec" { - return Ok(()); - } panic!( "failed to instantiate only one side: {:?} != {:?}", l.err(), @@ -172,15 +165,8 @@ fn run(data: &[u8]) -> Result<()> { break 'outer; } - // FIXME(#4852): the spec interpreter only supports one execution - // right now because each execution re-instantiates the module in - // its bindings. This should be removed once that issue is fixed. - if lhs.name() == "spec" { - break 'outer; - } - // We evaluate the same function with different arguments until we - // hit a predetermined limit or we run out of unstructured data--it + // Hit a predetermined limit or we run out of unstructured data--it // does not make sense to re-evaluate the same arguments over and // over. if invocations > NUM_INVOCATIONS || u.is_empty() {