Skip to content

Commit

Permalink
Add fuzzing based on serde_json
Browse files Browse the repository at this point in the history
This test ensures that we can parse anything that serde_json can
produce, which *ought* to ensure reasonable coverage?
  • Loading branch information
DeCarabas committed Aug 13, 2024
1 parent 43f6b75 commit 35dcf93
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 0 deletions.
40 changes: 40 additions & 0 deletions fuzz/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ".."
Expand All @@ -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
77 changes: 77 additions & 0 deletions fuzz/fuzz_targets/json_only_valid_serde.rs
Original file line number Diff line number Diff line change
@@ -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<Self, Error> {
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<usize>) {
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<String, TestInput>),
Array(Vec<TestInput>),
}

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());
});

0 comments on commit 35dcf93

Please sign in to comment.