Skip to content

Commit 8b0593e

Browse files
figtracerampagent
andcommitted
chore(symexec): CI fixes + rebase plumbing + import hygiene
- fmt/clippy/rustdoc fixes: const fn on BranchTrace::{len,is_empty}, allow too_many_arguments on SymBackend::propose, drop redundant clones in regression tests, fix broken intra-doc link, wrap SaferMaker URL - demote 'pub mod symexec' to 'pub(crate)' (private_interfaces lint) and trim re-exports to the surface actually used by the crate - propagate new CallDetails.value (introduced upstream in #14482) through the heuristic ABI rewrite - hoist fully-qualified inline imports (crate::executors::symexec::*, crate::inspectors::BranchTrace, alloy_primitives::U256::ZERO, std::time::*, foundry_common::fs::write_json_file, std::fs::*) to top-of-file 'use' blocks across our diff - drop unused MAX_SEEDS_PER_CYCLE constant Amp-Thread-ID: https://ampcode.com/threads/T-019e2bc9-ece8-718f-9279-971e3e982bec Co-authored-by: Amp <amp@ampcode.com>
1 parent 3af21c9 commit 8b0593e

10 files changed

Lines changed: 52 additions & 58 deletions

File tree

crates/evm/evm/src/executors/corpus.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
//! - This all happens periodically, there is no clear order in which workers export or import
3535
//! entries since it doesn't matter as long as the corpus eventually syncs across all workers
3636
37-
use crate::executors::{Executor, RawCallResult, invariant::execute_tx};
37+
use crate::executors::{Executor, RawCallResult, invariant::execute_tx, symexec::SeedSnapshot};
3838
use alloy_dyn_abi::JsonAbiExt;
3939
use alloy_json_abi::Function;
4040
use alloy_primitives::{Bytes, I256};
@@ -1138,13 +1138,13 @@ impl WorkerCorpus {
11381138
/// Snapshot a small candidate pool of seeds for symbolic-assist seed
11391139
/// scoring. Returns at most `MAX` entries (kept tiny so scoring stays
11401140
/// cheap).
1141-
pub fn symexec_seed_pool(&self) -> Vec<crate::executors::symexec::SeedSnapshot> {
1141+
pub fn symexec_seed_pool(&self) -> Vec<SeedSnapshot> {
11421142
const MAX: usize = 16;
11431143
self.in_memory_corpus
11441144
.iter()
11451145
.rev()
11461146
.take(MAX)
1147-
.map(|e| crate::executors::symexec::SeedSnapshot {
1147+
.map(|e| SeedSnapshot {
11481148
uuid: e.uuid,
11491149
tx_seq: e.tx_seq.clone(),
11501150
is_favored: e.is_favored,

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -511,8 +511,7 @@ impl<FEN: FoundryEvmNetwork> FuzzedExecutor<FEN> {
511511
// that flip an unseen branch, validates them via normal coverage,
512512
// and writes accepted candidates into its own `sync/` directory so
513513
// the existing corpus protocol distributes them.
514-
let symexec_active =
515-
worker_id == 0 && self.config.corpus.symexec_assist_active();
514+
let symexec_active = worker_id == 0 && self.config.corpus.symexec_assist_active();
516515
let symexec_interval = self.config.corpus.symexec_assist_interval.max(1);
517516
let mut runs_since_symexec: u32 = 0;
518517
let mut symexec_state = SymExecState::default();

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
// the concrete `Executor` type.
88

99
use crate::inspectors::{
10-
Cheatcodes, InspectorData, InspectorStack, cheatcodes::BroadcastableTransactions,
10+
BranchTrace, Cheatcodes, InspectorData, InspectorStack, cheatcodes::BroadcastableTransactions,
1111
};
1212
use alloy_dyn_abi::{DynSolValue, FunctionExt, JsonAbiExt};
1313
use alloy_json_abi::Function;
@@ -63,7 +63,7 @@ pub use invariant::InvariantExecutor;
6363

6464
mod corpus;
6565
mod sancov;
66-
pub mod symexec;
66+
pub(crate) mod symexec;
6767
mod trace;
6868

6969
pub use trace::TracingExecutor;
@@ -985,7 +985,7 @@ pub struct RawCallResult<FEN: FoundryEvmNetwork = EthEvmNetwork> {
985985
/// The edge coverage info collected during the call
986986
pub edge_coverage: Option<Vec<u8>>,
987987
/// Branch observations collected by the symbolic-assist worker.
988-
pub branch_trace: Option<crate::inspectors::BranchTrace>,
988+
pub branch_trace: Option<BranchTrace>,
989989
/// Sancov edge coverage from instrumented native Rust crates (e.g. precompiles).
990990
/// Tracked separately from EVM edge coverage to avoid ID-space collisions.
991991
pub sancov_coverage: Option<Vec<u8>>,

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

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,20 @@
22
//!
33
//! Implements the *worker-side* of the architectural plan inspired by
44
//! Echidna's `Echidna.SymExec.Exploration` and the SaferMaker writeup at
5-
//! https://hackmd.io/@SaferMaker/EVM-Sym-Exec.
5+
//! <https://hackmd.io/@SaferMaker/EVM-Sym-Exec>.
66
//!
77
//! It is *not* a full symbolic-execution engine; it is a directed mutation
88
//! assistant that:
99
//!
1010
//! 1. takes a corpus seed (`Vec<BasicTxDetails>`),
1111
//! 2. replays it concretely under a [`crate::inspectors::BranchTraceInspector`],
1212
//! 3. picks the deepest *unseen* opposite-side branch as the "frontier",
13-
//! 4. proposes ABI-aware calldata rewrites that — given the recovered
14-
//! compare operands — would flip the frontier branch,
15-
//! 5. validates each candidate through the normal executor (requiring a real
16-
//! new edge in coverage), and
17-
//! 6. writes accepted candidates to the master worker's `sync/` directory so
18-
//! the existing corpus protocol distributes them.
13+
//! 4. proposes ABI-aware calldata rewrites that — given the recovered compare operands — would flip
14+
//! the frontier branch,
15+
//! 5. validates each candidate through the normal executor (requiring a real new edge in coverage),
16+
//! and
17+
//! 6. writes accepted candidates to the master worker's `sync/` directory so the existing corpus
18+
//! protocol distributes them.
1919
//!
2020
//! v1 is intentionally minimal:
2121
//! - master worker only,
@@ -29,28 +29,32 @@ use crate::{
2929
inspectors::BranchTrace,
3030
};
3131
use alloy_json_abi::Function;
32-
use alloy_primitives::{B256, keccak256, map::DefaultHashBuilder};
32+
use alloy_primitives::{B256, U256, keccak256, map::DefaultHashBuilder};
3333
use eyre::Result;
34+
use foundry_common::fs::write_json_file;
3435
use foundry_evm_core::evm::FoundryEvmNetwork;
3536
use foundry_evm_fuzz::{BasicTxDetails, invariant::FuzzRunIdentifiedContracts};
36-
use std::path::Path;
37+
use std::{
38+
path::Path,
39+
time::{SystemTime, UNIX_EPOCH},
40+
};
3741

3842
mod mutate;
3943
mod select;
4044
mod types;
4145

42-
pub use mutate::propose_calldata_rewrites;
43-
pub use select::{SeedSnapshot, pick_frontier, pick_seed, score_seed, unseen_in_history};
44-
pub use types::{
45-
Candidate, FrontierKey, FrontierStats, MAX_CANDIDATES_PER_FRONTIER, MAX_FRONTIER_ATTEMPTS,
46-
MAX_SEEDS_PER_CYCLE, SymExecState,
47-
};
46+
use mutate::propose_calldata_rewrites;
47+
pub use select::SeedSnapshot;
48+
use select::pick_seed;
49+
use types::Candidate;
50+
pub use types::SymExecState;
4851

4952
/// Backend abstraction so v2 can swap the heuristic engine for a real SMT
5053
/// solver (or external `hevm`) without touching the assist loop.
5154
pub trait SymBackend {
5255
/// Generate candidate sequences from a seed and its branch trace.
5356
/// `tx_index` identifies the call to mutate (the last one, in v1).
57+
#[allow(clippy::too_many_arguments)]
5458
fn propose(
5559
&self,
5660
seed: &[BasicTxDetails],
@@ -166,7 +170,7 @@ pub fn run_symexec_assist<FEN: FoundryEvmNetwork>(
166170
tx.sender,
167171
tx.call_details.target,
168172
tx.call_details.calldata.clone(),
169-
alloy_primitives::U256::ZERO,
173+
U256::ZERO,
170174
)?;
171175
if i == tx_index
172176
&& let Some(t) = result.branch_trace.take()
@@ -181,9 +185,8 @@ pub fn run_symexec_assist<FEN: FoundryEvmNetwork>(
181185
return Ok(0);
182186
}
183187

184-
// 2. Resolve the ABI of the call slot we're allowed to mutate. For
185-
// invariant tests this is looked up from the targeted contracts;
186-
// for stateless fuzz the caller already supplied it.
188+
// 2. Resolve the ABI of the call slot we're allowed to mutate. For invariant tests this is
189+
// looked up from the targeted contracts; for stateless fuzz the caller already supplied it.
187190
let resolved_function: Option<Function> = match (function, targeted_contracts) {
188191
(Some(f), _) => Some(f.clone()),
189192
(None, Some(targets)) => seed
@@ -251,11 +254,8 @@ fn candidate_hash(seq: &[BasicTxDetails]) -> B256 {
251254
/// JSON file into a worker's `sync/` directory.
252255
pub fn write_sync_entry(sync_dir: &Path, seq: &[BasicTxDetails]) -> Result<()> {
253256
let uuid = uuid::Uuid::new_v4();
254-
let ts = std::time::SystemTime::now()
255-
.duration_since(std::time::UNIX_EPOCH)
256-
.map(|d| d.as_secs())
257-
.unwrap_or(0);
257+
let ts = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
258258
let path = sync_dir.join(format!("{uuid}-{ts}.json"));
259-
foundry_common::fs::write_json_file(&path, &seq)?;
259+
write_json_file(&path, &seq)?;
260260
Ok(())
261261
}

crates/evm/evm/src/executors/symexec/mutate.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ pub fn propose_calldata_rewrites(
9797
call_details: CallDetails {
9898
target: tx.call_details.target,
9999
calldata: Bytes::from(new_calldata),
100+
value: tx.call_details.value,
100101
},
101102
});
102103

crates/evm/evm/src/executors/symexec/select.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@ pub fn pick_seed(candidates: &[SeedSnapshot]) -> Option<&SeedSnapshot> {
4949
/// Pick a frontier from a replay trace.
5050
///
5151
/// Strategy:
52-
/// - prefer the *deepest* observation whose opposite edge is currently unseen
53-
/// (the seed already satisfies all earlier guards),
52+
/// - prefer the *deepest* observation whose opposite edge is currently unseen (the seed already
53+
/// satisfies all earlier guards),
5454
/// - skip frontiers that have hit `MAX_FRONTIER_ATTEMPTS`,
55-
/// - skip frontiers without a recoverable compare (v1 has no symbolic
56-
/// reasoning beyond ABI rewrites of compare operands).
55+
/// - skip frontiers without a recoverable compare (v1 has no symbolic reasoning beyond ABI rewrites
56+
/// of compare operands).
5757
pub fn pick_frontier<F>(
5858
trace: &BranchTrace,
5959
tx_index: u32,

crates/evm/evm/src/executors/symexec/types.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ pub struct FrontierKey {
3030
pub selector: [u8; 4],
3131
}
3232

33-
/// Per-frontier attempt bookkeeping, kept separately from [`super::corpus`]
33+
/// Per-frontier attempt bookkeeping, kept separately from the corpus
3434
/// stats so the symbolic worker doesn't pollute the fuzzer's counters.
3535
#[derive(Clone, Debug, Default)]
3636
pub struct FrontierStats {
@@ -46,9 +46,6 @@ pub const MAX_FRONTIER_ATTEMPTS: u16 = 3;
4646
/// Hard cap on candidates evaluated per frontier per cycle.
4747
pub const MAX_CANDIDATES_PER_FRONTIER: usize = 8;
4848

49-
/// Hard cap on seeds processed per assist cycle.
50-
pub const MAX_SEEDS_PER_CYCLE: usize = 1;
51-
5249
/// In-process state for the symbolic worker. Owned by the master worker; not
5350
/// persisted to disk (regenerated fresh each campaign).
5451
#[derive(Clone, Debug, Default)]

crates/evm/evm/src/inspectors/branch_trace.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,11 @@ impl BranchTrace {
121121
self.branches.clear();
122122
}
123123

124-
pub fn len(&self) -> usize {
124+
pub const fn len(&self) -> usize {
125125
self.branches.len()
126126
}
127127

128-
pub fn is_empty(&self) -> bool {
128+
pub const fn is_empty(&self) -> bool {
129129
self.branches.is_empty()
130130
}
131131
}

crates/forge/tests/cli/test_cmd/fuzz.rs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use alloy_primitives::U256;
22
use foundry_evm::fuzz::BaseCounterExample;
33
use foundry_test_utils::{TestCommand, forgetest_init, str};
44
use regex::Regex;
5+
use std::fs::{read_dir, read_to_string};
56

67
forgetest_init!(test_can_scrape_bytecode, |prj, cmd| {
78
prj.update_config(|config| config.optimizer = Some(true));
@@ -1068,18 +1069,15 @@ contract SymExecAssistTest is Test {
10681069
cmd.args(["test", "--mt", "testFuzz_FindMagic"]).assert_success();
10691070

10701071
// Master corpus directory: `<corpus_dir>/<contract>/<test>/worker0/corpus/`.
1071-
let corpus_root = prj
1072-
.root()
1073-
.join("symexec_corpus")
1074-
.join("SymExecAssistTest")
1075-
.join("testFuzz_FindMagic");
1072+
let corpus_root =
1073+
prj.root().join("symexec_corpus").join("SymExecAssistTest").join("testFuzz_FindMagic");
10761074
let master_corpus = corpus_root.join("worker0").join("corpus");
10771075
let master_sync = corpus_root.join("worker0").join("sync");
10781076

10791077
let magic_hex = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
10801078

1081-
let mut search_dirs = vec![master_corpus.clone(), master_sync.clone()];
1082-
if let Ok(entries) = std::fs::read_dir(&corpus_root) {
1079+
let mut search_dirs = vec![master_corpus, master_sync];
1080+
if let Ok(entries) = read_dir(&corpus_root) {
10831081
for entry in entries.flatten() {
10841082
let p = entry.path();
10851083
if p.is_dir() {
@@ -1091,13 +1089,13 @@ contract SymExecAssistTest is Test {
10911089

10921090
let mut found = false;
10931091
'outer: for dir in &search_dirs {
1094-
let Ok(entries) = std::fs::read_dir(dir) else { continue };
1092+
let Ok(entries) = read_dir(dir) else { continue };
10951093
for entry in entries.flatten() {
10961094
let path = entry.path();
10971095
if !path.is_file() {
10981096
continue;
10991097
}
1100-
let Ok(contents) = std::fs::read_to_string(&path) else { continue };
1098+
let Ok(contents) = read_to_string(&path) else { continue };
11011099
if contents.to_ascii_lowercase().contains(magic_hex) {
11021100
found = true;
11031101
break 'outer;

crates/forge/tests/cli/test_cmd/invariant/mod.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use alloy_primitives::U256;
22
use foundry_test_utils::{TestCommand, forgetest_init, snapbox::cmd::OutputAssert, str};
3+
use std::fs::{read_dir, read_to_string};
34

45
mod common;
56
mod handler;
@@ -1919,11 +1920,9 @@ contract SymExecAssistInvariantTest is Test {
19191920

19201921
let magic_hex = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
19211922

1922-
let mut search_dirs = vec![
1923-
corpus_root.join("worker0").join("corpus"),
1924-
corpus_root.join("worker0").join("sync"),
1925-
];
1926-
if let Ok(entries) = std::fs::read_dir(&corpus_root) {
1923+
let mut search_dirs =
1924+
vec![corpus_root.join("worker0").join("corpus"), corpus_root.join("worker0").join("sync")];
1925+
if let Ok(entries) = read_dir(&corpus_root) {
19271926
for entry in entries.flatten() {
19281927
let p = entry.path();
19291928
if p.is_dir() {
@@ -1935,13 +1934,13 @@ contract SymExecAssistInvariantTest is Test {
19351934

19361935
let mut found = false;
19371936
'outer: for dir in &search_dirs {
1938-
let Ok(entries) = std::fs::read_dir(dir) else { continue };
1937+
let Ok(entries) = read_dir(dir) else { continue };
19391938
for entry in entries.flatten() {
19401939
let path = entry.path();
19411940
if !path.is_file() {
19421941
continue;
19431942
}
1944-
let Ok(contents) = std::fs::read_to_string(&path) else { continue };
1943+
let Ok(contents) = read_to_string(&path) else { continue };
19451944
if contents.to_ascii_lowercase().contains(magic_hex) {
19461945
found = true;
19471946
break 'outer;

0 commit comments

Comments
 (0)