From d1ffb464934a6fb0e39a38ee57f0398b21522cec Mon Sep 17 00:00:00 2001 From: arvidn Date: Fri, 13 Oct 2023 01:17:45 +0200 Subject: [PATCH] add fuzzer for streamable update_digest() as well as round-trip serializing. Make message types implement the Arbitrary trait when building in fuzzing mode. Add utility functions, to_bytes(), from_bytes() and hash() on the Streamable trait --- Cargo.lock | 19 +++ chia-bls/Cargo.toml | 1 + chia-bls/src/public_key.rs | 8 ++ chia-bls/src/secret_key.rs | 9 ++ chia-bls/src/signature.rs | 8 ++ chia-protocol/Cargo.toml | 1 + chia-protocol/fuzz/Cargo.toml | 10 ++ .../fuzz/fuzz_targets/parse-foliage.rs | 3 +- .../fuzz/fuzz_targets/parse-full-block.rs | 3 +- .../fuzz/fuzz_targets/parse-header-block.rs | 3 +- .../fuzz/fuzz_targets/parse-program.rs | 3 +- .../fuzz/fuzz_targets/parse-tx-info.rs | 3 +- chia-protocol/fuzz/fuzz_targets/streamable.rs | 109 ++++++++++++++++++ chia-protocol/src/bytes.rs | 2 + chia-protocol/src/chia_protocol.rs | 2 + chia-protocol/src/message_struct.rs | 2 + chia-protocol/src/program.rs | 24 ++++ chia-traits/src/streamable.rs | 21 ++++ 18 files changed, 221 insertions(+), 10 deletions(-) create mode 100644 chia-protocol/fuzz/fuzz_targets/streamable.rs diff --git a/Cargo.lock b/Cargo.lock index 26822756a..135a9a68d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,6 +92,9 @@ name = "arbitrary" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2d098ff73c1ca148721f37baad5ea6a465a13f9573aba8641fbbbae8164a54e" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "autocfg" @@ -254,6 +257,7 @@ name = "chia-bls" version = "0.2.7" dependencies = [ "anyhow", + "arbitrary", "blst", "chia-traits", "chia_py_streamable_macro", @@ -308,6 +312,7 @@ dependencies = [ name = "chia-protocol" version = "0.2.7" dependencies = [ + "arbitrary", "chia-bls", "chia-traits", "chia_py_streamable_macro", @@ -324,9 +329,12 @@ dependencies = [ name = "chia-protocol-fuzz" version = "0.0.0" dependencies = [ + "arbitrary", "chia-protocol", "chia-traits", + "hex", "libfuzzer-sys", + "sha2 0.9.9", ] [[package]] @@ -686,6 +694,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "derive_arbitrary" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e0efad4403bfc52dc201159c4b842a246a14b98c64b55dfd0f2d89729dfeb8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + [[package]] name = "digest" version = "0.9.0" diff --git a/chia-bls/Cargo.toml b/chia-bls/Cargo.toml index cbcd01863..dee128617 100644 --- a/chia-bls/Cargo.toml +++ b/chia-bls/Cargo.toml @@ -25,6 +25,7 @@ hex = "0.4.3" thiserror = "1.0.44" pyo3 = { version = ">=0.19.0", features = ["multiple-pymethods"], optional = true } chia_py_streamable_macro = { version = "0.1.3", path = "../chia_py_streamable_macro", optional = true } +arbitrary = { version = "=1.3.0" } [dev-dependencies] rand = "0.8.5" diff --git a/chia-bls/src/public_key.rs b/chia-bls/src/public_key.rs index 303186a49..399690662 100644 --- a/chia-bls/src/public_key.rs +++ b/chia-bls/src/public_key.rs @@ -30,6 +30,14 @@ use pyo3::{pyclass, pymethods, IntoPy, PyAny, PyObject, PyResult, Python}; #[derive(Clone, Default)] pub struct PublicKey(pub(crate) blst_p1); +#[cfg(fuzzing)] +impl<'a> arbitrary::Arbitrary<'a> for PublicKey { + fn arbitrary(_u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + // placeholder + Ok(Self::default()) + } +} + impl PublicKey { pub fn from_bytes_unchecked(bytes: &[u8; 48]) -> Result { // check if the element is canonical diff --git a/chia-bls/src/secret_key.rs b/chia-bls/src/secret_key.rs index 08cdc5d2b..1ec3e22f0 100644 --- a/chia-bls/src/secret_key.rs +++ b/chia-bls/src/secret_key.rs @@ -29,6 +29,15 @@ use pyo3::{pyclass, pymethods, IntoPy, PyAny, PyObject, PyResult, Python}; #[derive(PartialEq, Eq, Clone)] pub struct SecretKey(pub(crate) blst_scalar); +#[cfg(fuzzing)] +impl<'a> arbitrary::Arbitrary<'a> for SecretKey { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let mut seed = [0_u8; 32]; + let _ = u.fill_buffer(seed.as_mut_slice()); + Ok(Self::from_seed(&seed)) + } +} + fn flip_bits(input: [u8; 32]) -> [u8; 32] { let mut ret = [0; 32]; for i in 0..32 { diff --git a/chia-bls/src/signature.rs b/chia-bls/src/signature.rs index 6e767393a..4e3c77574 100644 --- a/chia-bls/src/signature.rs +++ b/chia-bls/src/signature.rs @@ -34,6 +34,14 @@ pub const DST: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_AUG_"; #[derive(Clone, Default)] pub struct Signature(pub(crate) blst_p2); +#[cfg(fuzzing)] +impl<'a> arbitrary::Arbitrary<'a> for Signature { + fn arbitrary(_u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + // placeholder + Ok(Self::default()) + } +} + impl Signature { pub fn from_bytes_unchecked(buf: &[u8; 96]) -> Result { let p2 = unsafe { diff --git a/chia-protocol/Cargo.toml b/chia-protocol/Cargo.toml index e1660bf6b..910f73dbf 100644 --- a/chia-protocol/Cargo.toml +++ b/chia-protocol/Cargo.toml @@ -21,6 +21,7 @@ clvmr = "0.3.0" chia-traits = { version = "0.1.0", path = "../chia-traits" } clvm-traits = { version = "0.1.0", path = "../clvm-traits", features = ["derive"] } chia-bls = { version = "0.2.7", path = "../chia-bls" } +arbitrary = { version = "=1.3.0", features = ["derive"] } [dev-dependencies] rstest = "0.17.0" diff --git a/chia-protocol/fuzz/Cargo.toml b/chia-protocol/fuzz/Cargo.toml index fd577067a..db8b64394 100644 --- a/chia-protocol/fuzz/Cargo.toml +++ b/chia-protocol/fuzz/Cargo.toml @@ -11,6 +11,9 @@ cargo-fuzz = true [dependencies] libfuzzer-sys = "0.4" chia-traits = { path = "../../chia-traits" } +arbitrary = { version = "=1.3.0" } +sha2 = "0.9.9" +hex = "0.4.3" [dependencies.chia-protocol] path = ".." @@ -49,3 +52,10 @@ path = "fuzz_targets/parse-program.rs" test = false doc = false bench = false + +[[bin]] +name = "streamable" +path = "fuzz_targets/streamable.rs" +test = false +doc = false +bench = false diff --git a/chia-protocol/fuzz/fuzz_targets/parse-foliage.rs b/chia-protocol/fuzz/fuzz_targets/parse-foliage.rs index ca1a89e10..dd5a72781 100644 --- a/chia-protocol/fuzz/fuzz_targets/parse-foliage.rs +++ b/chia-protocol/fuzz/fuzz_targets/parse-foliage.rs @@ -2,8 +2,7 @@ use chia_protocol::Foliage; use chia_traits::Streamable; use libfuzzer_sys::fuzz_target; -use std::io::Cursor; fuzz_target!(|data: &[u8]| { - let _ret = ::parse(&mut Cursor::<&[u8]>::new(data)); + let _ = Foliage::from_bytes(data); }); diff --git a/chia-protocol/fuzz/fuzz_targets/parse-full-block.rs b/chia-protocol/fuzz/fuzz_targets/parse-full-block.rs index 49bf72fe5..3b2ef44ce 100644 --- a/chia-protocol/fuzz/fuzz_targets/parse-full-block.rs +++ b/chia-protocol/fuzz/fuzz_targets/parse-full-block.rs @@ -2,8 +2,7 @@ use chia_protocol::FullBlock; use chia_traits::Streamable; use libfuzzer_sys::fuzz_target; -use std::io::Cursor; fuzz_target!(|data: &[u8]| { - let _ret = ::parse(&mut Cursor::<&[u8]>::new(data)); + let _ = FullBlock::from_bytes(data); }); diff --git a/chia-protocol/fuzz/fuzz_targets/parse-header-block.rs b/chia-protocol/fuzz/fuzz_targets/parse-header-block.rs index 4033f8250..a4b363d70 100644 --- a/chia-protocol/fuzz/fuzz_targets/parse-header-block.rs +++ b/chia-protocol/fuzz/fuzz_targets/parse-header-block.rs @@ -2,8 +2,7 @@ use chia_protocol::HeaderBlock; use chia_traits::Streamable; use libfuzzer_sys::fuzz_target; -use std::io::Cursor; fuzz_target!(|data: &[u8]| { - let _ret = ::parse(&mut Cursor::<&[u8]>::new(data)); + let _ = HeaderBlock::from_bytes(data); }); diff --git a/chia-protocol/fuzz/fuzz_targets/parse-program.rs b/chia-protocol/fuzz/fuzz_targets/parse-program.rs index ccb27de9f..2d9daa685 100644 --- a/chia-protocol/fuzz/fuzz_targets/parse-program.rs +++ b/chia-protocol/fuzz/fuzz_targets/parse-program.rs @@ -2,8 +2,7 @@ use chia_protocol::Program; use chia_traits::Streamable; use libfuzzer_sys::fuzz_target; -use std::io::Cursor; fuzz_target!(|data: &[u8]| { - let _ret = ::parse(&mut Cursor::<&[u8]>::new(data)); + let _ = Program::from_bytes(data); }); diff --git a/chia-protocol/fuzz/fuzz_targets/parse-tx-info.rs b/chia-protocol/fuzz/fuzz_targets/parse-tx-info.rs index 0ff05352d..e14a60a21 100644 --- a/chia-protocol/fuzz/fuzz_targets/parse-tx-info.rs +++ b/chia-protocol/fuzz/fuzz_targets/parse-tx-info.rs @@ -2,8 +2,7 @@ use chia_protocol::TransactionsInfo; use chia_traits::Streamable; use libfuzzer_sys::fuzz_target; -use std::io::Cursor; fuzz_target!(|data: &[u8]| { - let _ret = ::parse(&mut Cursor::<&[u8]>::new(data)); + let _ = TransactionsInfo::from_bytes(data); }); diff --git a/chia-protocol/fuzz/fuzz_targets/streamable.rs b/chia-protocol/fuzz/fuzz_targets/streamable.rs new file mode 100644 index 000000000..5d197fc48 --- /dev/null +++ b/chia-protocol/fuzz/fuzz_targets/streamable.rs @@ -0,0 +1,109 @@ +#![no_main] +use ::chia_protocol::*; +use chia_traits::Streamable; +use libfuzzer_sys::fuzz_target; +use sha2::{Digest, Sha256}; + +#[cfg(fuzzing)] +use arbitrary::{Arbitrary, Unstructured}; + +pub fn test_streamable(obj: &T) { + let bytes = obj.to_bytes().unwrap(); + let obj2 = match T::from_bytes(&bytes) { + Err(_) => { + panic!( + "failed to parse input: {}, from object: {:?}", + hex::encode(bytes), + &obj + ); + } + Ok(o) => o, + }; + assert_eq!(obj, &obj2); + + let mut ctx = Sha256::new(); + ctx.update(bytes); + let expect_hash: [u8; 32] = ctx.finalize().into(); + assert_eq!(obj.hash(), expect_hash); +} +#[cfg(fuzzing)] +fn test<'a, T: Arbitrary<'a> + Streamable + std::fmt::Debug + PartialEq>(data: &'a [u8]) { + let mut u = Unstructured::new(data); + let obj = >::arbitrary(&mut u).unwrap(); + test_streamable(&obj); +} + +// this is here to make clippy happy +#[cfg(not(fuzzing))] +fn test(_data: &[u8]) {} + +fuzz_target!(|data: &[u8]| { + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + + test::(data); + + // Wallet Protocol + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); + test::(data); +}); diff --git a/chia-protocol/src/bytes.rs b/chia-protocol/src/bytes.rs index d3db0272e..635e7289b 100644 --- a/chia-protocol/src/bytes.rs +++ b/chia-protocol/src/bytes.rs @@ -24,6 +24,7 @@ use pyo3::prelude::*; use pyo3::types::PyBytes; #[derive(Hash, Clone, Eq, PartialEq, PartialOrd, Ord)] +#[cfg_attr(fuzzing, derive(arbitrary::Arbitrary))] pub struct Bytes(Vec); impl Bytes { @@ -183,6 +184,7 @@ impl fmt::Display for Bytes { } #[derive(Hash, PartialEq, Eq, Copy, Clone, PartialOrd, Ord)] +#[cfg_attr(fuzzing, derive(arbitrary::Arbitrary))] pub struct BytesImpl([u8; N]); impl Streamable for BytesImpl { diff --git a/chia-protocol/src/chia_protocol.rs b/chia-protocol/src/chia_protocol.rs index 3479e8f72..b2fd2a653 100644 --- a/chia-protocol/src/chia_protocol.rs +++ b/chia-protocol/src/chia_protocol.rs @@ -9,6 +9,7 @@ use chia_py_streamable_macro::{PyJsonDict, PyStreamable}; #[repr(u8)] #[cfg_attr(feature = "py-bindings", derive(PyJsonDict, PyStreamable))] +#[cfg_attr(fuzzing, derive(arbitrary::Arbitrary))] #[derive(Streamable, Hash, Debug, Copy, Clone, Eq, PartialEq)] pub enum ProtocolMessageTypes { // Shared protocol (all services) @@ -128,6 +129,7 @@ pub trait ChiaProtocolMessage { #[repr(u8)] #[cfg_attr(feature = "py-bindings", derive(PyJsonDict, PyStreamable))] +#[cfg_attr(fuzzing, derive(arbitrary::Arbitrary))] #[derive(Streamable, Hash, Debug, Copy, Clone, Eq, PartialEq)] pub enum NodeType { FullNode = 1, diff --git a/chia-protocol/src/message_struct.rs b/chia-protocol/src/message_struct.rs index 4e487e2cf..111163667 100644 --- a/chia-protocol/src/message_struct.rs +++ b/chia-protocol/src/message_struct.rs @@ -3,6 +3,7 @@ macro_rules! message_struct { ($name:ident {$($field:ident: $t:ty $(,)? )*}) => { #[cfg_attr(feature = "py-bindings", pyo3::pyclass(get_all, frozen), derive(chia_py_streamable_macro::PyJsonDict, chia_py_streamable_macro::PyStreamable))] #[derive(Streamable, Hash, Debug, Clone, Eq, PartialEq)] + #[cfg_attr(fuzzing, derive(arbitrary::Arbitrary))] pub struct $name { $(pub $field: $t),* } @@ -28,6 +29,7 @@ macro_rules! streamable_struct { ($name:ident {$($field:ident: $t:ty $(,)? )*}) => { #[cfg_attr(feature = "py-bindings", pyo3::pyclass(get_all, frozen), derive(chia_py_streamable_macro::PyJsonDict, chia_py_streamable_macro::PyStreamable))] #[derive(Streamable, Hash, Debug, Clone, Eq, PartialEq)] + #[cfg_attr(fuzzing, derive(arbitrary::Arbitrary))] pub struct $name { $(pub $field: $t),* } diff --git a/chia-protocol/src/program.rs b/chia-protocol/src/program.rs index 502e68809..10bff1f1b 100644 --- a/chia-protocol/src/program.rs +++ b/chia-protocol/src/program.rs @@ -21,6 +21,30 @@ use pyo3::prelude::*; #[derive(Hash, Debug, Clone, Eq, PartialEq)] pub struct Program(Bytes); +#[cfg(fuzzing)] +impl<'a> arbitrary::Arbitrary<'a> for Program { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + // generate an arbitrary CLVM structure. Not likely a valid program. + let mut items_left = 1; + let mut total_items = 0; + let mut buf = Vec::::with_capacity(200); + + while items_left > 0 { + if total_items < 100 && u.ratio(1, 4).unwrap() { + // make a pair + buf.push(0xff); + items_left += 2; + } else { + // make an atom. just single bytes for now + buf.push(u.int_in_range(0..=0x80).unwrap()); + } + total_items += 1; + items_left -= 1; + } + Ok(Self(buf.into())) + } +} + impl Program { pub fn len(&self) -> usize { self.0.len() diff --git a/chia-traits/src/streamable.rs b/chia-traits/src/streamable.rs index 2b1fcba1e..c127b8613 100644 --- a/chia-traits/src/streamable.rs +++ b/chia-traits/src/streamable.rs @@ -33,6 +33,27 @@ pub trait Streamable { fn parse(input: &mut Cursor<&[u8]>) -> Result where Self: Sized; + + // convenience functions for the top-level Streamable object + // these are meant to be used by *users* of streamable objects + // whereas the above functions are meant to be implemented by *implementers* + // of streamable types + fn to_bytes(&self) -> Result> { + let mut ret = Vec::::new(); + self.stream(&mut ret)?; + Ok(ret) + } + fn from_bytes(bytes: &[u8]) -> Result + where + Self: Sized, + { + Self::parse(&mut Cursor::new(bytes)) + } + fn hash(&self) -> [u8; 32] { + let mut ctx = Sha256::new(); + self.update_digest(&mut ctx); + ctx.finalize().into() + } } macro_rules! streamable_primitive {