fuzz: improve the spec interpreter (#4881)
* fuzz: improve the API of the `wasm-spec-interpreter` crate This change addresses key parts of #4852 by improving the bindings to the OCaml spec interpreter. The new API allows users to `instantiate` a module, `interpret` named functions on that instance, and `export` globals and memories from that instance. This currently leaves the existing implementation ("instantiate and interpret the first function in a module") present under a new name: `interpret_legacy`. * fuzz: adapt the differential spec engine to the new API This removes the legacy uses in the differential spec engine, replacing them with the new `instantiate`-`interpret`-`export` API from the `wasm-spec-interpreter` crate. * fix: make instance access thread-safe This changes the OCaml-side definition of the instance so that each instance carries round a reference to a "global store" that's specific to that instantiation. Because everything is updated by reference there should be no visible behavioural change on the Rust side, apart from everything suddenly being thread-safe (modulo the fact that access to the OCaml runtime still needs to be locked). This fix will need to be generalised slightly in future if we want to allow multiple modules to be instantiated in the same store. Co-authored-by: conrad-watt <cnrdwtt@gmail.com> Co-authored-by: Alex Crichton <alex@alexcrichton.com>
This commit is contained in:
@@ -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<Box<dyn DiffInstance>> {
|
||||
// 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<u8>,
|
||||
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<Option<Vec<DiffValue>>> {
|
||||
// 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<DiffValue> {
|
||||
// TODO: should implement this
|
||||
None
|
||||
fn get_global(&mut self, name: &str, _ty: DiffValueType) -> Option<DiffValue> {
|
||||
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<Vec<u8>> {
|
||||
// TODO: should implement this
|
||||
None
|
||||
fn get_memory(&mut self, name: &str, _shared: bool) -> Option<Vec<u8>> {
|
||||
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<DiffValue> for Value {
|
||||
impl Into<DiffValue> 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()))
|
||||
}
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<u8>),
|
||||
}
|
||||
|
||||
/// Represents a WebAssembly export from the OCaml interpreter side.
|
||||
#[allow(dead_code)]
|
||||
pub enum SpecExport {
|
||||
Global(SpecValue),
|
||||
Memory(Vec<u8>),
|
||||
}
|
||||
|
||||
/// Represents a WebAssembly instance from the OCaml interpreter side.
|
||||
pub struct SpecInstance {
|
||||
#[cfg(feature = "has-libinterpret")]
|
||||
repr: ocaml_interop::BoxRoot<SpecInstance>,
|
||||
}
|
||||
|
||||
#[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")
|
||||
}
|
||||
|
||||
@@ -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<Mutex<()>> = Lazy::new(|| Mutex::new(()));
|
||||
|
||||
/// Instantiate the WebAssembly module in the spec interpreter.
|
||||
pub fn instantiate(module: &[u8]) -> Result<SpecInstance, String> {
|
||||
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<Vec<SpecValue>>,
|
||||
) -> Result<Vec<SpecValue>, 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<Vec<Value>>) -> Result<Vec<Value>, 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<Vec<SpecValue>>,
|
||||
) -> Result<Vec<SpecValue>, 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<SpecExport, 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);
|
||||
|
||||
// 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<SpecInstance> for SpecInstance {
|
||||
fn from_ocaml(v: OCaml<SpecInstance>) -> Self {
|
||||
Self {
|
||||
repr: BoxRoot::new(v),
|
||||
}
|
||||
}
|
||||
}
|
||||
unsafe impl ToOCaml<SpecInstance> 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<OCamlList<Value>>) -> Result<OCamlList<Value>, String>;
|
||||
pub fn instantiate(module: OCamlBytes) -> Result<SpecInstance, String>;
|
||||
pub fn interpret(instance: SpecInstance, name: String, params: Option<OCamlList<SpecValue>>) -> Result<OCamlList<SpecValue>, String>;
|
||||
pub fn interpret_legacy(module: OCamlBytes, params: Option<OCamlList<SpecValue>>) -> Result<OCamlList<SpecValue>, String>;
|
||||
pub fn export(instance: SpecInstance, name: String) -> Result<SpecExport, String>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Vec<Value>>) -> Result<Vec<Value>, String> {
|
||||
pub fn instantiate(_module: &[u8]) -> Result<SpecInstance, String> {
|
||||
fail_at_runtime()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn interpret(
|
||||
_instance: &SpecInstance,
|
||||
_name: &str,
|
||||
_parameters: Option<Vec<SpecValue>>,
|
||||
) -> Result<Vec<SpecValue>, String> {
|
||||
fail_at_runtime()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn interpret_legacy(
|
||||
_module: &[u8],
|
||||
_parameters: Option<Vec<SpecValue>>,
|
||||
) -> Result<Vec<SpecValue>, String> {
|
||||
fail_at_runtime()
|
||||
}
|
||||
|
||||
pub fn export(_instance: &SpecInstance, _name: &str) -> Result<SpecExport, String> {
|
||||
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."
|
||||
|
||||
12
crates/fuzzing/wasm-spec-interpreter/tests/memory.wat
Normal file
12
crates/fuzzing/wasm-spec-interpreter/tests/memory.wat
Normal file
@@ -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)
|
||||
)
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user