Skip to content

Feat/coverage guided fuzzing #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

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
401 changes: 286 additions & 115 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -268,13 +268,13 @@ chrono = { version = "0.4", default-features = false, features = [
"std",
] }
axum = "0.7"
color-eyre = "0.6"
color-eyre = { git = "https://github.com/eyre-rs/eyre" } # https://github.com/eyre-rs/eyre/issues/174
comfy-table = "7"
dirs = "6"
dunce = "1"
evm-disassembler = "0.5"
evmole = "0.7"
eyre = "0.6"
eyre = { git = "https://github.com/eyre-rs/eyre" }
figment = "0.10"
futures = "0.3"
hyper = "1.5"
Expand Down
2 changes: 1 addition & 1 deletion crates/cast/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1366,7 +1366,7 @@ impl SimpleCast {
pub fn to_unit(value: &str, unit: &str) -> Result<String> {
let value = DynSolType::coerce_str(&DynSolType::Uint(256), value)?
.as_uint()
.wrap_err("Could not convert to uint")?
.context("Could not convert to uint")?
.0;
let unit = unit.parse().wrap_err("could not parse units")?;
Ok(Self::format_unit_as_string(value, unit))
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ pub fn parse_ether_value(value: &str) -> Result<U256> {
} else {
alloy_dyn_abi::DynSolType::coerce_str(&alloy_dyn_abi::DynSolType::Uint(256), value)?
.as_uint()
.wrap_err("Could not parse ether value from string")?
.context("Could not parse ether value from string")?
.0
})
}
Expand Down
4 changes: 2 additions & 2 deletions crates/common/src/abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ pub async fn get_func_etherscan(
) -> Result<Function> {
let client = Client::new(chain, etherscan_api_key)?;
let source = find_source(client, contract).await?;
let metadata = source.items.first().wrap_err("etherscan returned empty metadata")?;
let metadata = source.items.first().context("etherscan returned empty metadata")?;

let mut abi = metadata.abi()?;
let funcs = abi.functions.remove(function_name).unwrap_or_default();
Expand All @@ -146,7 +146,7 @@ pub fn find_source(
Box::pin(async move {
trace!(%address, "find Etherscan source");
let source = client.contract_source_code(address).await?;
let metadata = source.items.first().wrap_err("Etherscan returned no data")?;
let metadata = source.items.first().context("Etherscan returned no data")?;
if metadata.proxy == 0 {
Ok(source)
} else {
Expand Down
2 changes: 1 addition & 1 deletion crates/common/src/evm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ pub struct EnvArgs {
impl EvmArgs {
/// Ensures that fork url exists and returns its reference.
pub fn ensure_fork_url(&self) -> eyre::Result<&String> {
self.fork_url.as_ref().wrap_err("Missing `--fork-url` field.")
self.fork_url.as_ref().context("Missing `--fork-url` field.")
}
}

Expand Down
6 changes: 5 additions & 1 deletion crates/config/src/fuzz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ pub struct FuzzConfig {
pub dictionary: FuzzDictionaryConfig,
/// Number of runs to execute and include in the gas report.
pub gas_report_samples: u32,
/// Path where stateless fuzz test corpus is stored.
pub corpus_dir: Option<PathBuf>,
/// Path where fuzz failures are recorded and replayed.
pub failure_persist_dir: Option<PathBuf>,
/// Name of the file to record fuzz failures, defaults to `failures`.
Expand All @@ -40,6 +42,7 @@ impl Default for FuzzConfig {
seed: None,
dictionary: FuzzDictionaryConfig::default(),
gas_report_samples: 256,
corpus_dir: None,
failure_persist_dir: None,
failure_persist_file: None,
show_logs: false,
Expand All @@ -52,7 +55,8 @@ impl FuzzConfig {
/// Creates fuzz configuration to write failures in `{PROJECT_ROOT}/cache/fuzz` dir.
pub fn new(cache_dir: PathBuf) -> Self {
Self {
failure_persist_dir: Some(cache_dir),
corpus_dir: Some(cache_dir.join("fuzz/corpus")),
failure_persist_dir: Some(cache_dir.join("fuzz")),
failure_persist_file: Some("failures".to_string()),
..Default::default()
}
Expand Down
6 changes: 5 additions & 1 deletion crates/config/src/invariant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ 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.
pub corpus_dir: Option<PathBuf>,
/// 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 +49,7 @@ impl Default for InvariantConfig {
shrink_run_limit: 5000,
max_assume_rejects: 65536,
gas_report_samples: 256,
corpus_dir: None,
failure_persist_dir: None,
show_metrics: false,
timeout: None,
Expand All @@ -67,7 +70,8 @@ impl InvariantConfig {
shrink_run_limit: 5000,
max_assume_rejects: 65536,
gas_report_samples: 256,
failure_persist_dir: Some(cache_dir),
corpus_dir: Some(cache_dir.join("invariant/corpus")),
failure_persist_dir: Some(cache_dir.join("invariant")),
show_metrics: false,
timeout: None,
show_solidity: false,
Expand Down
9 changes: 6 additions & 3 deletions crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1075,7 +1075,9 @@ impl Config {
}
}
};
remove_test_dir(&self.fuzz.corpus_dir); // TODO maybe force?
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 @@ -1859,7 +1861,7 @@ impl Config {
/// | macOS | `$HOME`/Library/Application Support/foundry | /Users/Alice/Library/Application Support/foundry |
/// | Windows | `{FOLDERID_RoamingAppData}/foundry` | C:\Users\Alice\AppData\Roaming/foundry |
pub fn data_dir() -> eyre::Result<PathBuf> {
let path = dirs::data_dir().wrap_err("Failed to find data directory")?.join("foundry");
let path = dirs::data_dir().context("Failed to find data directory")?.join("foundry");
std::fs::create_dir_all(&path).wrap_err("Failed to create module directory")?;
Ok(path)
}
Expand Down Expand Up @@ -2316,8 +2318,8 @@ impl Default for Config {
test_failures_file: "cache/test-failures".into(),
threads: None,
show_progress: false,
fuzz: FuzzConfig::new("cache/fuzz".into()),
invariant: InvariantConfig::new("cache/invariant".into()),
fuzz: FuzzConfig::new("cache".into()),
invariant: InvariantConfig::new("cache".into()),
always_use_create_2_factory: false,
ffi: false,
allow_internal_expect_revert: false,
Expand Down Expand Up @@ -4430,6 +4432,7 @@ mod tests {
runs: 512,
depth: 10,
failure_persist_dir: Some(PathBuf::from("cache/invariant")),
corpus_dir: Some(PathBuf::from("cache/invariant/corpus"),),
..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 @@ -5,7 +5,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 @@ -16,10 +16,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 @@ -29,7 +29,7 @@ impl Default for CoverageCollector {
}
}

impl<DB: Database> Inspector<DB> for CoverageCollector {
impl<DB: Database> Inspector<DB> for LineCoverageCollector {
fn initialize_interp(&mut self, interpreter: &mut Interpreter, _context: &mut EvmContext<DB>) {
get_or_insert_contract_hash(interpreter);
self.insert_map(interpreter);
Expand All @@ -42,7 +42,7 @@ impl<DB: Database> Inspector<DB> for CoverageCollector {
}
}

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 @@ -51,3 +51,4 @@ thiserror.workspace = true
tracing.workspace = true
indicatif.workspace = true
serde.workspace = true
libafl_bolts = { git = "https://github.com/AFLplusplus/LibAFL", features = ["wide"] }
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