Skip to content

Commit e6de72c

Browse files
0xalpharushgrandizzyDaniPopes
authored
feat(forge): coverage guided fuzzing & time based campaigns for invariant mode (#10190)
* rename coverage to line coverage for clarity * WIP: coverage guided fuzzing * wip persist invariant corpus * add binning and history map * rm proptest runner, add corpus mutations * fix: splice mutation, add some notes * Clippy and more tests * save * use libafl_bolt's SIMD hitmap * fix eyre issues * add comments and psuedocode * Revert libafl * Typo * Fix win config test * cleanup, save corpus at the end of run, if new coverage * consolidate corpus manager * Consolidate tx manager corpus logic * Review changes: do not stop fuzzing if corpus replay failures, report number of failures, uuids for corpus file * Default gzip corpus and config to toggle json/gzip * Evict oldest corpus with more than x mutations * Add min corpus size config, bump max mutations to default depth run * Simplify corpus manager and corpus struct, enable prefix / suffix mutation, manager to handle generate from strategy * Fuzz arg from ABI * Corpus max mutations default 5 * Save metadata on disk at eviction time * Remove more than 2 branches branch, make sure we always have one * Load gz and json seeds, ignore metadata files * ABI mutation replaces subset of arguments sometimes * prevent empty range but perform at least 1 round * trim selector when using abi_decode_input * Nit, remove clippy allow * retain corpus items that are highly likely to produce new finds * rename corpus_max_mutations to corpus_min_mutations * update cli test expectations * Stateless fuzz corpus config revert, add invariant time based campaigns * Changes after review - revert cache dir configs, invariant corpus can be external of cache - save and load as json.gz - comment update - introduce mutation type enum * Remove outdated comment * Update crates/evm/evm/src/executors/mod.rs Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com> * Changes after review: comment, update merge_edge_coverage, use rng.gen * Fix docs * Keep test assert, found faster than without guidance * Fix * Do not use in memory mutated corpus if coverage guided is disabled. --------- Co-authored-by: grandizzy <grandizzy.the.egg@gmail.com> Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com> Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com>
1 parent 2b3f9ff commit e6de72c

File tree

29 files changed

+919
-169
lines changed

29 files changed

+919
-169
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,8 @@ yansi = { version = "1.0", features = ["detect-tty", "detect-env"] }
349349
path-slash = "0.2"
350350
jiff = "0.2"
351351
heck = "0.5"
352+
uuid = "1.17.0"
353+
flate2 = "1.1"
352354

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

crates/anvil/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ futures.workspace = true
8484
async-trait.workspace = true
8585

8686
# misc
87-
flate2 = "1.1"
87+
flate2.workspace = true
8888
serde_json.workspace = true
8989
serde.workspace = true
9090
thiserror.workspace = true

crates/common/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ anstyle.workspace = true
7373
terminal_size.workspace = true
7474
ciborium.workspace = true
7575

76+
flate2.workspace = true
77+
7678
[build-dependencies]
7779
chrono.workspace = true
7880
vergen = { workspace = true, features = ["build", "git", "gitcl"] }

crates/common/src/fs.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
//! Contains various `std::fs` wrapper functions that also contain the target path in their errors.
22
33
use crate::errors::FsPathError;
4+
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
45
use serde::{de::DeserializeOwned, Serialize};
56
use std::{
67
fs::{self, File},
7-
io::{BufWriter, Write},
8+
io::{BufReader, BufWriter, Write},
89
path::{Component, Path, PathBuf},
910
};
1011

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

53+
/// Reads and decodes the json gzip file, then deserialize it into the provided type.
54+
pub fn read_json_gzip_file<T: DeserializeOwned>(path: &Path) -> Result<T> {
55+
let file = open(path)?;
56+
let reader = BufReader::new(file);
57+
let decoder = GzDecoder::new(reader);
58+
serde_json::from_reader(decoder)
59+
.map_err(|source| FsPathError::ReadJson { source, path: path.into() })
60+
}
61+
5262
/// Writes the object as a JSON object.
5363
pub fn write_json_file<T: Serialize>(path: &Path, obj: &T) -> Result<()> {
5464
let file = create_file(path)?;
@@ -67,6 +77,20 @@ pub fn write_pretty_json_file<T: Serialize>(path: &Path, obj: &T) -> Result<()>
6777
writer.flush().map_err(|e| FsPathError::write(e, path))
6878
}
6979

80+
/// Writes the object as a gzip compressed file.
81+
pub fn write_json_gzip_file<T: Serialize>(path: &Path, obj: &T) -> Result<()> {
82+
let file = create_file(path)?;
83+
let writer = BufWriter::new(file);
84+
let mut encoder = GzEncoder::new(writer, Compression::default());
85+
serde_json::to_writer(&mut encoder, obj)
86+
.map_err(|source| FsPathError::WriteJson { source, path: path.into() })?;
87+
encoder
88+
.finish()
89+
.map_err(serde_json::Error::io)
90+
.map_err(|source| FsPathError::WriteJson { source, path: path.into() })?;
91+
Ok(())
92+
}
93+
7094
/// Wrapper for `std::fs::write`
7195
pub fn write(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()> {
7296
let path = path.as_ref();

crates/config/src/invariant.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ pub struct InvariantConfig {
2626
pub max_assume_rejects: u32,
2727
/// Number of runs to execute and include in the gas report.
2828
pub gas_report_samples: u32,
29+
/// Path where invariant corpus is stored. If not configured then coverage guided fuzzing is
30+
/// disabled.
31+
pub corpus_dir: Option<PathBuf>,
32+
/// Whether corpus to use gzip file compression and decompression.
33+
pub corpus_gzip: bool,
34+
// Number of corpus mutations until marked as eligible to be flushed from memory.
35+
pub corpus_min_mutations: usize,
36+
// Number of corpus that won't be evicted from memory.
37+
pub corpus_min_size: usize,
2938
/// Path where invariant failures are recorded and replayed.
3039
pub failure_persist_dir: Option<PathBuf>,
3140
/// Whether to collect and display fuzzed selectors metrics.
@@ -47,6 +56,10 @@ impl Default for InvariantConfig {
4756
shrink_run_limit: 5000,
4857
max_assume_rejects: 65536,
4958
gas_report_samples: 256,
59+
corpus_dir: None,
60+
corpus_gzip: true,
61+
corpus_min_mutations: 5,
62+
corpus_min_size: 0,
5063
failure_persist_dir: None,
5164
show_metrics: true,
5265
timeout: None,
@@ -67,6 +80,10 @@ impl InvariantConfig {
6780
shrink_run_limit: 5000,
6881
max_assume_rejects: 65536,
6982
gas_report_samples: 256,
83+
corpus_dir: None,
84+
corpus_gzip: true,
85+
corpus_min_mutations: 5,
86+
corpus_min_size: 0,
7087
failure_persist_dir: Some(cache_dir),
7188
show_metrics: true,
7289
timeout: None,

crates/config/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,6 +1059,7 @@ impl Config {
10591059
}
10601060
};
10611061
remove_test_dir(&self.fuzz.failure_persist_dir);
1062+
remove_test_dir(&self.invariant.corpus_dir);
10621063
remove_test_dir(&self.invariant.failure_persist_dir);
10631064

10641065
Ok(())
@@ -4537,6 +4538,7 @@ mod tests {
45374538
runs: 512,
45384539
depth: 10,
45394540
failure_persist_dir: Some(PathBuf::from("cache/invariant")),
4541+
corpus_dir: None,
45404542
..Default::default()
45414543
}
45424544
);

crates/evm/coverage/src/inspector.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use std::ptr::NonNull;
1010

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

2323
// SAFETY: See comments on `current_map`.
24-
unsafe impl Send for CoverageCollector {}
25-
unsafe impl Sync for CoverageCollector {}
24+
unsafe impl Send for LineCoverageCollector {}
25+
unsafe impl Sync for LineCoverageCollector {}
2626

27-
impl Default for CoverageCollector {
27+
impl Default for LineCoverageCollector {
2828
fn default() -> Self {
2929
Self {
3030
current_map: NonNull::dangling(),
@@ -34,7 +34,7 @@ impl Default for CoverageCollector {
3434
}
3535
}
3636

37-
impl<CTX> Inspector<CTX> for CoverageCollector
37+
impl<CTX> Inspector<CTX> for LineCoverageCollector
3838
where
3939
CTX: ContextTr<Journal: JournalExt>,
4040
{
@@ -50,7 +50,7 @@ where
5050
}
5151
}
5252

53-
impl CoverageCollector {
53+
impl LineCoverageCollector {
5454
/// Finish collecting coverage information and return the [`HitMaps`].
5555
pub fn finish(self) -> HitMaps {
5656
self.maps

crates/evm/coverage/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ pub mod analysis;
2929
pub mod anchors;
3030

3131
mod inspector;
32-
pub use inspector::CoverageCollector;
32+
pub use inspector::LineCoverageCollector;
3333

3434
/// A coverage report.
3535
///

crates/evm/evm/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,4 @@ thiserror.workspace = true
5252
tracing.workspace = true
5353
indicatif.workspace = true
5454
serde.workspace = true
55+
uuid.workspace = true

crates/evm/evm/src/executors/fuzz/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ impl FuzzedExecutor {
181181
traces: last_run_traces,
182182
breakpoints: last_run_breakpoints,
183183
gas_report_traces: traces.into_iter().map(|a| a.arena).collect(),
184-
coverage: fuzz_result.coverage,
184+
line_coverage: fuzz_result.coverage,
185185
deprecated_cheatcodes: fuzz_result.deprecated_cheatcodes,
186186
};
187187

@@ -258,7 +258,7 @@ impl FuzzedExecutor {
258258
Ok(FuzzOutcome::Case(CaseOutcome {
259259
case: FuzzCase { calldata, gas: call.gas_used, stipend: call.stipend },
260260
traces: call.traces,
261-
coverage: call.coverage,
261+
coverage: call.line_coverage,
262262
breakpoints,
263263
logs: call.logs,
264264
deprecated_cheatcodes,

0 commit comments

Comments
 (0)