Skip to content

Commit f0193d1

Browse files
feat: coverage-guided fuzzing for Tempo precompiles
Add SanitizerCoverage instrumentation support to collect edge coverage from native Rust precompile execution during Foundry invariant/fuzz tests. This makes the fuzzer coverage-guided for Tempo precompile code paths, which were previously invisible to the EVM-level EdgeCovInspector. Changes: - New crate: foundry-tempo-coverage (crates/evm/tempo-coverage/) Provides thread-local coverage map, SanitizerCoverage callbacks (__sanitizer_cov_trace_pc_guard, __sanitizer_cov_trace_pc_guard_init), and RAII guard for map lifecycle management. - New config field: tempo_precompile_coverage (bool, default false) Added to FuzzCorpusConfig, controls whether Tempo precompile coverage collection is active. When enabled, implicitly enables edge coverage. - Executor wiring (TempoCoverageGuard in tempo_cov.rs) Wraps EVM transact calls with a coverage map guard that collects sancov hits into a scratch buffer, then merges them into the RawCallResult edge_coverage for the CorpusManager to consume. - New build profile: fuzz (inherits release, no LTO/strip) Preserves sancov instrumentation through linking. - Build script: scripts/build-fuzz.sh Convenience script to build forge with sancov RUSTFLAGS. No changes to the tempo repository are required. SanitizerCoverage is a compiler pass applied via RUSTFLAGS at build time; the callback symbols are resolved by the linker from foundry-tempo-coverage. Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-019c2f47-ec1b-76d3-ac35-429e06bf2460
1 parent f87a6c4 commit f0193d1

File tree

12 files changed

+261
-4
lines changed

12 files changed

+261
-4
lines changed

Cargo.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ members = [
1818
"crates/evm/coverage/",
1919
"crates/evm/evm/",
2020
"crates/evm/fuzz/",
21+
"crates/evm/tempo-coverage/",
2122
"crates/evm/traces/",
2223
"crates/fmt/",
2324
"crates/forge/",
@@ -98,6 +99,17 @@ inherits = "release"
9899
lto = "fat"
99100
codegen-units = 1
100101

102+
# Coverage-guided fuzzing of Tempo precompiles.
103+
# Inherits release performance but disables LTO and stripping so that
104+
# SanitizerCoverage instrumentation is preserved in the final binary.
105+
# Build with: cargo build --profile fuzz
106+
# The sancov RUSTFLAGS are set automatically via .cargo/config.toml [profile.fuzz].
107+
[profile.fuzz]
108+
inherits = "release"
109+
lto = false
110+
strip = "none"
111+
debug = "line-tables-only"
112+
101113
# Speed up tests and dev build.
102114
[profile.dev.package]
103115
# Solc and artifacts.

crates/config/src/fuzz.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ pub struct FuzzCorpusConfig {
113113
pub corpus_min_size: usize,
114114
/// Whether to collect and display edge coverage metrics.
115115
pub show_edge_coverage: bool,
116+
/// Whether to collect Tempo Rust precompile coverage via SanitizerCoverage.
117+
/// When enabled, coverage from Tempo precompile execution is fed into the
118+
/// same hitcount map used by the EVM edge coverage, making the fuzzer
119+
/// coverage-guided for precompile code paths.
120+
/// Requires building with sancov RUSTFLAGS (see docs/tempo-coverage.md).
121+
pub tempo_precompile_coverage: bool,
116122
}
117123

118124
impl FuzzCorpusConfig {
@@ -124,7 +130,12 @@ impl FuzzCorpusConfig {
124130

125131
/// Whether edge coverage should be collected and displayed.
126132
pub fn collect_edge_coverage(&self) -> bool {
127-
self.corpus_dir.is_some() || self.show_edge_coverage
133+
self.corpus_dir.is_some() || self.show_edge_coverage || self.tempo_precompile_coverage
134+
}
135+
136+
/// Whether Tempo precompile coverage collection is enabled.
137+
pub fn collect_tempo_precompile_coverage(&self) -> bool {
138+
self.tempo_precompile_coverage && self.collect_edge_coverage()
128139
}
129140

130141
/// Whether coverage guided fuzzing is enabled.
@@ -141,6 +152,7 @@ impl Default for FuzzCorpusConfig {
141152
corpus_min_mutations: 5,
142153
corpus_min_size: 0,
143154
show_edge_coverage: false,
155+
tempo_precompile_coverage: false,
144156
}
145157
}
146158
}

crates/evm/evm/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ foundry-compilers.workspace = true
2020
foundry-config.workspace = true
2121
foundry-evm-core.workspace = true
2222
foundry-evm-coverage.workspace = true
23+
foundry-tempo-coverage = { path = "../tempo-coverage" }
2324
foundry-evm-fuzz.workspace = true
2425
foundry-evm-hardforks.workspace = true
2526
foundry-evm-networks.workspace = true

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

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ mod trace;
6767

6868
pub use trace::TracingExecutor;
6969

70+
mod tempo_cov;
71+
use tempo_cov::TempoCoverageGuard;
72+
7073
const DURATION_BETWEEN_METRICS_REPORT: Duration = Duration::from_secs(5);
7174

7275
sol! {
@@ -556,19 +559,35 @@ impl Executor {
556559
#[instrument(name = "call", level = "debug", skip_all)]
557560
pub fn call_with_env(&self, mut env: Env) -> eyre::Result<RawCallResult> {
558561
let mut stack = self.inspector().clone();
562+
let tempo_cov = stack.inner.tempo_precompile_coverage;
559563
let mut backend = CowBackend::new_borrowed(self.backend());
560-
let result = backend.inspect(&mut env, stack.as_inspector())?;
561-
convert_executed_result(env, stack, result, backend.has_state_snapshot_failure())
564+
let result = {
565+
let _guard = tempo_cov.then(TempoCoverageGuard::new);
566+
backend.inspect(&mut env, stack.as_inspector())?
567+
};
568+
let mut result =
569+
convert_executed_result(env, stack, result, backend.has_state_snapshot_failure())?;
570+
if tempo_cov {
571+
TempoCoverageGuard::merge_into(&mut result);
572+
}
573+
Ok(result)
562574
}
563575

564576
/// Execute the transaction configured in `env.tx`.
565577
#[instrument(name = "transact", level = "debug", skip_all)]
566578
pub fn transact_with_env(&mut self, mut env: Env) -> eyre::Result<RawCallResult> {
567579
let mut stack = self.inspector().clone();
580+
let tempo_cov = stack.inner.tempo_precompile_coverage;
568581
let backend = self.backend_mut();
569-
let result = backend.inspect(&mut env, stack.as_inspector())?;
582+
let result = {
583+
let _guard = tempo_cov.then(TempoCoverageGuard::new);
584+
backend.inspect(&mut env, stack.as_inspector())?
585+
};
570586
let mut result =
571587
convert_executed_result(env, stack, result, backend.has_state_snapshot_failure())?;
588+
if tempo_cov {
589+
TempoCoverageGuard::merge_into(&mut result);
590+
}
572591
self.commit(&mut result);
573592
Ok(result)
574593
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use foundry_tempo_coverage::COVERAGE_MAP_SIZE;
2+
3+
use super::RawCallResult;
4+
5+
/// RAII guard that activates Tempo precompile coverage collection for the duration of an EVM call.
6+
///
7+
/// Allocates a thread-local scratch buffer, sets it as the active coverage map via
8+
/// `foundry_tempo_coverage`, and on drop clears it. The collected hits can then be merged
9+
/// into the `RawCallResult`'s `edge_coverage` via [`Self::merge_into`].
10+
pub(super) struct TempoCoverageGuard;
11+
12+
thread_local! {
13+
static TEMPO_COV_BUFFER: std::cell::RefCell<Vec<u8>> =
14+
std::cell::RefCell::new(vec![0u8; COVERAGE_MAP_SIZE]);
15+
}
16+
17+
impl TempoCoverageGuard {
18+
pub(super) fn new() -> Self {
19+
TEMPO_COV_BUFFER.with(|buf| {
20+
let mut buf = buf.borrow_mut();
21+
buf.fill(0);
22+
let ptr = buf.as_mut_ptr();
23+
let len = buf.len();
24+
foundry_tempo_coverage::set_coverage_map(ptr, len);
25+
});
26+
Self
27+
}
28+
29+
/// Merge Tempo precompile coverage hits into the `RawCallResult`'s edge coverage.
30+
///
31+
/// If the result already has an `edge_coverage` map (from `EdgeCovInspector`), Tempo precompile hits
32+
/// are added into it. If not, the Tempo precompile coverage buffer becomes the edge coverage.
33+
pub(super) fn merge_into(result: &mut RawCallResult) {
34+
TEMPO_COV_BUFFER.with(|buf| {
35+
let buf = buf.borrow();
36+
let has_any_hit = buf.iter().any(|&b| b > 0);
37+
if !has_any_hit {
38+
return;
39+
}
40+
41+
match &mut result.edge_coverage {
42+
Some(existing) => {
43+
for (existing_slot, &native_hit) in existing.iter_mut().zip(buf.iter()) {
44+
*existing_slot = existing_slot.saturating_add(native_hit);
45+
}
46+
}
47+
None => {
48+
result.edge_coverage = Some(buf.clone());
49+
}
50+
}
51+
});
52+
}
53+
}
54+
55+
impl Drop for TempoCoverageGuard {
56+
fn drop(&mut self) {
57+
foundry_tempo_coverage::clear_coverage_map();
58+
}
59+
}

crates/evm/evm/src/inspectors/stack.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@ pub struct InspectorStackInner {
339339
pub tracer: Option<Box<TracingInspector>>,
340340

341341
// InspectorExt and other internal data.
342+
pub tempo_precompile_coverage: bool,
342343
pub enable_isolation: bool,
343344
pub networks: NetworkConfigs,
344345
pub create2_deployer: Address,
@@ -462,6 +463,12 @@ impl InspectorStack {
462463
self.edge_coverage = yes.then(EdgeCovInspector::new).map(Into::into);
463464
}
464465

466+
/// Set whether to enable Tempo precompile coverage collection.
467+
#[inline]
468+
pub fn collect_tempo_precompile_coverage(&mut self, yes: bool) {
469+
self.tempo_precompile_coverage = yes;
470+
}
471+
465472
/// Set whether to enable call isolation.
466473
#[inline]
467474
pub fn enable_isolation(&mut self, yes: bool) {
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "foundry-tempo-coverage"
3+
description = "Tempo precompile coverage feedback for coverage-guided fuzzing"
4+
5+
version.workspace = true
6+
edition.workspace = true
7+
rust-version.workspace = true
8+
authors.workspace = true
9+
license.workspace = true
10+
homepage.workspace = true
11+
repository.workspace = true
12+
13+
[lints]
14+
workspace = true
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
//! Tempo precompile coverage feedback for coverage-guided fuzzing.
2+
//!
3+
//! Provides SanitizerCoverage callbacks and a thread-local coverage map pointer
4+
//! that can be set by the fuzzing executor to collect edge coverage from Tempo precompile code.
5+
6+
use std::cell::Cell;
7+
use std::sync::atomic::{AtomicU32, Ordering};
8+
9+
pub const COVERAGE_MAP_SIZE: usize = 65536;
10+
11+
thread_local! {
12+
static COVERAGE_MAP_PTR: Cell<*mut u8> = const { Cell::new(std::ptr::null_mut()) };
13+
static COVERAGE_MAP_LEN: Cell<usize> = const { Cell::new(0) };
14+
}
15+
16+
pub fn set_coverage_map(ptr: *mut u8, len: usize) {
17+
COVERAGE_MAP_PTR.with(|c| c.set(ptr));
18+
COVERAGE_MAP_LEN.with(|c| c.set(len));
19+
}
20+
21+
pub fn clear_coverage_map() {
22+
COVERAGE_MAP_PTR.with(|c| c.set(std::ptr::null_mut()));
23+
COVERAGE_MAP_LEN.with(|c| c.set(0));
24+
}
25+
26+
pub fn is_active() -> bool {
27+
COVERAGE_MAP_PTR.with(|c| !c.get().is_null())
28+
}
29+
30+
pub struct CoverageMapGuard;
31+
32+
impl CoverageMapGuard {
33+
pub fn new(ptr: *mut u8, len: usize) -> Self {
34+
set_coverage_map(ptr, len);
35+
Self
36+
}
37+
}
38+
39+
impl Drop for CoverageMapGuard {
40+
fn drop(&mut self) {
41+
clear_coverage_map();
42+
}
43+
}
44+
45+
#[inline(always)]
46+
pub fn record_hit(guard_id: u32) {
47+
COVERAGE_MAP_PTR.with(|ptr_cell| {
48+
let ptr = ptr_cell.get();
49+
if ptr.is_null() {
50+
return;
51+
}
52+
COVERAGE_MAP_LEN.with(|len_cell| {
53+
let len = len_cell.get();
54+
let idx = guard_id as usize % len;
55+
unsafe {
56+
let slot = ptr.add(idx);
57+
*slot = (*slot).wrapping_add(1);
58+
}
59+
});
60+
});
61+
}
62+
63+
static GUARD_COUNTER: AtomicU32 = AtomicU32::new(1);
64+
65+
/// # Safety
66+
///
67+
/// Called by the LLVM SanitizerCoverage runtime at startup. `[start, stop)` must be a valid
68+
/// range of mutable `u32` guard slots allocated by the compiler for the current DSO.
69+
#[unsafe(no_mangle)]
70+
pub unsafe extern "C" fn __sanitizer_cov_trace_pc_guard_init(mut start: *mut u32, stop: *mut u32) {
71+
while start < stop {
72+
let id = GUARD_COUNTER.fetch_add(1, Ordering::Relaxed);
73+
unsafe {
74+
*start = id;
75+
start = start.add(1);
76+
}
77+
}
78+
}
79+
80+
/// # Safety
81+
///
82+
/// Called by the LLVM SanitizerCoverage runtime at every instrumented CFG edge.
83+
/// `guard` must point to a valid `u32` guard slot initialized by `__sanitizer_cov_trace_pc_guard_init`.
84+
#[unsafe(no_mangle)]
85+
pub unsafe extern "C" fn __sanitizer_cov_trace_pc_guard(guard: *mut u32) {
86+
let id = unsafe { *guard };
87+
if id == 0 {
88+
return;
89+
}
90+
record_hit(id);
91+
}

crates/forge/src/runner.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,11 @@ impl<'a> FunctionRunner<'a> {
746746
executor
747747
.inspector_mut()
748748
.collect_edge_coverage(invariant_config.corpus.collect_edge_coverage());
749+
executor
750+
.inspector_mut()
751+
.collect_tempo_precompile_coverage(
752+
invariant_config.corpus.collect_tempo_precompile_coverage(),
753+
);
749754
let mut config = invariant_config.clone();
750755
let (failure_dir, failure_file) = test_paths(
751756
&mut config.corpus,
@@ -1041,6 +1046,11 @@ impl<'a> FunctionRunner<'a> {
10411046
// Enable edge coverage if running with coverage guided fuzzing or with edge coverage
10421047
// metrics (useful for benchmarking the fuzzer).
10431048
executor.inspector_mut().collect_edge_coverage(fuzz_config.corpus.collect_edge_coverage());
1049+
executor
1050+
.inspector_mut()
1051+
.collect_tempo_precompile_coverage(
1052+
fuzz_config.corpus.collect_tempo_precompile_coverage(),
1053+
);
10441054
// Load persisted counterexample, if any.
10451055
let persisted_failure =
10461056
foundry_common::fs::read_json_file::<BaseCounterExample>(failure_file.as_path()).ok();

0 commit comments

Comments
 (0)