diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 39315c4..4ffa551 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -169,3 +169,23 @@ jobs: uses: dtolnay/rust-toolchain@stable - run: rustup target add wasm32-unknown-unknown - run: cargo check --target wasm32-unknown-unknown + + Fuzz: + name: Check Fuzz + needs: Prepare + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - name: "Checkout repo" + uses: actions/checkout@v4 + + - name: "Select nightly toolchain" + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ needs.Prepare.outputs.nightly_version }} + + - name: "Check fuzz crate" + run: | + cargo check --manifest-path=fuzz/Cargo.toml + diff --git a/Cargo.lock b/Cargo.lock index 485cff6..78ba77b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,6 +92,12 @@ version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bech32" version = "0.11.0" @@ -179,6 +185,7 @@ version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ + "jobserver", "libc", ] @@ -350,6 +357,15 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.69" @@ -365,6 +381,16 @@ version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +[[package]] +name = "libfuzzer-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "log" version = "0.4.22" @@ -653,7 +679,7 @@ name = "simplicityhl" version = "0.3.0" dependencies = [ "arbitrary", - "base64", + "base64 0.21.3", "clap", "either", "getrandom", @@ -666,6 +692,18 @@ dependencies = [ "simplicity-lang", ] +[[package]] +name = "simplicityhl-fuzz" +version = "0.0.0" +dependencies = [ + "arbitrary", + "base64 0.22.1", + "itertools", + "libfuzzer-sys", + "serde_json", + "simplicityhl", +] + [[package]] name = "strsim" version = "0.11.1" diff --git a/Cargo.toml b/Cargo.toml index ec7a60e..0ae20e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,8 +38,8 @@ clap = "4.5.37" getrandom = { version = "0.2", features = ["js"] } [workspace] -members = ["codegen"] -exclude = ["fuzz", "bitcoind-tests"] +members = ["codegen", "fuzz"] +exclude = ["bitcoind-tests"] [lints.clippy] # Exclude lints we don't think are valuable. diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index e70be25..da5a994 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -14,6 +14,12 @@ simplicityhl = { path = "..", features = ["arbitrary", "serde"] } itertools = "0.13.0" serde_json = "1.0.105" +[dev-dependencies] +base64 = "0.22.1" + +[lints.rust] +unexpected_cfgs = { level = "deny", check-cfg = ['cfg(fuzzing)'] } + [[bin]] name = "compile_text" path = "fuzz_targets/compile_text.rs" diff --git a/fuzz/fuzz_targets/compile_parse_tree.rs b/fuzz/fuzz_targets/compile_parse_tree.rs index 05014f1..1861350 100644 --- a/fuzz/fuzz_targets/compile_parse_tree.rs +++ b/fuzz/fuzz_targets/compile_parse_tree.rs @@ -1,12 +1,12 @@ -#![no_main] +#![cfg_attr(fuzzing, no_main)] -use arbitrary::Arbitrary; -use libfuzzer_sys::fuzz_target; +#[cfg(any(fuzzing, test))] +fn do_test(data: &[u8]) { + use arbitrary::Arbitrary; -use simplicityhl::error::WithFile; -use simplicityhl::{ast, named, parse, ArbitraryOfType, Arguments}; + use simplicityhl::error::WithFile; + use simplicityhl::{ast, named, parse, ArbitraryOfType, Arguments}; -fuzz_target!(|data: &[u8]| { let mut u = arbitrary::Unstructured::new(data); let parse_program = match parse::Program::arbitrary(&mut u) { Ok(x) => x, @@ -24,6 +24,24 @@ fuzz_target!(|data: &[u8]| { .compile(arguments, false) .with_file("") .expect("AST should compile with given arguments"); - let _simplicity_commit = named::to_commit_node(&simplicity_named_construct) - .expect("Conversion to commit node should never fail"); -}); + let _simplicity_commit = named::forget_names(&simplicity_named_construct); +} + +#[cfg(fuzzing)] +libfuzzer_sys::fuzz_target!(|data| do_test(data)); + +#[cfg(not(fuzzing))] +fn main() {} + +#[cfg(test)] +mod tests { + use base64::Engine; + + #[test] + fn duplicate_crash() { + let data = base64::prelude::BASE64_STANDARD + .decode("Cg==") + .expect("base64 should be valid"); + super::do_test(&data); + } +} diff --git a/fuzz/fuzz_targets/compile_text.rs b/fuzz/fuzz_targets/compile_text.rs index ea2ff34..0ac1564 100644 --- a/fuzz/fuzz_targets/compile_text.rs +++ b/fuzz/fuzz_targets/compile_text.rs @@ -1,9 +1,4 @@ -#![no_main] - -use arbitrary::Arbitrary; -use libfuzzer_sys::{fuzz_target, Corpus}; - -use simplicityhl::{ArbitraryOfType, Arguments}; +#![cfg_attr(fuzzing, no_main)] /// The PEST parser is slow for inputs with many open brackets. /// Detect some of these inputs to reject them from the corpus. @@ -11,6 +6,7 @@ use simplicityhl::{ArbitraryOfType, Arguments}; /// ```text /// fn n(){ { (s,(( (Ns,(s,(x,(((s,((s,(s,(s,(x,(( {5 /// ``` +#[cfg(any(fuzzing, test))] fn slow_input(program_text: &str) -> bool { let mut consecutive_open_brackets = 0; @@ -28,7 +24,12 @@ fn slow_input(program_text: &str) -> bool { false } -fuzz_target!(|data: &[u8]| -> Corpus { +#[cfg(any(fuzzing, test))] +fn do_test(data: &[u8]) -> libfuzzer_sys::Corpus { + use arbitrary::Arbitrary; + use libfuzzer_sys::Corpus; + use simplicityhl::{ArbitraryOfType, Arguments}; + let mut u = arbitrary::Unstructured::new(data); let program_text = match ::arbitrary(&mut u) { @@ -49,4 +50,25 @@ fuzz_target!(|data: &[u8]| -> Corpus { let _ = template.instantiate(arguments, false); Corpus::Keep +} + +#[cfg(fuzzing)] +libfuzzer_sys::fuzz_target!(|data: &[u8]| { + let _ = do_test(data); }); + +#[cfg(not(fuzzing))] +fn main() {} + +#[cfg(test)] +mod tests { + use base64::Engine; + + #[test] + fn duplicate_crash() { + let data = base64::prelude::BASE64_STANDARD + .decode("Cg==") + .expect("base64 should be valid"); + super::do_test(&data); + } +} diff --git a/fuzz/fuzz_targets/display_parse_tree.rs b/fuzz/fuzz_targets/display_parse_tree.rs index 064952a..799ff64 100644 --- a/fuzz/fuzz_targets/display_parse_tree.rs +++ b/fuzz/fuzz_targets/display_parse_tree.rs @@ -1,15 +1,38 @@ -#![no_main] +#![cfg_attr(fuzzing, no_main)] -use libfuzzer_sys::fuzz_target; +#[cfg(any(fuzzing, test))] +fn do_test(parse_program: simplicityhl::parse::Program) { + use simplicityhl::parse::{ParseFromStr, Program}; -use simplicityhl::parse::{self, ParseFromStr}; - -fuzz_target!(|parse_program: parse::Program| { let program_text = parse_program.to_string(); - let restored_parse_program = parse::Program::parse_from_str(program_text.as_str()) + let restored_parse_program = Program::parse_from_str(program_text.as_str()) .expect("Output of fmt::Display should be parseable"); assert_eq!( parse_program, restored_parse_program, "Output of fmt::Display should parse to original program" ); +} + +#[cfg(not(fuzzing))] +fn main() {} + +#[cfg(fuzzing)] +libfuzzer_sys::fuzz_target!(|data: simplicityhl::parse::Program| { + do_test(data); }); + +#[cfg(test)] +mod test { + + use simplicityhl::parse::{ParseFromStr, Program}; + #[test] + fn test() { + let program_test = r#"fn main() { + assert!(jet::eq_32(witness::A, witness::A)); + }"#; + + let program = Program::parse_from_str(program_test) + .expect("expected conversion to Program to be successfull"); + super::do_test(program); + } +} diff --git a/fuzz/fuzz_targets/parse_value_rtt.rs b/fuzz/fuzz_targets/parse_value_rtt.rs index fdb835c..4c8a4df 100644 --- a/fuzz/fuzz_targets/parse_value_rtt.rs +++ b/fuzz/fuzz_targets/parse_value_rtt.rs @@ -1,10 +1,9 @@ -#![no_main] +#![cfg_attr(fuzzing, no_main)] -use libfuzzer_sys::fuzz_target; +#[cfg(any(fuzzing, test))] +fn do_test(value: simplicityhl::value::Value) { + use simplicityhl::value::Value; -use simplicityhl::value::Value; - -fuzz_target!(|value: Value| { let value_string = value.to_string(); let parsed_value = Value::parse_from_str(&value_string, value.ty()).expect("Value string should be parseable"); @@ -12,4 +11,26 @@ fuzz_target!(|value: Value| { value, parsed_value, "Value string should parse to original value" ); +} + +#[cfg(not(fuzzing))] +fn main() {} + +#[cfg(fuzzing)] +libfuzzer_sys::fuzz_target!(|data: simplicityhl::value::Value| { + do_test(data); }); + +#[cfg(test)] +mod test { + use simplicityhl::{types::TypeConstructible, value::Value, ResolvedType}; + + use crate::do_test; + #[test] + fn test() { + let value = Value::parse_from_str("true", &ResolvedType::boolean()) + .expect("should parse a valid value"); + + do_test(value); + } +} diff --git a/fuzz/fuzz_targets/parse_witness_json_rtt.rs b/fuzz/fuzz_targets/parse_witness_json_rtt.rs index b371c61..ee9ceee 100644 --- a/fuzz/fuzz_targets/parse_witness_json_rtt.rs +++ b/fuzz/fuzz_targets/parse_witness_json_rtt.rs @@ -1,10 +1,7 @@ -#![no_main] +#![cfg_attr(fuzzing, no_main)] -use libfuzzer_sys::fuzz_target; - -use simplicityhl::WitnessValues; - -fuzz_target!(|witness_values: WitnessValues| { +#[cfg(any(fuzzing, test))] +fn do_test(witness_values: simplicityhl::WitnessValues) { let witness_text = serde_json::to_string(&witness_values) .expect("Witness map should be convertible into JSON"); let parsed_witness_text = @@ -13,4 +10,27 @@ fuzz_target!(|witness_values: WitnessValues| { witness_values, parsed_witness_text, "Witness JSON should parse to original witness map" ); -}); +} + +#[cfg(not(fuzzing))] +fn main() {} + +#[cfg(fuzzing)] +libfuzzer_sys::fuzz_target!(|data: simplicityhl::WitnessValues| do_test(data)); + +#[cfg(test)] +mod test { + use simplicityhl::{parse::ParseFromStr, WitnessValues}; + #[test] + fn test() { + let witness_text = r#"mod witness { + const A: u32 = 1; + const B: u32 = 2; + const C: u32 = 3; + }"#; + + let witness_values = WitnessValues::parse_from_str(witness_text) + .expect("parsing of valid string should work"); + super::do_test(witness_values); + } +} diff --git a/fuzz/fuzz_targets/parse_witness_module_rtt.rs b/fuzz/fuzz_targets/parse_witness_module_rtt.rs index ac5f645..2b6e520 100644 --- a/fuzz/fuzz_targets/parse_witness_module_rtt.rs +++ b/fuzz/fuzz_targets/parse_witness_module_rtt.rs @@ -1,11 +1,10 @@ -#![no_main] +#![cfg_attr(fuzzing, no_main)] -use libfuzzer_sys::fuzz_target; +#[cfg(any(fuzzing, test))] +fn do_test(witness_values: simplicityhl::WitnessValues) { + use simplicityhl::parse::ParseFromStr; + use simplicityhl::WitnessValues; -use simplicityhl::parse::ParseFromStr; -use simplicityhl::WitnessValues; - -fuzz_target!(|witness_values: WitnessValues| { let witness_text = witness_values.to_string(); let parsed_witness_text = WitnessValues::parse_from_str(&witness_text).expect("Witness module should be parseable"); @@ -13,4 +12,27 @@ fuzz_target!(|witness_values: WitnessValues| { witness_values, parsed_witness_text, "Witness module should parse to original witness values" ); -}); +} + +#[cfg(not(fuzzing))] +fn main() {} + +#[cfg(fuzzing)] +libfuzzer_sys::fuzz_target!(|data: simplicityhl::WitnessValues| do_test(data)); + +#[cfg(test)] +mod test { + use simplicityhl::{parse::ParseFromStr, WitnessValues}; + #[test] + fn test() { + let witness_text = r#"mod witness { + const A: u32 = 1; + const B: u32 = 2; + const C: u32 = 3; + }"#; + + let witness_values = WitnessValues::parse_from_str(witness_text) + .expect("parsing of valid string should work"); + super::do_test(witness_values); + } +} diff --git a/fuzz/fuzz_targets/reconstruct_value.rs b/fuzz/fuzz_targets/reconstruct_value.rs index d17abd6..7859cc0 100644 --- a/fuzz/fuzz_targets/reconstruct_value.rs +++ b/fuzz/fuzz_targets/reconstruct_value.rs @@ -1,11 +1,29 @@ -#![no_main] +#![cfg_attr(fuzzing, no_main)] -use libfuzzer_sys::fuzz_target; - -use simplicityhl::value::{StructuralValue, Value}; - -fuzz_target!(|value: Value| { +#[cfg(any(fuzzing, test))] +fn do_test(value: simplicityhl::value::Value) { + use simplicityhl::value::{StructuralValue, Value}; let structural_value = StructuralValue::from(&value); let reconstructed_value = Value::reconstruct(&structural_value, value.ty()).unwrap(); assert_eq!(reconstructed_value, value); -}); +} + +#[cfg(not(fuzzing))] +fn main() {} + +#[cfg(fuzzing)] +libfuzzer_sys::fuzz_target!(|data: simplicityhl::value::Value| do_test(data)); + +#[cfg(test)] +mod test { + use simplicityhl::{types::TypeConstructible, value::Value, ResolvedType}; + + use crate::do_test; + #[test] + fn test() { + let value = Value::parse_from_str("true", &ResolvedType::boolean()) + .expect("should parse a valid value"); + + do_test(value); + } +}