ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Foundry fuzz 失败回放修复:消除陈旧 counterexample 对生成 run 的消耗与错误元数据记录

Foundry fuzz 失败回放修复:消除陈旧 counterexample 对生成 run 的消耗与错误元数据记录 Foundry fuzz 失败回放修复消除陈旧 counterexample 对生成 run 的消耗与错误元数据记录【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry本文解读 Foundry 仓库 changelog 条目 fuzz-stale-replay-run-accounting.md 所记录的一项forge: patch级别修复陈旧的 fuzz 失败回放stale fuzz failure replay不再消耗一次生成的 fuzz run也不再记录错误的复现元数据。文章将以该修复为骨架结合 runner.rs、fuzz executor、CLI 入口 与 fuzz 测试 等源码讲清forge fuzz replay的机制、陈旧失败的产生原因、本修复的具体行为变化以及如何验证与规避这类问题。背景forge fuzz replay是什么Foundry 的 fuzz 测试在发现反例counterexample后会把失败输入持久化到缓存目录默认cache/fuzz/failures/供后续复现。forge fuzz replay是专门用于回放这些持久化失败的子命令不指定--corpus-dir时它进入失败回放模式failure replay只针对已持久化的 fuzz/invariant 失败执行确定性回放绝不重新开始一次全新 fuzz 战役指定--corpus-dir时它转为语料库回放模式把已有语料条目重放一遍如配合 showmap 生成覆盖数据。两种模式的切换逻辑在 crates/forge/src/cmd/fuzz.rs#L212-L235async fn run(self) - ResultTestOutcome { let corpus_dir self.run.campaign.corpus_dir.clone(); if corpus_dir.is_some() self.run.fuzz_input_file.is_some() { bail!(--fuzz-input-file cannot be combined with --corpus-dir); } let mut test TestArgs::from_fuzz_run(self.run); if corpus_dir.is_none() { test.enable_fuzz_failure_replay(); return test.run().await; } // ... showmap 覆盖模式 }其中enable_fuzz_failure_replay()会把fuzz_failure_replay标志置位见 crates/forge/src/cmd/test/mod.rs#L1272-L1274该标志贯穿整个执行链最终驱动 runner.rs 中的回放分支。修复内容解读changelog 原文只有一句话Fixed stale fuzz failure replay consuming a generated run and recording incorrect reproduction metadata.拆解出三个关键点陈旧的失败stale fuzz failure源码或测试逻辑已变化但缓存里仍保留着旧的失败文件cache/fuzz/failures/Contract/testFn。回放时旧 calldata 可能已经无法复现原失败——例如测试条件放宽、逻辑修复或输入被vm.assume拒绝。消耗一次生成的 runconsuming a generated run修复前回放陈旧失败时代码路径上仍会计入一次生成的 fuzz run导致runs:统计虚高、结果报表失真。记录错误的复现元数据incorrect reproduction metadata回放应忠实地沿用失败文件中记录的原 run 号、worker、seed 等复现信息修复前陈旧失败的回放会把不正确的元数据写进新的 counterexample 记录破坏可复现性。本条目属于forge: patch级别的行为修正聚焦于无状态statelessfuzz 失败回放的记账与元数据正确性而非功能新增。回放路径源码剖析入口runner 中的回放分支在 runner.rs#L5152-L5176当fuzz_failure_replay开启时runner 先做三重校验if self.cr.mcr.tcfg.fuzz_failure_replay { let skip_reason match persisted_failure { None { Some(format!(no persisted fuzz failure found at {}, failure_file.display())) } Some(failure) if failure .calldata .get(..4) .is_none_or(|selector| func.selector() ! selector) { Some(format!(persisted fuzz failure selector does not match {}, func.name)) } Some(_) None, }; // ... fuzz_config.corpus.corpus_dir None; }无失败文件→ 跳过该测试calldata 前 4 字节函数选择器与当前测试不匹配→ 跳过这是陈旧最常见的形态之一函数签名变化后旧 calldata 对不上新函数校验通过后fuzz_config.corpus.corpus_dir被置为None确保回放不会混入语料库种子只回放持久化的失败输入。随后 runner 不再调用常规的fuzzed_executor.fuzz(...)循环而是走replay_persisted_failure(...)runner.rs#L5205-L5222。核心replay_persisted_failure 的记账逻辑回放的执行实现在 crates/evm/evm/src/executors/fuzz/mod.rs#L295-L402其文档注释明确约定Replays the persisted single-call counterexample exactly once. Unlike [Self::fuzz], this never falls through to generated inputs if the persisted calldata now succeeds or is rejected byvm.assume.即只精确回放一次且绝不会因为旧输入现在能通过了或被 assume 拒绝而退化为生成新输入。这正是不消耗生成 run修复的语义核心seed 恢复let seed failure.fuzz.seed.or(self.config.seed);并把run默认 1与worker默认 0还原调用cheats.set_seed(...)重建与原战役一致的确定性环境单次调用用持久化的 calldata/value 构造一笔交易call_raw执行一次assume 拒绝处理若结果等于MAGIC_ASSUME直接返回skipped原因为persisted fuzz failure rejected by vm.assume——此时不产生 counterexample、不计 runskip 处理vm.skip等跳过后同样以skipped退出不记账成功/失败记账若回放成功不再失败success true只记录first_case、gas、覆盖率、日志等执行事实若回放仍然失败构造CounterExample::Single(...)并通过FuzzRunMetadata::new(seed, failure.fuzz.run, Some(worker))从原始失败文件中继承 seed/run/worker 元数据而不是凭空生成新的 run 编号。从源码结构看本修复涉及的就是replay_persisted_failure的返回路径无论陈旧失败最终仍失败已修复还是被 assume 拒绝该函数都在单次回放后立即返回FuzzTestResultrun 计数不会被多算复现元数据也严格取自持久化文件。持久化与反持久化的对称性正常 fuzz 战役发现反例后runner 在 runner.rs#L5231-L5242 将 counterexample 写回failure_dirif !self.cr.mcr.tcfg.fuzz_failure_replay let Some(CounterExample::Single(counterexample)) result.counterexample { foundry_common::fs::create_dir_all(failure_dir)?; foundry_common::fs::write_json_file(failure_file, counterexample)?; }注意写入被!fuzz_failure_replay守卫回放模式不会再次落盘避免回放产生的元数据污染原始失败文件。BaseCounterExample中with_fuzz_metadata(FuzzRunMetadata)携带的 seed/run/worker 字段正是回放时还原确定性环境的依据也是本次修复记录正确的 reproduction metadata所守护的数据结构。陈旧失败如何产生结合代码可以归纳三类典型的stale来源场景表现回放结果源码修复后未清缓存旧 calldata 不再触发 revert回放成功返回successtrue函数签名/选择器变更旧 calldata 前 4 字节对不上runner 直接[SKIP: persisted fuzz failure selector does not match ...]测试加入vm.assume约束旧输入被过滤返回[SKIP: ... rejected by vm.assume]在 fuzz.rs 测试 中forge_fuzz_replay_reports_missing_corpus、forge_fuzz_replay_invariant_skips_without_persisted_failure等用例验证了无失败可回放时跳过而非新开战役的行为forge_fuzz_replay_replays_persisted_fuzz_failure则验证了回放仍失败的场景会输出[FAIL: EvmError: Revert; counterexample: calldata0x... args[200]]且runs: 0——run 计数为 0 正是不消耗生成 run的直接可观测证据。修复带来的可观测行为变化修复前后用户可见的行为差异可总结为run 统计修复前陈旧失败回放会计入 1 次 run报表显示runs: 1之类修复后回放路径不消耗任何生成 runruns: 0统计与回放≠生成的语义一致元数据修复前回放产生的新 counterexample 可能携带错误的 run/worker/seed修复后严格继承持久化文件中的FuzzRunMetadata保证同一失败任意次回放结果与元数据完全一致回归语义陈旧失败现在能通过时回放如实报告成功/跳过而不会偷偷生成新输入去撞新的反例——要验证修复后的行为必须重新跑一次完整 fuzz 战役并清空cache/fuzz/failures/。实践建议与验证方法复现陈旧失败场景先让一个 fuzz 测试失败如forge test --match-test testFuzz_reverts -q确认cache/fuzz/failures/Contract/fn生成随后修改测试放宽条件或改函数签名再执行forge fuzz replay --mc Contract -vvv观察输出签名不匹配时应看到 selector 跳过信息条件放宽时应看到回放成功或 assume 拒绝且runs:不增长。清缓存习惯改动测试逻辑后主动清理cache/fuzz/failures/避免陈旧失败干扰 CI 与本地回放结论。配合--fuzz-input-file精确回放fuzz replay支持--fuzz-input-file PATH指定单个失败文件回放见 cmd/fuzz.rs#L199-L201且与--corpus-dir互斥多文件匹配多个测试时会拒绝执行保证回放目标唯一。回归测试参考仓库内 crates/forge/tests/cli/test_cmd/fuzz.rs 的forge_fuzz_replay_*系列用例是验证该行为的权威参考修改相关行为时可用它们作为回归基线。小结本 changelog 条目记录的是 Foundry 在 fuzz 失败回放链路上一处细致的正确性修复让陈旧的失败回放不再消耗生成的 run、不再写入错误的复现元数据。其背后是forge fuzz replay严格遵循只回放、不生成的执行模型——单次调用、无回退、元数据继承自持久化文件。理解这一模型有助于在 CI 与本地复现流程中正确解读回放结果避免把回放通过误读为问题已修复。【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表