ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Foundry EVM Sancov:用 LLVM SanitizerCoverage 为 Rust 原生代码做覆盖率引导模糊测试

Foundry EVM Sancov:用 LLVM SanitizerCoverage 为 Rust 原生代码做覆盖率引导模糊测试 Foundry EVM Sancov用 LLVM SanitizerCoverage 为 Rust 原生代码做覆盖率引导模糊测试【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry本文围绕crates/evm/sancov子包的 READMEcrates/evm/sancov/README.md展开讲解 Foundry 中一套针对原生 Rust 代码预编译实现、revm 内部逻辑等的覆盖率引导模糊测试机制通过RUSTC_WRAPPER注入 LLVM SanitizerCoverage 插桩在模糊测试时用原生代码的边覆盖edge coverage指导变异并把比较运算的操作数trace-cmp注入模糊词典以破解比较守卫。读完本文你可以完整掌握sancov_edges、sancov_trace_cmp、corpus_dir三个配置项的语义与组合行为、sancov 构建包装脚本的编写方式、覆盖率回调的底层实现__sanitizer_cov_trace_pc_guard等以及如何用forge test --showmap-out把持久化语料回放为 AFL 风格的覆盖率文件。1. 解决什么问题EVM 字节码覆盖之外还要看见 Rust 原生代码Foundry 的模糊/不变量测试默认以EVM 字节码层面的边覆盖作为引导信号。但一笔交易在执行过程中还会经过大量原生 Rust 代码——预编译合约precompile的实现、revm 内部的处理逻辑等。这些代码中的边界检查、余额校验、溢出守卫对 EVM 边覆盖来说是黑盒。foundry-evm-sancovcrates/evm/sancov/Cargo.toml包描述即 SanitizerCoverage callbacks for coverage-guided fuzzing of native Rust code提供两类能力边覆盖采集实现 LLVM SanitizerCoverage 回调把插桩后 Rust 代码的 CFG 边命中记录到一个由模糊执行器提供的缓冲区作为变异引导信号比较操作数采集trace-cmp捕获插桩代码中比较指令的操作数交给模糊器的词典帮助它求解比较守卫如余额检查、溢出保护等分支条件。按 README 的说法其使用前提是forge 必须以某种能注入 sancov 编译标志的RUSTC_WRAPPER重新构建。只有经过插桩的 crate 才会触发这些回调因此运行时不需要额外过滤crates/evm/sancov/src/lib.rs 第 1112 行的模块文档同样强调 Only crates compiled with sancov instrumentation … will trigger these callbacks — no runtime filtering needed。2. 配置项语义sancov_edges、sancov_trace_cmp与corpus_dir的组合行为README 给出的最小配置如下[invariant]段fuzz 测试同理对应[fuzz]段[invariant] sancov_edges true sancov_trace_cmp true corpus_dir corpus/invariant这三个开关的行为在 crates/config/src/fuzz.rs 的FuzzCorpusConfig中有精确实现配置项默认值作用corpus_dirNone语料目录一旦设置即开启覆盖引导模糊测试is_coverage_guided()产生新覆盖的序列会被持久化并参与变异sancov_edgesfalse从 SanitizerCoverage 插桩的原生 crate 采集边覆盖作为引导信号sancov_trace_cmpfalse从插桩 crate 捕获比较操作数并注入模糊词典独立于sancov_edges配置项对应的判定函数crates/config/src/fuzz.rscollect_edge_coverage()corpus_dir已设置、show_edge_coverage或sancov_edges三者任一成立即需要采集某种边覆盖collect_evm_edge_coverage()!sancov_edges (corpus_dir.is_some() || show_edge_coverage)。这就是 README 中开启sancov_edges时 EVMEdgeCovInspector被自动禁用的出处——源码注释解释其理由为sancov 已提供覆盖信号Solidity 处理侧的 EVM 命中只会稀释信号。注意仅 trace-cmp 模式不会禁用 EVM 边覆盖因为 trace-cmp 只贡献词典条目、不提供边覆盖collect_evm_cmp_log()EVM 比较操作数采集同样在sancov_edges开启时被禁用分支前沿frontier_dir的采集不受影响因为它面向 Solidity 字节码分支sancov_active()任一 sancov 模式开启即为真。README 中的三句话配置语义可以精确概括为sancov_edges true→ EVM 边覆盖被替换为 sancov 边覆盖EdgeCovInspector自动禁用corpus_dir已设置且sancov_edges关闭 → 自动采集 EVM 边覆盖与 EVM 比较操作数用于覆盖引导模糊测试这是不带插桩时的普通覆盖引导模式sancov_trace_cmp true→ 只额外从插桩原生代码追加比较操作数到词典与边覆盖采集正交。3. 回调实现边覆盖如何被记录foundry-evm-sancov的全部实现集中在单文件 crates/evm/sancov/src/lib.rs其结构与 LLVM SanitizerCoverage 的约定一一对应3.1 边覆盖pc-guard回调LLVM 插桩后每条 CFG 边对应一个u32guard 槽位运行时在每个插桩点调用__sanitizer_cov_trace_pc_guard。lib.rs 中__sanitizer_cov_trace_pc_guard_init(start, stop)lib.rs#L95-L104程序启动时由 SanitizerCoverage 运行时调用把[start, stop)的 guard 槽位依次写入从 1 开始的编号GUARD_COUNTER使每个 guard 获得稳定 ID__sanitizer_cov_trace_pc_guard(guard)lib.rs#L111-L118每次边命中时读取 guard 中的 ID 并调用record_hit。record_hitlib.rs#L44-L82的设计兼顾热路径性能全局COVERAGE_MAP_PTR/LENAtomicPtr/AtomicUsize指向执行器提供的命中缓冲区若未激活则直接返回快路径持读锁查GUARD_LOOKUPguard ID → 稠密索引的映射慢路径持写锁为新 guard ID 分配稠密索引NEXT_SANCOV_IDX.fetch_add对缓冲区中对应槽位执行wrapping_add(1)计数。执行器侧通过三个 API 管理这个缓冲区set_coverage_map(ptr, len)激活、clear_coverage_map()停用、is_active()查询sancov_edge_count()返回目前已发现的唯一 sancov 边数lib.rs#L21-L35、lib.rs#L84-L87。3.2 比较操作数trace-cmp回调trace-cmp 部分暴露__sanitizer_cov_trace_cmp1/2/4/8、__sanitizer_cov_trace_const_cmp1/2/4/8以及__sanitizer_cov_trace_switch共 9 个回调lib.rs#L179-L258全部汇入record_cmp只在is_active()即 trace-cmp 模式开启时记录arg1 0 arg2 0直接丢弃每条线程用thread_local!的CMP_OPERANDS缓冲上限MAX_CMP_OPERANDS 512操作数以大端、右对齐方式写入 32 字节缓冲buf[24..]CmpSample { width, value }携带原始比较位宽8/16/32/64arg2仅在非零且与arg1不同时才记录去重冗余switch回调中cases[0]为分支数、cases[2..]为各 case 常量回调对最多 16 个 case 常量逐一与比较值val配对记录帮助模糊器命中多路分支。采集到的样本由drain_cmp_operands()一次性取出mem::take、clear_cmp_operands()清空lib.rs#L166-L177。3.3 执行器侧接线模糊执行器crates/evm/evm的 executors把两条线索串起来每次执行调用前若sancov_edges || sancov_trace_cmp创建 RAII 的SancovGuardsancov::SancovGuard::new(...)来激活/设置覆盖缓冲与 trace-cmp 捕获crates/evm/evm/src/executors/mod.rs 等处fuzz 与 invariant 的多个执行路径均如此调用结果结构携带sancov_coverage: OptionVecu8边命中缓冲与sancov_cmp_values: OptionVecCmpSample比较操作数并提供merge_sancov_coverage等方法把命中并入历史图executors/mod.rs、#L1640-L1676不变量执行器随后把 sancov trace-cmp 操作数按类型注入模糊词典crates/evm/evm/src/executors/invariant/mod.rsif let Some(cmp_values) call_result.sancov_cmp_values { ... }crates/forge/src/runner.rs在装配 fuzz/invariant 执行器时透传配置executor.inspector_mut().collect_sancov_edges(fuzz_config.corpus.collect_sancov_edges())等crates/forge/src/runner.rs、#L4044-L4047语料回放阶段若请求了 sancov 覆盖却未观察到任何命中runner 会给出提示sancov coverage requested but no hits observed (build is likely not sancov-instrumented)crates/forge/src/runner.rs——这是判断构建时是否真的插桩成功的实用信号。4. 构建方法编写注入 sancov 标志的RUSTC_WRAPPERREADME 给出的完整包装脚本可按需把your_target_crate替换为你要插桩的 crate如预编译实现所在 crate#!/usr/bin/env bash RUSTC$1; shift CRATE_NAME PREV for arg in $; do [ $PREV --crate-name ] CRATE_NAME$arg break PREV$arg done if [ $CRATE_NAME your_target_crate ]; then exec $RUSTC $ \ -Cpassessancov-module \ -Cllvm-args-sanitizer-coverage-level3 \ -Cllvm-args-sanitizer-coverage-trace-pc-guard \ -Cllvm-args-sanitizer-coverage-trace-compares else exec $RUSTC $ fi脚本要点从cargo透传的编译参数中解析--crate-name只对目标 crate 追加插桩参数其余 crate 原样编译控制插桩体积与开销-Cpassessancov-module启用 LLVM 的 SanitizerCoverage 模块路径-sanitizer-coverage-level3是最高覆盖粒度-sanitizer-coverage-trace-pc-guard产生第 3.1 节的 pc-guard 回调对应sancov_edges-sanitizer-coverage-trace-compares产生第 3.2 节的比较回调对应sancov_trace_cmp依赖的__sanitizer_cov_trace_cmp*系列。然后构建RUSTC_WRAPPER./sancov-wrapper.sh cargo build --profile fuzz --bin forge注意事项插桩必须发生在forge 二进制自身的构建期而不是被测 Solidity 项目因为回调函数被链接进 forge运行时若回调无人实现插桩代码无法工作只有经过插桩的 crate 会产生回调命中未插桩构建上运行--showmap-domain sancov会得到空结果并伴随告警见 docs/dev/showmap.md 的 Caveats运行期配置与构建期插桩相互独立sancov_edges打开但二进制未插桩时功能不会报错只是观察不到 sancov 命中runner 会在语料回放时提示第 3.3 节。5. 语料回放为 AFL 风格文件--showmap-outREADME 最后一节指出要把持久化语料的覆盖导出为 AFLafl-showmap风格文件用于跨模糊器比较使用forge test --showmap-out DIR完整说明见 docs/dev/showmap.md。该文档给出的工作流# 1. 先跑一次带 corpus_dir 的 campaign让语料目录有内容 forge test # 2. 回放并导出覆盖 forge test \ --showmap-out coverage_data \ --showmap-approach foundry \ --showmap-domain evm关键 flag 摘要详见 docs/dev/showmap.md 的 Flags 表Flag说明--showmap-out DIR输出根目录必填开启 showmap 模式--showmap-approach NAME方法前缀与测试标识拼成目录名默认replay--showmap-domain evm\|sancov\|both要导出的位图默认evmsancov 域导出即本文档主题的覆盖文件--showmap-per-input逐语料条目出文件而非每测试一份聚合--showmap-corpus-dir PATH覆盖回放用的语料目录输出文件中每行是id:countsancov 域的行格式为sancov_0xguard_idx:04xguard 索引在链接期分配跨进程确定EVM 域为evm_bytecode_hash[:16hex]_pc:04x——两种域可在--showmap-domain both下同文件混排下划线分隔保证了id:count解析无歧义。6. 实践要点小结两种 sancov 模式分工不同sancov_edges改变引导信号并自动关闭 EVM 边覆盖sancov_trace_cmp只增强词典只想要破解比较守卫的能力而保留 EVM 引导时单独开启 trace-cmp 即可crates/config/src/fuzz.rs 的注释明确了这一设计意图。构建期插桩 运行期开关缺一不可RUSTC_WRAPPER注入编译标志第 4 节脚本→cargo build --profile fuzz --bin forge→ 配置sancov_edges/sancov_trace_cmp并设置corpus_dir跑 campaign。验证插桩是否生效无命中时 runner 的 build is likely not sancov-instrumented 提示crates/forge/src/runner.rs是最直接的诊断手段。热路径开销可控guard ID 到稠密索引的读写锁映射、is_active()的原子指针判空、trace-cmp 的线程局部缓冲与 512 条上限crates/evm/sancov/src/lib.rs都是为高频 EVM 执行场景设计的轻量实现。相关机制的延伸阅读EVM 边覆盖与词典的默认配置在 crates/config/src/fuzz.rsFuzzCorpusConfig各字段showmap 回放与差分覆盖工具链在 docs/dev/showmap.md。【免费下载链接】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),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进