diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index c6243e0..463a394 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -43,6 +43,9 @@ name = "arbitrary" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "autocfg" @@ -247,6 +250,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dlib" version = "0.5.2" @@ -335,8 +349,10 @@ dependencies = [ name = "fwd-fuzz" version = "0.0.0" dependencies = [ + "arbitrary", "fwd", "libfuzzer-sys", + "serde_json", ] [[package]] @@ -433,6 +449,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + [[package]] name = "jobserver" version = "0.1.32" @@ -817,6 +839,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + [[package]] name = "scoped-tls" version = "1.0.1" @@ -849,6 +877,18 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.124" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66ad62847a56b3dba58cc891acd13884b9c61138d330c0d7b6181713d4fce38d" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "signal-hook" version = "0.3.17" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 0eeeb41..bc252d7 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -8,7 +8,9 @@ edition = "2021" cargo-fuzz = true [dependencies] +arbitrary = { version = "1.3.2", features = ["derive"] } libfuzzer-sys = "0.4" +serde_json = "1.0.124" [dependencies.fwd] path = ".." @@ -19,3 +21,10 @@ path = "fuzz_targets/json_raw_input.rs" test = false doc = false bench = false + +[[bin]] +name = "json_only_valid_serde" +path = "fuzz_targets/json_only_valid_serde.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/json_only_valid_serde.rs b/fuzz/fuzz_targets/json_only_valid_serde.rs new file mode 100644 index 0000000..567642f --- /dev/null +++ b/fuzz/fuzz_targets/json_only_valid_serde.rs @@ -0,0 +1,77 @@ +#![no_main] + +use arbitrary::{Arbitrary, Error, Unstructured}; +use libfuzzer_sys::fuzz_target; +use std::collections::HashMap; + +extern crate fwd; +use fwd::server::refresh::docker::JsonValue; + +/// InputNumber is a JSON number, i.e., a finite 64-bit floating point value +/// that is not NaN. We need to define our own little wrapper here so that we +/// can convince Arbitrary to only make finite f64s. +/// +/// Ideally we would actually wrap serde_json::Number but there are rules +/// about mixing 3rd party traits with 3rd party types. +#[derive(Debug, PartialEq)] +struct InputNumber(f64); + +impl<'a> Arbitrary<'a> for InputNumber { + fn arbitrary(u: &mut Unstructured<'a>) -> Result { + let value = f64::arbitrary(u)?; + if value.is_finite() { + Ok(InputNumber(value)) + } else { + Err(Error::IncorrectFormat) // REJECT + } + } + + #[inline] + fn size_hint(depth: usize) -> (usize, Option) { + f64::size_hint(depth) + } +} + +/// TestInput is basically serde_json::Value, except (a) it has a HashMap and +/// not serde_json's special `Map` structure, and (b) it has `InputNumber` +/// instead of `json_serde::Number` for reasons described above. +#[derive(Debug, PartialEq, Arbitrary)] +enum TestInput { + Null, + Bool(bool), + Number(InputNumber), + String(String), + Object(HashMap), + Array(Vec), +} + +fn convert(value: &TestInput) -> serde_json::Value { + match value { + TestInput::Null => serde_json::Value::Null, + TestInput::Bool(b) => serde_json::Value::Bool(*b), + TestInput::Number(n) => serde_json::Value::Number( + serde_json::Number::from_f64(n.0).expect("Unable to make an f64"), + ), + TestInput::String(s) => serde_json::Value::String(s.clone()), + TestInput::Object(o) => { + let mut out = serde_json::map::Map::new(); + for (k, v) in o.into_iter() { + out.insert(k.clone(), convert(v)); + } + serde_json::Value::Object(out) + } + TestInput::Array(v) => { + serde_json::Value::Array(v.into_iter().map(convert).collect()) + } + } +} + +fuzz_target!(|data: TestInput| { + // Convert the arbitrary TestInput into an arbitrary serde_json::Value, + // then use serde_json to write out arbitrary JSON. + let converted = convert(&data).to_string(); + + // Parse the JSON that serde_json produced. This fuzz test should ensure + // that we can parse anything that serde_json can produce. + let _ = JsonValue::parse(converted.as_bytes()); +});