Skip to content

Commit

Permalink
feat(fuzz): arbitrary implementation for Cell and CellBuilder
Browse files Browse the repository at this point in the history
  • Loading branch information
0xdeafbeef committed Jun 21, 2024
1 parent 0abd815 commit 21795f6
Show file tree
Hide file tree
Showing 9 changed files with 279 additions and 5 deletions.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ members = ["proc"]
[dependencies]
ahash = "0.8"
anyhow = { version = "1.0", optional = true }
arbitrary = { version = "1", optional = true }
base64 = { version = "0.21.0", optional = true }
bitflags = "2.3"
bytes = { version = "1.4", optional = true }
Expand All @@ -47,12 +48,14 @@ anyhow = "1.0"
base64 = "0.21"
criterion = "0.5"
libc = "0.2"
include_dir = "0.7.3"
rand = "0.8"
rand_xorshift = "0.3"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

[features]
arbitrary = ["dep:arbitrary"]
default = ["base64", "serde", "models", "sync"]
sync = []
stats = []
Expand Down
50 changes: 50 additions & 0 deletions cov.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/bin/bash
set -e

# Check if the correct number of arguments is provided
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <fuzz_target>"
exit 1
fi

# Assign the first argument to the fuzz_target variable
FUZZ_TARGET=$1

# Ensure necessary tools are installed
if ! command -v cargo &> /dev/null; then
echo "cargo could not be found, please install Rust and Cargo."
exit 1
fi

if ! command -v llvm-cov &> /dev/null; then
echo "llvm-cov could not be found, please install LLVM."
exit 1
fi

if ! command -v rustfilt &> /dev/null; then
echo "rustfilt could not be found, please install rustfilt."
exit 1
fi

# Run the cargo fuzz coverage command with the provided fuzz target
cargo +nightly fuzz coverage "$FUZZ_TARGET"

# Define the paths
TARGET_DIR="target/x86_64-unknown-linux-gnu/coverage/x86_64-unknown-linux-gnu/release"
PROF_DATA="fuzz/coverage/$FUZZ_TARGET/coverage.profdata"
OUTPUT_FILE="lcov.info"

# Check if the coverage data file exists
if [ ! -f "$PROF_DATA" ]; then
echo "Coverage data file $PROF_DATA not found."
exit 1
fi

# Generate the coverage report in HTML format
llvm-cov export "$TARGET_DIR/$FUZZ_TARGET" --format=lcov \
-Xdemangler=rustfilt \
--ignore-filename-regex="\.cargo" \
-instr-profile="$PROF_DATA" \
> "$OUTPUT_FILE"

echo "Coverage report generated as $OUTPUT_FILE"
1 change: 1 addition & 0 deletions fuzz/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
target
corpus
artifacts
coverage
14 changes: 14 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
26 changes: 22 additions & 4 deletions fuzz/fuzz_targets/boc_decode_encode.rs
Original file line number Diff line number Diff line change
@@ -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()
}
12 changes: 12 additions & 0 deletions fuzz/fuzz_targets/boc_dict_arb.rs
Original file line number Diff line number Diff line change
@@ -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::<RawDict<32>>() {
_ = map.iter().count();
return Corpus::Keep;
}
Corpus::Reject
});
13 changes: 13 additions & 0 deletions fuzz/fuzz_targets/boc_message_arb.rs
Original file line number Diff line number Diff line change
@@ -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::<Message>().is_ok() {
return Corpus::Keep;
}

Corpus::Reject
});
55 changes: 54 additions & 1 deletion src/cell/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -786,6 +788,30 @@ impl CellBuilder {
Err(Error::CellOverflow)
}
}

#[cfg(feature = "arbitrary")]
fn arbitrary_with_depth(u: &mut Unstructured, depth: usize) -> ArbitraryResult<Self> {
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]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1211,6 +1237,33 @@ impl CellImpl for IntermediateFullCell {
}
}

#[cfg(feature = "arbitrary")]
impl<'a> Arbitrary<'a> for CellBuilder {
fn arbitrary(u: &mut Unstructured<'a>) -> ArbitraryResult<Self> {
let depth = u.int_in_range(0..=5)?;
Self::arbitrary_with_depth(u, depth)
}

fn size_hint(depth: usize) -> (usize, Option<usize>) {
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 = <CellBuilder as Arbitrary>::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::*;
Expand Down
110 changes: 110 additions & 0 deletions src/cell/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1507,9 +1507,23 @@ 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<Self> {
let builder = CellBuilder::arbitrary(u)?;
builder
.build()
.map_err(|_| arbitrary::Error::IncorrectFormat)
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::boc::Boc;
use crate::dict::RawDict;
use crate::models::Message;
use include_dir::Dir;

#[test]
fn correct_level() {
Expand Down Expand Up @@ -1585,4 +1599,100 @@ mod tests {
assert_eq!(pruned3.repr_hash(), cell.repr_hash());
assert_eq!(pruned3.repr_depth(), cell.repr_depth());
}

#[test]
fn cell_test() {
let data = base64::decode("te6ccgEBAwEACAEABwAAAN4AARcAAbM=").unwrap();

Check warning on line 1605 in src/cell/mod.rs

View workflow job for this annotation

GitHub Actions / Test Suite

use of deprecated function `base64::decode`: Use Engine::decode

Check warning on line 1605 in src/cell/mod.rs

View workflow job for this annotation

GitHub Actions / Test Suite

unused variable: `data`

Check warning on line 1605 in src/cell/mod.rs

View workflow job for this annotation

GitHub Actions / Test Suite

use of deprecated function `base64::decode`: Use Engine::decode

Check warning on line 1605 in src/cell/mod.rs

View workflow job for this annotation

GitHub Actions / Test Suite

unused variable: `data`
let data = [
181, 238, 156, 114, 1, 1, 3, 1, 0, 45, 0, 35, 0, 2, 1, 1, 40, 72, 1, 1, 8, 67, 0, 0, 0,
0, 0, 16, 0, 255, 255, 5, 255, 0, 0, 0, 0, 0, 40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 16, 0, 0, 0,
];
println!("{}", base64::encode(&data));

Check warning on line 1611 in src/cell/mod.rs

View workflow job for this annotation

GitHub Actions / Test Suite

use of deprecated function `base64::encode`: Use Engine::encode

Check warning on line 1611 in src/cell/mod.rs

View workflow job for this annotation

GitHub Actions / Test Suite

use of deprecated function `base64::encode`: Use Engine::encode

if let Ok(cell) = Boc::decode(&data) {
let res = Boc::encode(cell.as_ref());
assert_eq!(res, data);
}

fn call_all_cell_methods(cell: &Cell) -> CellTreeStats {
let hash = cell.hash(0);

Check warning on line 1619 in src/cell/mod.rs

View workflow job for this annotation

GitHub Actions / Test Suite

unused variable: `hash`

Check warning on line 1619 in src/cell/mod.rs

View workflow job for this annotation

GitHub Actions / Test Suite

unused variable: `hash`
let hash = cell.hash(1);

Check warning on line 1620 in src/cell/mod.rs

View workflow job for this annotation

GitHub Actions / Test Suite

unused variable: `hash`

Check warning on line 1620 in src/cell/mod.rs

View workflow job for this annotation

GitHub Actions / Test Suite

unused variable: `hash`
let hash = cell.hash(2);

Check warning on line 1621 in src/cell/mod.rs

View workflow job for this annotation

GitHub Actions / Test Suite

unused variable: `hash`

Check warning on line 1621 in src/cell/mod.rs

View workflow job for this annotation

GitHub Actions / Test Suite

unused variable: `hash`
let hash = cell.hash(3);

Check warning on line 1622 in src/cell/mod.rs

View workflow job for this annotation

GitHub Actions / Test Suite

unused variable: `hash`

Check warning on line 1622 in src/cell/mod.rs

View workflow job for this annotation

GitHub Actions / Test Suite

unused variable: `hash`

let _ = cell.virtualize();
cell.compute_unique_stats(usize::MAX).unwrap()
}
}

#[test]
fn miri_from_fuzz_inputs() {
static FUZZ_CORPUS: Dir = include_dir::include_dir!("$CARGO_MANIFEST_DIR/fuzz/corpus");

Check failure on line 1631 in src/cell/mod.rs

View workflow job for this annotation

GitHub Actions / Test Suite

proc macro panicked

Check failure on line 1631 in src/cell/mod.rs

View workflow job for this annotation

GitHub Actions / Test Suite

proc macro panicked

let total = FUZZ_CORPUS.get_dir("boc_decode_encode").unwrap().files().count();
for (idx,entry) in FUZZ_CORPUS.get_dir("boc_decode_encode").unwrap().files().enumerate() {
let bytes = entry.contents();
boc_decode_encode(&bytes);
println!("{} / {}", idx, total);
}

for entry in FUZZ_CORPUS.get_dir("boc_message").unwrap().files() {
let data = entry.contents();
if let Ok(cell) = Boc::decode(data) {
if cell.parse::<Message>().is_ok() {}
}
}

for entry in FUZZ_CORPUS.get_dir("boc_dict").unwrap().files() {
let data = entry.contents();
if let Ok(cell) = Boc::decode(data) {
if let Ok(map) = cell.parse::<RawDict<32>>() {
_ = map.iter().count();
}
}
}
}

fn boc_decode_encode(data: &[u8]) {
if let Ok(cell) = Boc::decode(data) {
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);
}
}

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

fn test_cell(bytes: &[u8]) {
if let Ok(cell) = Boc::decode(bytes) {
if let Ok(mut slice) = cell.as_slice() {
_ = slice.get_u8(0);
_ = slice.get_u16(0);
_ = slice.get_u32(0);
_ = slice.get_u64(0);
_ = slice.get_u128(0);
_ = slice.get_u256(0);
if slice.try_advance(3, 0) {
_ = slice.get_u8(0);
_ = slice.get_u16(0);
_ = slice.get_u32(0);
_ = slice.get_u64(0);
_ = slice.get_u128(0);
_ = slice.get_u256(0);
}
}
}
}
}

0 comments on commit 21795f6

Please sign in to comment.