Skip to content

Commit caccaeb

Browse files
committed
feat: replay-driven fuzz minimization
1 parent 807f37c commit caccaeb

7 files changed

Lines changed: 694 additions & 45 deletions

File tree

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

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,10 @@ pub use corpus::DynamicTargetCtx;
7575
pub use corpus_io::{
7676
CorpusDirEntry, canonical_replay_dirs, parse_corpus_filename, read_corpus_dir, read_corpus_tree,
7777
};
78-
pub use showmap::{ShowmapDomain, ShowmapOpts, ShowmapStats, replay_corpus_to_showmap};
78+
pub use showmap::{
79+
MinimizationReplayInput, ReplayObservation, ShowmapDomain, ShowmapOpts, ShowmapStats,
80+
replay_corpus_to_showmap, replay_sequence_for_minimization,
81+
};
7982
pub use trace::TracingExecutor;
8083

8184
const DURATION_BETWEEN_METRICS_REPORT: Duration = Duration::from_secs(5);
@@ -1167,10 +1170,7 @@ impl<FEN: FoundryEvmNetwork> RawCallResult<FEN> {
11671170
for hit in hits.drain(..) {
11681171
let edge_index = edge_indices.edge_index(hit.edge);
11691172
if history_map.len() <= edge_index {
1170-
debug_assert_eq!(history_map.len(), edge_index);
1171-
// `Vec::push` already amortizes geometric growth; no need
1172-
// to pre-reserve a single slot.
1173-
history_map.push(0);
1173+
history_map.resize(edge_index + 1, 0);
11741174
}
11751175
Self::merge_edge_count(
11761176
hit.count,
@@ -1499,6 +1499,24 @@ mod tests {
14991499
assert_eq!(history, [1, 1]);
15001500
}
15011501

1502+
#[test]
1503+
fn collision_free_edge_merge_handles_sparse_observation_indices() {
1504+
let first =
1505+
EdgeKey { address: Address::ZERO, depth: None, pc: 0, jump_dest: U256::from(10) };
1506+
let second =
1507+
EdgeKey { address: Address::ZERO, depth: None, pc: 0, jump_dest: U256::from(20) };
1508+
let mut edge_indices = EdgeIndexMap::default();
1509+
edge_indices.edge_index(first);
1510+
edge_indices.edge_index(second);
1511+
let mut history = Vec::new();
1512+
1513+
assert_eq!(
1514+
dense_call(second).merge_edge_coverage(&mut history, &mut edge_indices),
1515+
(true, true)
1516+
);
1517+
assert_eq!(history, [0, 1]);
1518+
}
1519+
15021520
#[test]
15031521
fn cheatcode_skip_payload_is_classified_as_skip() {
15041522
let raw = RawCallResult::<EthEvmNetwork> {

crates/evm/evm/src/executors/showmap.rs

Lines changed: 142 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,24 @@
1212
//!
1313
//! Output is consumable by tools like `riesentoaster/differential-coverage`.
1414
15-
use crate::executors::{
16-
Executor,
17-
corpus::{DynamicTargetCtx, WorkerCorpus, register_replay_created, rollback_replay_created},
18-
corpus_io::read_corpus_tree,
19-
invariant::execute_tx,
15+
use crate::{
16+
executors::{
17+
Executor,
18+
corpus::{
19+
DynamicTargetCtx, WorkerCorpus, register_replay_created, rollback_replay_created,
20+
},
21+
corpus_io::read_corpus_tree,
22+
invariant::{call_invariant_function, execute_tx},
23+
},
24+
inspectors::EdgeIndexMap,
2025
};
26+
use alloy_dyn_abi::JsonAbiExt;
2127
use alloy_json_abi::Function;
22-
use alloy_primitives::{Address, B256, hex};
28+
use alloy_primitives::{Address, B256, Selector, hex};
2329
use eyre::Result;
24-
use foundry_evm_core::evm::FoundryEvmNetwork;
30+
use foundry_evm_core::{constants::CHEATCODE_ADDRESS, evm::FoundryEvmNetwork};
2531
use foundry_evm_coverage::HitMaps;
26-
use foundry_evm_fuzz::invariant::FuzzRunIdentifiedContracts;
32+
use foundry_evm_fuzz::{BasicTxDetails, invariant::FuzzRunIdentifiedContracts};
2733
use std::{
2834
collections::BTreeMap,
2935
fmt,
@@ -95,6 +101,28 @@ pub struct ShowmapStats {
95101
pub sancov_observed: bool,
96102
}
97103

104+
/// Facts observed while replaying one candidate for minimization.
105+
#[derive(Clone, Debug, Default, PartialEq, Eq)]
106+
pub struct ReplayObservation {
107+
/// AFL-bucketed EVM edge coverage for the candidate.
108+
pub evm_edges: Vec<u8>,
109+
/// AFL-bucketed native sancov edge coverage for the candidate.
110+
pub sancov_edges: Vec<u8>,
111+
/// Comparable failure identity, if replaying this candidate fails.
112+
pub failure: Option<String>,
113+
/// Number of replayable transactions executed.
114+
pub replayed: usize,
115+
/// Number of transactions skipped because they do not target this fuzz/invariant context.
116+
pub skipped: usize,
117+
}
118+
119+
impl ReplayObservation {
120+
pub fn has_coverage(&self) -> bool {
121+
self.evm_edges.iter().any(|&edge| edge != 0)
122+
|| self.sancov_edges.iter().any(|&edge| edge != 0)
123+
}
124+
}
125+
98126
/// Replay every corpus entry under `corpus_dir` and emit showmap files.
99127
///
100128
/// `fuzzed_function` is set for stateless fuzz tests; `fuzzed_contracts` is set
@@ -206,6 +234,112 @@ pub fn replay_corpus_to_showmap<FEN: FoundryEvmNetwork>(
206234
Ok(stats)
207235
}
208236

237+
/// Replay one in-memory candidate and return edge/failure observations.
238+
pub struct MinimizationReplayInput<'a> {
239+
pub sequence: &'a [BasicTxDetails],
240+
pub evm_edge_indices: &'a mut EdgeIndexMap,
241+
}
242+
243+
pub fn replay_sequence_for_minimization<FEN: FoundryEvmNetwork>(
244+
executor: &Executor<FEN>,
245+
input: MinimizationReplayInput<'_>,
246+
fuzzed_function: Option<&Function>,
247+
fuzzed_contracts: Option<&FuzzRunIdentifiedContracts>,
248+
invariant_address: Option<Address>,
249+
invariant_fns: &[&Function],
250+
dynamic: Option<&DynamicTargetCtx<'_>>,
251+
) -> Result<ReplayObservation> {
252+
let mut observation = ReplayObservation::default();
253+
let mut executor = executor.clone();
254+
executor.inspector_mut().collect_edge_coverage(true);
255+
executor.inspector_mut().collect_sancov_edges(true);
256+
257+
let mut created = Vec::new();
258+
for tx in input.sequence {
259+
if !WorkerCorpus::can_replay_tx(tx, fuzzed_function, fuzzed_contracts) {
260+
observation.skipped += 1;
261+
continue;
262+
}
263+
observation.replayed += 1;
264+
let mut call_result = execute_tx(&mut executor, tx)?;
265+
let target = tx.call_details.target;
266+
let selector =
267+
tx.call_details.calldata.get(..4).map(Selector::from_slice).unwrap_or_default();
268+
let reverter = call_result.reverter;
269+
let reverted = call_result.reverted;
270+
call_result.merge_all_coverage(
271+
&mut observation.evm_edges,
272+
input.evm_edge_indices,
273+
&mut observation.sancov_edges,
274+
);
275+
276+
register_replay_created(
277+
&call_result.state_changeset,
278+
dynamic,
279+
fuzzed_contracts,
280+
&mut created,
281+
);
282+
283+
if fuzzed_contracts.is_some() {
284+
if observation.failure.is_none() {
285+
observation.failure =
286+
invariant_handler_failure(target, selector, reverter, reverted);
287+
}
288+
executor.commit(&mut call_result);
289+
if observation.failure.is_none()
290+
&& let Some(address) = invariant_address
291+
&& let Some(name) = broken_invariant(&executor, address, invariant_fns)?
292+
{
293+
observation.failure = Some(format!("invariant:{name}"));
294+
}
295+
} else {
296+
let success = if call_result
297+
.reverter
298+
.is_some_and(|reverter| reverter != target && reverter != CHEATCODE_ADDRESS)
299+
{
300+
true
301+
} else {
302+
executor.is_raw_call_mut_success(target, &mut call_result, false)
303+
};
304+
if !success && observation.failure.is_none() {
305+
observation.failure = Some(format!(
306+
"fuzz:{target:?}:{selector:?}:{reverter:?}:{:?}",
307+
call_result.exit_reason
308+
));
309+
}
310+
}
311+
}
312+
rollback_replay_created(fuzzed_contracts, created);
313+
Ok(observation)
314+
}
315+
316+
fn invariant_handler_failure(
317+
target: Address,
318+
selector: Selector,
319+
reverter: Option<Address>,
320+
reverted: bool,
321+
) -> Option<String> {
322+
reverted.then(|| format!("handler:{target:?}:{selector:?}:{reverter:?}"))
323+
}
324+
325+
fn broken_invariant<FEN: FoundryEvmNetwork>(
326+
executor: &Executor<FEN>,
327+
invariant_address: Address,
328+
invariant_fns: &[&Function],
329+
) -> Result<Option<String>> {
330+
for invariant in invariant_fns {
331+
let (_, success) = call_invariant_function(
332+
executor,
333+
invariant_address,
334+
(*invariant).abi_encode_input(&[])?.into(),
335+
)?;
336+
if !success {
337+
return Ok(Some(invariant.name.clone()));
338+
}
339+
}
340+
Ok(None)
341+
}
342+
209343
/// Saturating-add per-(bytecode, pc) hits from a `HitMaps` snapshot into `dst`.
210344
fn accumulate_evm(dst: &mut BTreeMap<(B256, u32), u64>, src: Option<&HitMaps>) {
211345
let Some(maps) = src else { return };

0 commit comments

Comments
 (0)