|
12 | 12 | //! |
13 | 13 | //! Output is consumable by tools like `riesentoaster/differential-coverage`. |
14 | 14 |
|
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, |
20 | 25 | }; |
| 26 | +use alloy_dyn_abi::JsonAbiExt; |
21 | 27 | use alloy_json_abi::Function; |
22 | | -use alloy_primitives::{Address, B256, hex}; |
| 28 | +use alloy_primitives::{Address, B256, Selector, hex}; |
23 | 29 | use eyre::Result; |
24 | | -use foundry_evm_core::evm::FoundryEvmNetwork; |
| 30 | +use foundry_evm_core::{constants::CHEATCODE_ADDRESS, evm::FoundryEvmNetwork}; |
25 | 31 | use foundry_evm_coverage::HitMaps; |
26 | | -use foundry_evm_fuzz::invariant::FuzzRunIdentifiedContracts; |
| 32 | +use foundry_evm_fuzz::{BasicTxDetails, invariant::FuzzRunIdentifiedContracts}; |
27 | 33 | use std::{ |
28 | 34 | collections::BTreeMap, |
29 | 35 | fmt, |
@@ -95,6 +101,28 @@ pub struct ShowmapStats { |
95 | 101 | pub sancov_observed: bool, |
96 | 102 | } |
97 | 103 |
|
| 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 | + |
98 | 126 | /// Replay every corpus entry under `corpus_dir` and emit showmap files. |
99 | 127 | /// |
100 | 128 | /// `fuzzed_function` is set for stateless fuzz tests; `fuzzed_contracts` is set |
@@ -206,6 +234,112 @@ pub fn replay_corpus_to_showmap<FEN: FoundryEvmNetwork>( |
206 | 234 | Ok(stats) |
207 | 235 | } |
208 | 236 |
|
| 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 | + |
209 | 343 | /// Saturating-add per-(bytecode, pc) hits from a `HitMaps` snapshot into `dst`. |
210 | 344 | fn accumulate_evm(dst: &mut BTreeMap<(B256, u32), u64>, src: Option<&HitMaps>) { |
211 | 345 | let Some(maps) = src else { return }; |
|
0 commit comments