Skip to content

feat(forge): coverage guided fuzzing & time based campaigns for invariant mode #10190

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 57 commits into from
Jun 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
eb00624
rename coverage to line coverage for clarity
0xalpharush Jan 24, 2025
5964850
WIP: coverage guided fuzzing
0xalpharush Mar 13, 2025
9297097
wip persist invariant corpus
0xalpharush Mar 27, 2025
58bbaff
add binning and history map
0xalpharush Mar 28, 2025
d03aafd
rm proptest runner, add corpus mutations
0xalpharush Mar 28, 2025
2540b4d
fix: splice mutation, add some notes
0xalpharush Mar 29, 2025
9818d37
Merge remote-tracking branch 'upstream/master' into feat/coverage-fuzz
grandizzy Mar 31, 2025
e7a878f
Clippy and more tests
grandizzy Mar 31, 2025
1ceefaa
save
0xalpharush Apr 8, 2025
bc214cb
Merge branch 'master' into feat/coverage-guided-fuzzing
0xalpharush Apr 8, 2025
b7f09d8
use libafl_bolt's SIMD hitmap
0xalpharush Apr 16, 2025
ac19ef3
fix eyre issues
0xalpharush Apr 16, 2025
ce5f38d
add comments and psuedocode
0xalpharush Apr 16, 2025
5954648
Revert libafl
grandizzy May 24, 2025
9f6588c
Merge remote-tracking branch 'upstream/master' into coverage-re
grandizzy May 24, 2025
047bb42
Typo
grandizzy May 24, 2025
ecbed4a
Fix win config test
grandizzy May 24, 2025
06bc7ec
cleanup, save corpus at the end of run, if new coverage
grandizzy May 26, 2025
c24aa37
consolidate corpus manager
grandizzy May 26, 2025
0b678fe
Consolidate tx manager corpus logic
grandizzy May 27, 2025
b1a189b
Review changes: do not stop fuzzing if corpus replay failures, report
grandizzy May 27, 2025
1077cad
Default gzip corpus and config to toggle json/gzip
grandizzy May 28, 2025
616d930
Evict oldest corpus with more than x mutations
grandizzy May 28, 2025
36a8f11
Add min corpus size config, bump max mutations to default depth run
grandizzy May 28, 2025
e8fcb4b
Simplify corpus manager and corpus struct, enable prefix / suffix
grandizzy May 29, 2025
c1a1039
Merge remote-tracking branch 'upstream/master' into tt-test
grandizzy May 29, 2025
e9ffa00
Fuzz arg from ABI
grandizzy May 29, 2025
1d70d17
Corpus max mutations default 5
grandizzy May 29, 2025
15c55b2
Save metadata on disk at eviction time
grandizzy May 29, 2025
93e2da3
Remove more than 2 branches branch, make sure we always have one
grandizzy May 29, 2025
e7d39fb
Load gz and json seeds, ignore metadata files
grandizzy May 29, 2025
27dc9c7
ABI mutation replaces subset of arguments sometimes
0xalpharush Jun 1, 2025
b6bb837
prevent empty range but perform at least 1 round
0xalpharush Jun 1, 2025
f2d637e
trim selector when using abi_decode_input
0xalpharush Jun 1, 2025
7f27484
Nit, remove clippy allow
grandizzy Jun 1, 2025
cf44636
retain corpus items that are highly likely to produce new finds
0xalpharush Jun 2, 2025
d90ad7f
rename corpus_max_mutations to corpus_min_mutations
0xalpharush Jun 2, 2025
383b347
update cli test expectations
0xalpharush Jun 2, 2025
a5f92bf
Merge branch 'master' into feat/coverage-guided-fuzzing
grandizzy Jun 2, 2025
08e501a
Stateless fuzz corpus config revert, add invariant time based campaigns
grandizzy Jun 2, 2025
d305fff
Changes after review
grandizzy Jun 2, 2025
7e07a35
Remove outdated comment
grandizzy Jun 2, 2025
6c46792
Merge branch 'master' into fuzz-cov
grandizzy Jun 4, 2025
6f981ed
Update crates/evm/evm/src/executors/mod.rs
grandizzy Jun 5, 2025
741eb4e
Changes after review: comment, update merge_edge_coverage, use rng.gen
grandizzy Jun 5, 2025
437c170
Fix docs
grandizzy Jun 5, 2025
1f3cd97
Merge branch 'master' into feat/coverage-guided-fuzzing
grandizzy Jun 5, 2025
0148fd7
Merge branch 'master' into feat/coverage-guided-fuzzing
grandizzy Jun 6, 2025
6211433
Merge branch 'master' into fuzz-cov-merge-prop
grandizzy Jun 9, 2025
b81af72
Keep test assert, found faster than without guidance
grandizzy Jun 9, 2025
dfb5058
Merge branch 'master' into feat/coverage-guided-fuzzing
grandizzy Jun 10, 2025
a5fc10e
Merge branch 'master' into feat/coverage-guided-fuzzing
grandizzy Jun 14, 2025
6803a95
Merge branch 'master' into feat/coverage-guided-fuzzing
grandizzy Jun 16, 2025
5f0b1e6
Fix
grandizzy Jun 16, 2025
1e56ca3
Do not use in memory mutated corpus if coverage guided is disabled.
grandizzy Jun 16, 2025
06172a6
Merge branch 'master' into feat/coverage-guided-fuzzing
grandizzy Jun 18, 2025
d6502bd
Merge remote-tracking branch 'upstream/master' into fuzz-merge-re
grandizzy Jun 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,8 @@ yansi = { version = "1.0", features = ["detect-tty", "detect-env"] }
path-slash = "0.2"
jiff = "0.2"
heck = "0.5"
uuid = "1.17.0"
flate2 = "1.1"

## Pinned dependencies. Enabled for the workspace in crates/test-utils.

Expand Down
2 changes: 1 addition & 1 deletion crates/anvil/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ futures.workspace = true
async-trait.workspace = true

# misc
flate2 = "1.1"
flate2.workspace = true
serde_json.workspace = true
serde.workspace = true
thiserror.workspace = true
Expand Down
2 changes: 2 additions & 0 deletions crates/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ anstyle.workspace = true
terminal_size.workspace = true
ciborium.workspace = true

flate2.workspace = true

[build-dependencies]
chrono.workspace = true
vergen = { workspace = true, features = ["build", "git", "gitcl"] }
Expand Down
26 changes: 25 additions & 1 deletion crates/common/src/fs.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
//! Contains various `std::fs` wrapper functions that also contain the target path in their errors.

use crate::errors::FsPathError;
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
use serde::{de::DeserializeOwned, Serialize};
use std::{
fs::{self, File},
io::{BufWriter, Write},
io::{BufReader, BufWriter, Write},
path::{Component, Path, PathBuf},
};

Expand Down Expand Up @@ -49,6 +50,15 @@ pub fn read_json_file<T: DeserializeOwned>(path: &Path) -> Result<T> {
serde_json::from_str(&s).map_err(|source| FsPathError::ReadJson { source, path: path.into() })
}

/// Reads and decodes the json gzip file, then deserialize it into the provided type.
pub fn read_json_gzip_file<T: DeserializeOwned>(path: &Path) -> Result<T> {
let file = open(path)?;
let reader = BufReader::new(file);
let decoder = GzDecoder::new(reader);
serde_json::from_reader(decoder)
.map_err(|source| FsPathError::ReadJson { source, path: path.into() })
}

/// Writes the object as a JSON object.
pub fn write_json_file<T: Serialize>(path: &Path, obj: &T) -> Result<()> {
let file = create_file(path)?;
Expand All @@ -67,6 +77,20 @@ pub fn write_pretty_json_file<T: Serialize>(path: &Path, obj: &T) -> Result<()>
writer.flush().map_err(|e| FsPathError::write(e, path))
}

/// Writes the object as a gzip compressed file.
pub fn write_json_gzip_file<T: Serialize>(path: &Path, obj: &T) -> Result<()> {
let file = create_file(path)?;
let writer = BufWriter::new(file);
let mut encoder = GzEncoder::new(writer, Compression::default());
serde_json::to_writer(&mut encoder, obj)
.map_err(|source| FsPathError::WriteJson { source, path: path.into() })?;
encoder
.finish()
.map_err(serde_json::Error::io)
.map_err(|source| FsPathError::WriteJson { source, path: path.into() })?;
Ok(())
}

/// Wrapper for `std::fs::write`
pub fn write(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()> {
let path = path.as_ref();
Expand Down
17 changes: 17 additions & 0 deletions crates/config/src/invariant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ pub struct InvariantConfig {
pub max_assume_rejects: u32,
/// Number of runs to execute and include in the gas report.
pub gas_report_samples: u32,
/// Path where invariant corpus is stored. If not configured then coverage guided fuzzing is
/// disabled.
pub corpus_dir: Option<PathBuf>,
/// Whether corpus to use gzip file compression and decompression.
pub corpus_gzip: bool,
// Number of corpus mutations until marked as eligible to be flushed from memory.
pub corpus_min_mutations: usize,
// Number of corpus that won't be evicted from memory.
pub corpus_min_size: usize,
/// Path where invariant failures are recorded and replayed.
pub failure_persist_dir: Option<PathBuf>,
/// Whether to collect and display fuzzed selectors metrics.
Expand All @@ -47,6 +56,10 @@ impl Default for InvariantConfig {
shrink_run_limit: 5000,
max_assume_rejects: 65536,
gas_report_samples: 256,
corpus_dir: None,
corpus_gzip: true,
corpus_min_mutations: 5,
corpus_min_size: 0,
failure_persist_dir: None,
show_metrics: true,
timeout: None,
Expand All @@ -67,6 +80,10 @@ impl InvariantConfig {
shrink_run_limit: 5000,
max_assume_rejects: 65536,
gas_report_samples: 256,
corpus_dir: None,
corpus_gzip: true,
corpus_min_mutations: 5,
corpus_min_size: 0,
failure_persist_dir: Some(cache_dir),
show_metrics: true,
timeout: None,
Expand Down
2 changes: 2 additions & 0 deletions crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1059,6 +1059,7 @@ impl Config {
}
};
remove_test_dir(&self.fuzz.failure_persist_dir);
remove_test_dir(&self.invariant.corpus_dir);
remove_test_dir(&self.invariant.failure_persist_dir);

Ok(())
Expand Down Expand Up @@ -4537,6 +4538,7 @@ mod tests {
runs: 512,
depth: 10,
failure_persist_dir: Some(PathBuf::from("cache/invariant")),
corpus_dir: None,
..Default::default()
}
);
Expand Down
12 changes: 6 additions & 6 deletions crates/evm/coverage/src/inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use std::ptr::NonNull;

/// Inspector implementation for collecting coverage information.
#[derive(Clone, Debug)]
pub struct CoverageCollector {
pub struct LineCoverageCollector {
// NOTE: `current_map` is always a valid reference into `maps`.
// It is accessed only through `get_or_insert_map` which guarantees that it's valid.
// Both of these fields are unsafe to access directly outside of `*insert_map`.
Expand All @@ -21,10 +21,10 @@ pub struct CoverageCollector {
}

// SAFETY: See comments on `current_map`.
unsafe impl Send for CoverageCollector {}
unsafe impl Sync for CoverageCollector {}
unsafe impl Send for LineCoverageCollector {}
unsafe impl Sync for LineCoverageCollector {}

impl Default for CoverageCollector {
impl Default for LineCoverageCollector {
fn default() -> Self {
Self {
current_map: NonNull::dangling(),
Expand All @@ -34,7 +34,7 @@ impl Default for CoverageCollector {
}
}

impl<CTX> Inspector<CTX> for CoverageCollector
impl<CTX> Inspector<CTX> for LineCoverageCollector
where
CTX: ContextTr<Journal: JournalExt>,
{
Expand All @@ -50,7 +50,7 @@ where
}
}

impl CoverageCollector {
impl LineCoverageCollector {
/// Finish collecting coverage information and return the [`HitMaps`].
pub fn finish(self) -> HitMaps {
self.maps
Expand Down
2 changes: 1 addition & 1 deletion crates/evm/coverage/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pub mod analysis;
pub mod anchors;

mod inspector;
pub use inspector::CoverageCollector;
pub use inspector::LineCoverageCollector;

/// A coverage report.
///
Expand Down
1 change: 1 addition & 0 deletions crates/evm/evm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,4 @@ thiserror.workspace = true
tracing.workspace = true
indicatif.workspace = true
serde.workspace = true
uuid.workspace = true
4 changes: 2 additions & 2 deletions crates/evm/evm/src/executors/fuzz/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ impl FuzzedExecutor {
traces: last_run_traces,
breakpoints: last_run_breakpoints,
gas_report_traces: traces.into_iter().map(|a| a.arena).collect(),
coverage: fuzz_result.coverage,
line_coverage: fuzz_result.coverage,
deprecated_cheatcodes: fuzz_result.deprecated_cheatcodes,
};

Expand Down Expand Up @@ -258,7 +258,7 @@ impl FuzzedExecutor {
Ok(FuzzOutcome::Case(CaseOutcome {
case: FuzzCase { calldata, gas: call.gas_used, stipend: call.stipend },
traces: call.traces,
coverage: call.coverage,
coverage: call.line_coverage,
breakpoints,
logs: call.logs,
deprecated_cheatcodes,
Expand Down
Loading
Loading