implement fuzzing for component types (#4537)
This addresses #4307. For the static API we generate 100 arbitrary test cases at build time, each of which includes 0-5 parameter types, a result type, and a WAT fragment containing an imported function and an exported function. The exported function calls the imported function, which is implemented by the host. At runtime, the fuzz test selects a test case at random and feeds it zero or more sets of arbitrary parameters and results, checking that values which flow host-to-guest and guest-to-host make the transition unchanged. The fuzz test for the dynamic API follows a similar pattern, the only difference being that test cases are generated at runtime. Signed-off-by: Joel Dice <joel.dice@fermyon.com>
This commit is contained in:
@@ -9,6 +9,8 @@ publish = false
|
||||
cargo-fuzz = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = { version = "1.0.19" }
|
||||
arbitrary = { version = "1.1.0", features = ["derive"] }
|
||||
cranelift-codegen = { path = "../cranelift/codegen" }
|
||||
cranelift-reader = { path = "../cranelift/reader" }
|
||||
cranelift-wasm = { path = "../cranelift/wasm" }
|
||||
@@ -19,6 +21,16 @@ libfuzzer-sys = "0.4.0"
|
||||
target-lexicon = "0.12"
|
||||
wasmtime = { path = "../crates/wasmtime" }
|
||||
wasmtime-fuzzing = { path = "../crates/fuzzing" }
|
||||
component-test-util = { path = "../crates/misc/component-test-util" }
|
||||
component-fuzz-util = { path = "../crates/misc/component-fuzz-util" }
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = "1.0.19"
|
||||
proc-macro2 = "1.0"
|
||||
arbitrary = { version = "1.1.0", features = ["derive"] }
|
||||
rand = { version = "0.8.0" }
|
||||
quote = "1.0"
|
||||
component-fuzz-util = { path = "../crates/misc/component-fuzz-util" }
|
||||
|
||||
[features]
|
||||
default = ['fuzz-spec-interpreter']
|
||||
@@ -102,3 +114,9 @@ name = "instantiate-many"
|
||||
path = "fuzz_targets/instantiate-many.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
[[bin]]
|
||||
name = "component_api"
|
||||
path = "fuzz_targets/component_api.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
144
fuzz/build.rs
Normal file
144
fuzz/build.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
fn main() -> anyhow::Result<()> {
|
||||
component::generate_static_api_tests()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
mod component {
|
||||
use anyhow::{anyhow, Context, Error, Result};
|
||||
use arbitrary::{Arbitrary, Unstructured};
|
||||
use component_fuzz_util::{self, Declarations, TestCase};
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
use rand::rngs::StdRng;
|
||||
use rand::{Rng, SeedableRng};
|
||||
use std::env;
|
||||
use std::fmt::Write;
|
||||
use std::fs;
|
||||
use std::iter;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
pub fn generate_static_api_tests() -> Result<()> {
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
let out_dir = PathBuf::from(
|
||||
env::var_os("OUT_DIR").expect("The OUT_DIR environment variable must be set"),
|
||||
);
|
||||
|
||||
let mut out = String::new();
|
||||
write_static_api_tests(&mut out)?;
|
||||
|
||||
let output = out_dir.join("static_component_api.rs");
|
||||
fs::write(&output, out)?;
|
||||
|
||||
drop(Command::new("rustfmt").arg(&output).status());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_static_api_tests(out: &mut String) -> Result<()> {
|
||||
let seed = if let Ok(seed) = env::var("WASMTIME_FUZZ_SEED") {
|
||||
seed.parse::<u64>()
|
||||
.with_context(|| anyhow!("expected u64 in WASMTIME_FUZZ_SEED"))?
|
||||
} else {
|
||||
StdRng::from_entropy().gen()
|
||||
};
|
||||
|
||||
eprintln!(
|
||||
"using seed {seed} (set WASMTIME_FUZZ_SEED={seed} in your environment to reproduce)"
|
||||
);
|
||||
|
||||
let mut rng = StdRng::seed_from_u64(seed);
|
||||
|
||||
const TEST_CASE_COUNT: usize = 100;
|
||||
|
||||
let mut tests = TokenStream::new();
|
||||
|
||||
let name_counter = &mut 0;
|
||||
|
||||
let mut declarations = TokenStream::new();
|
||||
|
||||
for index in 0..TEST_CASE_COUNT {
|
||||
let mut bytes = Vec::new();
|
||||
|
||||
let case = loop {
|
||||
let count = rng.gen_range(1000..2000);
|
||||
bytes.extend(iter::repeat_with(|| rng.gen::<u8>()).take(count));
|
||||
|
||||
match TestCase::arbitrary(&mut Unstructured::new(&bytes)) {
|
||||
Ok(case) => break case,
|
||||
Err(arbitrary::Error::NotEnoughData) => (),
|
||||
Err(error) => return Err(Error::from(error)),
|
||||
}
|
||||
};
|
||||
|
||||
let Declarations {
|
||||
types,
|
||||
params,
|
||||
result,
|
||||
import_and_export,
|
||||
} = case.declarations();
|
||||
|
||||
let test = format_ident!("static_api_test{}", case.params.len());
|
||||
|
||||
let rust_params = case
|
||||
.params
|
||||
.iter()
|
||||
.map(|ty| {
|
||||
let ty = component_fuzz_util::rust_type(&ty, name_counter, &mut declarations);
|
||||
quote!(#ty,)
|
||||
})
|
||||
.collect::<TokenStream>();
|
||||
|
||||
let rust_result =
|
||||
component_fuzz_util::rust_type(&case.result, name_counter, &mut declarations);
|
||||
|
||||
let test = quote!(#index => component_types::#test::<#rust_params #rust_result>(
|
||||
input,
|
||||
&Declarations {
|
||||
types: #types.into(),
|
||||
params: #params.into(),
|
||||
result: #result.into(),
|
||||
import_and_export: #import_and_export.into()
|
||||
}
|
||||
),);
|
||||
|
||||
tests.extend(test);
|
||||
}
|
||||
|
||||
let module = quote! {
|
||||
#[allow(unused_imports)]
|
||||
fn static_component_api_target(input: &mut arbitrary::Unstructured) -> arbitrary::Result<()> {
|
||||
use anyhow::Result;
|
||||
use arbitrary::{Unstructured, Arbitrary};
|
||||
use component_test_util::{self, Float32, Float64};
|
||||
use component_fuzz_util::Declarations;
|
||||
use std::sync::{Arc, Once};
|
||||
use wasmtime::component::{ComponentType, Lift, Lower};
|
||||
use wasmtime_fuzzing::generators::component_types;
|
||||
|
||||
const SEED: u64 = #seed;
|
||||
|
||||
static ONCE: Once = Once::new();
|
||||
|
||||
ONCE.call_once(|| {
|
||||
eprintln!(
|
||||
"Seed {SEED} was used to generate static component API fuzz tests.\n\
|
||||
Set WASMTIME_FUZZ_SEED={SEED} in your environment at build time to reproduce."
|
||||
);
|
||||
});
|
||||
|
||||
#declarations
|
||||
|
||||
match input.int_in_range(0..=(#TEST_CASE_COUNT-1))? {
|
||||
#tests
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
write!(out, "{module}")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
22
fuzz/fuzz_targets/component_api.rs
Normal file
22
fuzz/fuzz_targets/component_api.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
#![no_main]
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use wasmtime_fuzzing::oracles;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/static_component_api.rs"));
|
||||
|
||||
#[allow(unused_imports)]
|
||||
fn target(input: &mut arbitrary::Unstructured) -> arbitrary::Result<()> {
|
||||
if input.arbitrary()? {
|
||||
static_component_api_target(input)
|
||||
} else {
|
||||
oracles::dynamic_component_api_target(input)
|
||||
}
|
||||
}
|
||||
|
||||
fuzz_target!(|bytes: &[u8]| {
|
||||
match target(&mut arbitrary::Unstructured::new(bytes)) {
|
||||
Ok(()) | Err(arbitrary::Error::NotEnoughData) => (),
|
||||
Err(error) => panic!("{}", error),
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user