Fuzzing against verified fork of spec interpreter (#3843)

* Revert "Remove spec interpreter fuzz target temporarily (#3399)"

This reverts commit 25d3fa4d7b.

* add support for differential fuzzing against verified OCaml interpreter

* formatting

* comments

* fix missing dep case

* fix build error

* fix unit tests?

* restore previous differential_v8 max_table config

* attempt: add OCaml deps

* fix interpeter github repo

* fix spec repo url

* fix zarith package

* fix unit test
This commit is contained in:
Conrad Watt
2022-03-01 18:01:46 +00:00
committed by GitHub
parent ceab1e62fa
commit 98ef18a22a
15 changed files with 142 additions and 59 deletions

View File

@@ -10,8 +10,8 @@ use std::{env, path::PathBuf, process::Command};
const LIB_NAME: &'static str = "interpret";
const OCAML_DIR: &'static str = "ocaml";
const SPEC_DIR: &'static str = "ocaml/spec";
const SPEC_REPOSITORY: &'static str = "https://github.com/bytecodealliance/wasm-spec-mirror";
const SPEC_REPOSITORY_BRANCH: &'static str = "fuzzing";
const SPEC_REPOSITORY: &'static str = "https://github.com/conrad-watt/spec";
const SPEC_REPOSITORY_BRANCH: &'static str = "wasmtime_fuzzing";
fn main() {
if cfg!(feature = "build-libinterpret") {

View File

@@ -11,14 +11,19 @@ SPEC_DIR := spec/interpreter
SPEC_BUILD_DIR := $(SPEC_DIR)/_build
SPEC_LIB := $(SPEC_BUILD_DIR)/wasm.cmxa
# A space-separated list of paths that the linker will use to search for libgmp.
# Override with `make LIBGMP_PATHS=...`.
LIBGMP_PATHS := /usr/lib /usr/lib/x86_64-linux-gnu
PKGS = zarith
# Build and package the static library, `libinterpret.a`.
$(BUILD_DIR)/libinterpret.a: $(BUILD_DIR)/interpret.lib.o
ar qs $@ $^
$(BUILD_DIR)/interpret.lib.o: $(SPEC_LIB) $(BUILD_DIR)/interpret.cmx
ocamlopt $(OCAML_FLAGS) -I $(SPEC_BUILD_DIR) -o $@ -output-complete-obj $^
ocamlfind ocamlopt $(OCAML_FLAGS) -I $(SPEC_BUILD_DIR) -o $@ -output-complete-obj $^ -linkpkg $(PKGS:%=-package %) -cclib "$(LIBGMP_PATHS:%=-L%)"
$(BUILD_DIR)/interpret.cmx: interpret.ml $(SPEC_BUILD_DIR) $(BUILD_DIR)
ocamlopt $(OCAML_FLAGS) -I $(SPEC_BUILD_DIR) -o $@ -c -impl $<
ocamlfind ocamlopt $(OCAML_FLAGS) -I $(SPEC_BUILD_DIR) -o $@ -c -impl $< -linkpkg $(PKGS:%=-package %)
$(BUILD_DIR):
mkdir -p $@

View File

@@ -1,7 +1,10 @@
This directory contains the necessary parts for building a library with FFI
access to the Wasm spec interpreter. Its major parts:
- `spec`: the Wasm spec code as a Git submodule (you may need to retrieve it:
`git clone https://github.com/bytecodealliance/wasm-spec-mirror).
`git clone https://github.com/conrad-watt/spec/tree/wasmtime_fuzzing).
- `interpret.ml`: a shim layer for calling the Wasm spec code and exposing it
for FFI access
- `Makefile`: the steps for gluing these pieces together into a static library
Note: the makefile must be configured with the path to libgmp. See LIBGMP_PATHS
in the makefile.

View File

@@ -7,6 +7,7 @@ understand this better, see:
(* 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` *)
@@ -17,18 +18,18 @@ type ffi_value =
| F64 of int64
(** Helper for converting the FFI values to their spec interpreter type. *)
let convert_to_wasm (v: ffi_value) : Values.value = match v with
| I32 n -> Values.Num (I32 n)
| I64 n -> Values.Num (I64 n)
| F32 n -> Values.Num (F32 (F32.of_bits n))
| F64 n -> Values.Num (F64 (F64.of_bits n))
let convert_to_wasm (v: ffi_value) : v = match v with
| I32 n -> ConstInt32 (I32_impl_abs n)
| I64 n -> ConstInt64 (I64_impl_abs n)
| F32 n -> ConstFloat32 (F32.of_bits n)
| F64 n -> ConstFloat64 (F64.of_bits n)
(** Helper for converting the spec interpreter values to their FFI type. *)
let convert_from_wasm (v: Values.value) : ffi_value = match v with
| Values.Num (I32 n) -> I32 n
| Values.Num (I64 n) -> I64 n
| Values.Num (F32 n) -> F32 (F32.to_bits n)
| Values.Num (F64 n) -> F64 (F64.to_bits n)
let convert_from_wasm (v: v) : ffi_value = match v with
| (ConstInt32 (I32_impl_abs n)) -> I32 n
| (ConstInt64 (I64_impl_abs n)) -> I64 n
| (ConstFloat32 n) -> F32 (F32.to_bits n)
| (ConstFloat64 n) -> F64 (F64.to_bits n)
| _ -> failwith "Unknown type"
(** Parse the given WebAssembly module binary into an Ast.module_. At some point in the future this
@@ -36,30 +37,37 @@ 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
(Decode.decode "default" bytes_as_str)
(** Return true if an export is a function. *)
let match_exported_func export = match export with
| (_, Instance.ExternFunc(func)) -> true
| Module_export_ext(_,Ext_func n,_) -> true
| _ -> false
(** Extract a function from its export or fail. *)
let extract_exported_func export = match export with
| (_, Instance.ExternFunc(func)) -> func
| Module_export_ext(_,Ext_func n,_) -> n
| _ -> failwith ""
(** Interpret the first exported function with the given parameters and return the result. *)
let interpret_exn module_bytes params =
let params' = List.map convert_to_wasm params in
(** 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 opt_params_ = Option.map (List.map convert_to_wasm) opt_params in
let module_ = parse module_bytes in
let instance = Eval.init module_ [] in
let func = extract_exported_func (List.find match_exported_func instance.exports) in
let returns = Eval.invoke func params' in
let returns' = List.map convert_from_wasm returns in
returns' (* TODO eventually we should hash the memory state and return the hash *)
let m_isa = Ast_convert.convert_module (module_.it) in
let fuel = Z.of_string "4611686018427387904" in
let max_call_depth = Z.of_string "300" in
(match run_fuzz (nat_of_integer fuel) (nat_of_integer max_call_depth) (make_empty_store_m ()) m_isa [] opt_params_ () with
| (s', RValue vs_isa') -> List.map convert_from_wasm (List.rev vs_isa')
| (s', RTrap str) -> raise (Eval.Trap (Source.no_region, "(Isabelle) trap: " ^ str))
| (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 params =
try Ok(interpret_exn module_bytes params) with
let interpret module_bytes opt_params =
try Ok(interpret_exn module_bytes opt_params) with
| _ as e -> Error(Printexc.to_string e)
let () =

View File

@@ -28,7 +28,6 @@ mod without_library;
#[cfg(not(feature = "has-libinterpret"))]
pub use without_library::*;
// FIXME(#3251) should re-enable once spec interpreter won't time out
// If the user is fuzzing`, we expect the OCaml library to have been built.
// #[cfg(all(fuzzing, not(feature = "has-libinterpret")))]
// compile_error!("The OCaml library was not built.");
#[cfg(all(fuzzing, not(feature = "has-libinterpret")))]
compile_error!("The OCaml library was not built.");

View File

@@ -3,7 +3,7 @@
//! # use wasm_spec_interpreter::{Value, interpret};
//! let module = wat::parse_file("tests/add.wat").unwrap();
//! let parameters = vec![Value::I32(42), Value::I32(1)];
//! let results = interpret(&module, parameters).unwrap();
//! let results = interpret(&module, Some(parameters)).unwrap();
//! assert_eq!(results, &[Value::I32(43)]);
//! ```
use crate::Value;
@@ -16,8 +16,9 @@ lazy_static! {
}
/// Interpret the first function in the passed WebAssembly module (in Wasm form,
/// currently, not WAT) with the given parameters.
pub fn interpret(module: &[u8], parameters: Vec<Value>) -> Result<Vec<Value>, String> {
/// 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
@@ -35,8 +36,9 @@ pub fn interpret(module: &[u8], parameters: Vec<Value>) -> Result<Vec<Value>, St
let ocaml_runtime = unsafe { OCamlRuntime::recover_handle() };
// Parse and execute, returning results converted to Rust.
let module = module.to_boxroot(ocaml_runtime);
let parameters = parameters.to_boxroot(ocaml_runtime);
let results = ocaml_bindings::interpret(ocaml_runtime, &module, &parameters);
let opt_parameters = opt_parameters.to_boxroot(ocaml_runtime);
let results = ocaml_bindings::interpret(ocaml_runtime, &module, &opt_parameters);
results.to_rust(ocaml_runtime)
}
@@ -66,7 +68,7 @@ mod ocaml_bindings {
// In Rust, this function becomes:
// `pub fn interpret(_: &mut OCamlRuntime, ...: OCamlRef<...>) -> BoxRoot<...>;`
ocaml! {
pub fn interpret(module: OCamlBytes, params: OCamlList<Value>) -> Result<OCamlList<Value>, String>;
pub fn interpret(module: OCamlBytes, params: Option<OCamlList<Value>>) -> Result<OCamlList<Value>, String>;
}
}
@@ -77,22 +79,28 @@ mod tests {
#[test]
fn multiple() {
let module = wat::parse_file("tests/add.wat").unwrap();
let parameters = vec![Value::I32(42), Value::I32(1)];
let results1 = interpret(&module, parameters.clone()).unwrap();
let results2 = interpret(&module, parameters.clone()).unwrap();
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();
assert_eq!(results1, results2);
let results3 = interpret(&module, parameters).unwrap();
let parameters3 = Some(vec![Value::I32(20), Value::I32(23)]);
let results3 = interpret(&module, parameters3.clone()).unwrap();
assert_eq!(results2, results3);
}
#[test]
fn oob() {
let module = wat::parse_file("tests/oob.wat").unwrap();
let parameters = vec![];
let results = interpret(&module, parameters);
let results = interpret(&module, None);
assert_eq!(
results,
Err("Error(_, \"out of bounds memory access\")".to_string())
Err("Error(_, \"(Isabelle) trap: load\")".to_string())
);
}
}

View File

@@ -3,13 +3,13 @@
//!
//! ```should_panic
//! # use wasm_spec_interpreter::interpret;
//! let _ = interpret(&[], vec![]);
//! let _ = interpret(&[], Some(vec![]));
//! ```
use crate::Value;
#[allow(dead_code)]
pub fn interpret(_module: &[u8], _parameters: Vec<Value>) -> Result<Vec<Value>, String> {
pub fn interpret(_module: &[u8], _parameters: Option<Vec<Value>>) -> Result<Vec<Value>, String> {
panic!(
"wasm-spec-interpreter was built without its Rust-to-OCaml shim \
library; re-compile with the dependencies listed in its README.md."

View File

@@ -2,4 +2,5 @@
(memory (;0;) 0 0)
(func (export "oob")
i32.const 42
f32.load align=1))
f32.load align=1
return))