From c51f8010686447d1202971fb40d6702b01aff151 Mon Sep 17 00:00:00 2001 From: Vladimir Petrzhikovskii Date: Wed, 22 May 2024 14:43:26 +0200 Subject: [PATCH] feat(fuzz): arbitrary implementation for Cell and CellBuilder --- Cargo.toml | 2 + fuzz/.gitignore | 1 + fuzz/Cargo.toml | 14 +++++++ fuzz/fuzz_targets/boc_decode_encode.rs | 26 ++++++++++-- fuzz/fuzz_targets/boc_dict_arb.rs | 12 ++++++ fuzz/fuzz_targets/boc_message_arb.rs | 13 ++++++ src/cell/builder.rs | 55 +++++++++++++++++++++++++- src/cell/mod.rs | 10 +++++ 8 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 fuzz/fuzz_targets/boc_dict_arb.rs create mode 100644 fuzz/fuzz_targets/boc_message_arb.rs diff --git a/Cargo.toml b/Cargo.toml index 3bc9322e..24c60cd0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ members = ["proc"] [dependencies] ahash = "0.8" anyhow = { version = "1.0", optional = true } +arbitrary = { version = "1", optional = true } base64 = { version = "0.22", optional = true } bitflags = "2.3" blake3 = { version = "1.5", optional = true } @@ -62,6 +63,7 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" [features] +arbitrary = ["dep:arbitrary"] default = ["base64", "serde", "models", "sync"] sync = ["dep:scc"] stats = [] diff --git a/fuzz/.gitignore b/fuzz/.gitignore index a0925114..1a45eee7 100644 --- a/fuzz/.gitignore +++ b/fuzz/.gitignore @@ -1,3 +1,4 @@ target corpus artifacts +coverage diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 7b2cd142..7ea31c97 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -9,10 +9,12 @@ edition = "2021" cargo-fuzz = true [dependencies] +arbitrary = { version = "1.0.1", features = ["derive"] } libfuzzer-sys = "0.4" [dependencies.everscale-types] path = ".." +features = ["arbitrary"] # Prevent this from interfering with workspaces [workspace] @@ -42,8 +44,20 @@ path = "fuzz_targets/boc_dict.rs" test = false doc = false +[[bin]] +name = "boc_dict_arb" +path = "fuzz_targets/boc_dict_arb.rs" +test = false +doc = false + [[bin]] name = "boc_message" path = "fuzz_targets/boc_message.rs" test = false doc = false + +[[bin]] +name = "boc_message_arb" +path = "fuzz_targets/boc_message_arb.rs" +test = false +doc = false diff --git a/fuzz/fuzz_targets/boc_decode_encode.rs b/fuzz/fuzz_targets/boc_decode_encode.rs index 4eddac4e..c1a8feed 100644 --- a/fuzz/fuzz_targets/boc_decode_encode.rs +++ b/fuzz/fuzz_targets/boc_decode_encode.rs @@ -1,10 +1,28 @@ #![no_main] -use libfuzzer_sys::fuzz_target; +use libfuzzer_sys::{fuzz_target, Corpus}; -use everscale_types::prelude::Boc; +use everscale_types::cell::CellTreeStats; +use everscale_types::prelude::*; -fuzz_target!(|data: &[u8]| { +fuzz_target!(|data: &[u8]| -> Corpus { if let Ok(cell) = Boc::decode(data) { - _ = Boc::encode(cell.as_ref()); + let res = Boc::encode(cell.as_ref()); + let redecoded = Boc::decode(&res).unwrap(); + assert_eq!(cell.as_ref(), redecoded.as_ref()); + let l = call_all_cell_methods(&cell); + let r = call_all_cell_methods(&redecoded); + assert_eq!(l, r); + return Corpus::Keep; } + Corpus::Reject }); + +fn call_all_cell_methods(cell: &Cell) -> CellTreeStats { + let hash = cell.hash(0); + let hash = cell.hash(1); + let hash = cell.hash(2); + let hash = cell.hash(3); + + let _ = cell.virtualize(); + cell.compute_unique_stats(usize::MAX).unwrap() +} diff --git a/fuzz/fuzz_targets/boc_dict_arb.rs b/fuzz/fuzz_targets/boc_dict_arb.rs new file mode 100644 index 00000000..4681450c --- /dev/null +++ b/fuzz/fuzz_targets/boc_dict_arb.rs @@ -0,0 +1,12 @@ +#![no_main] +use libfuzzer_sys::{fuzz_target, Corpus}; + +use everscale_types::prelude::{Cell, RawDict}; + +fuzz_target!(|data: Cell| -> Corpus { + if let Ok(map) = data.parse::>() { + _ = map.iter().count(); + return Corpus::Keep; + } + Corpus::Reject +}); diff --git a/fuzz/fuzz_targets/boc_message_arb.rs b/fuzz/fuzz_targets/boc_message_arb.rs new file mode 100644 index 00000000..aae93028 --- /dev/null +++ b/fuzz/fuzz_targets/boc_message_arb.rs @@ -0,0 +1,13 @@ +#![no_main] +use libfuzzer_sys::{fuzz_target, Corpus}; + +use everscale_types::models::Message; +use everscale_types::prelude::Cell; + +fuzz_target!(|cell: Cell| -> Corpus { + if cell.parse::().is_ok() { + return Corpus::Keep; + } + + Corpus::Reject +}); diff --git a/src/cell/builder.rs b/src/cell/builder.rs index 52d69955..c4a556dd 100644 --- a/src/cell/builder.rs +++ b/src/cell/builder.rs @@ -14,6 +14,8 @@ use crate::util::{ArrayVec, Bitstring}; use super::CellFamily; #[cfg(feature = "stats")] use super::CellTreeStats; +#[cfg(feature = "arbitrary")] +use arbitrary::{Arbitrary, Result as ArbitraryResult, Unstructured}; /// A data structure that can be serialized into cells. pub trait Store { @@ -786,6 +788,30 @@ impl CellBuilder { Err(Error::CellOverflow) } } + + #[cfg(feature = "arbitrary")] + fn arbitrary_with_depth(u: &mut Unstructured, depth: usize) -> ArbitraryResult { + let mut builder = CellBuilder::new(); + + // Generate a random bit length within the valid range + let random_bit_len = u.int_in_range(0..=MAX_BIT_LEN)?; + let random_bytes = u.bytes(random_bit_len as usize / 8 + 1)?; + builder + .store_raw(random_bytes, random_bit_len) + .expect("valid bit length"); + + if depth > 0 { + let ref_count = u.int_in_range(0..=MAX_REF_COUNT as u8)?; + for _ in 0..ref_count { + let child = Self::arbitrary_with_depth(u, depth - 1)? + .build() + .map_err(|_| arbitrary::Error::IncorrectFormat)?; + builder.store_reference(child).expect("reference fits"); + } + } + + Ok(builder) + } } #[inline] @@ -882,7 +908,7 @@ impl CellBuilder { } /// Tries to append a builder (its data and references), - /// returning `false` if there is not enough remaining capacity. + /// returning `Error::CellOverflow` if there is not enough remaining capacity. pub fn store_builder(&mut self, builder: &Self) -> Result<(), Error> { if self.bit_len + builder.bit_len <= MAX_BIT_LEN && self.references.len() + builder.references.len() <= MAX_REF_COUNT @@ -1219,6 +1245,33 @@ impl CellImpl for IntermediateFullCell { } } +#[cfg(feature = "arbitrary")] +impl<'a> Arbitrary<'a> for CellBuilder { + fn arbitrary(u: &mut Unstructured<'a>) -> ArbitraryResult { + let depth = u.int_in_range(0..=5)?; + Self::arbitrary_with_depth(u, depth) + } + + fn size_hint(depth: usize) -> (usize, Option) { + let bit_len_hint = (0, Some((MAX_BIT_LEN / 8 + 1 + 2) as usize)); // from 0 to MAX_BIT_LEN bits + bit len + + // Base case: if depth is zero, we do not include recursive cell hints + if depth == 0 { + return bit_len_hint; + } + + // Recursive case: include recursive cell hints + let child_hint = ::size_hint(depth - 1); + + let lower = bit_len_hint.0 + child_hint.0 * MAX_REF_COUNT; + let upper = bit_len_hint + .1 + .and_then(|x| child_hint.1.map(|y| x + y * MAX_REF_COUNT)); + + (lower, upper) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/cell/mod.rs b/src/cell/mod.rs index 02bc25ad..7376dc8e 100644 --- a/src/cell/mod.rs +++ b/src/cell/mod.rs @@ -1527,6 +1527,16 @@ pub const MAX_BIT_LEN: u16 = 1023; /// Maximum number of child cells pub const MAX_REF_COUNT: usize = 4; +#[cfg(feature = "arbitrary")] +impl arbitrary::Arbitrary<'_> for Cell { + fn arbitrary(u: &mut arbitrary::Unstructured) -> arbitrary::Result { + let builder = CellBuilder::arbitrary(u)?; + builder + .build() + .map_err(|_| arbitrary::Error::IncorrectFormat) + } +} + #[cfg(test)] mod tests { use super::*;