Skip to content

Commit f3425f8

Browse files
committed
add fuzz_with_reset macro
1 parent bb3e216 commit f3425f8

4 files changed

Lines changed: 239 additions & 32 deletions

File tree

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,38 @@ environment variable `AFL_NO_CFG_FUZZING` to `1` when building.
5252
[AFLplusplus]: https://aflplus.plus/
5353
[rust]: https://www.rust-lang.org
5454

55+
## Resettable State (`fuzz_with_reset!`)
56+
57+
AFL++ persistent mode runs the fuzz target in a loop. Static initialization (e.g., `OnceLock`, `lazy_static`, `once_cell::Lazy`) only executes on the first iteration — subsequent iterations skip those code paths, causing AFL's stability metric to drop.
58+
59+
Use `fuzz_with_reset!` to provide a reset closure that clears static state after each iteration.
60+
61+
Note: the example uses `Mutex<Option<T>>` instead of `OnceLock`/`OnceCell` because those types do not support resetting out-of-the-box.
62+
63+
```rust
64+
use std::sync::Mutex;
65+
66+
static CACHE: Mutex<Option<Vec<u8>>> = Mutex::new(None);
67+
68+
fn main() {
69+
afl::fuzz_with_reset!(|data: &[u8]| {
70+
let mut cache = CACHE.lock().unwrap();
71+
if cache.is_none() {
72+
*cache = Some(data.to_vec());
73+
}
74+
drop(cache);
75+
// ... fuzz logic ...
76+
}, || {
77+
// Reset closure: called after each successful iteration
78+
*CACHE.lock().unwrap() = None;
79+
});
80+
}
81+
```
82+
83+
A `fuzz_with_reset_nohook!` variant is also available (like `fuzz_nohook!`, it does not override the panic hook).
84+
85+
See [`afl/examples/reset_demo.rs`](afl/examples/reset_demo.rs) for a complete example.
86+
5587
## IJON
5688

5789
If you want to use [IJON](https://github.com/AFLplusplus/AFLplusplus/blob/stable/docs/IJON.md) - helping fuzzer coverage through code annotation - then

afl/examples/reset_demo.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Demonstrates how `fuzz_with_reset!` improves AFL++ persistent mode stability
2+
// when using static state.
3+
//
4+
// Setup:
5+
// `cargo run -p cargo-afl -- afl build --example reset_demo --manifest-path afl/Cargo.toml`
6+
// `mkdir -p /tmp/afl-input && echo "test" > /tmp/afl-input/seed`
7+
//
8+
// Without reset (low stability):
9+
// `AFL_NO_UI=1 cargo run -p cargo-afl -- afl fuzz \
10+
// -i /tmp/afl-input -o /tmp/afl-out-bad -V 15 target/debug/examples/reset_demo`
11+
//
12+
// With reset (high stability):
13+
// `USE_RESET=1 AFL_NO_UI=1 cargo run -p cargo-afl -- afl fuzz \
14+
// -i /tmp/afl-input -o /tmp/afl-out-reset -V 15 target/debug/examples/reset_demo`
15+
//
16+
// Compare stability:
17+
// `grep stability /tmp/afl-out-bad/default/fuzzer_stats /tmp/afl-out-reset/default/fuzzer_stats`
18+
19+
use std::sync::Mutex;
20+
21+
static CACHE: Mutex<Option<Vec<u8>>> = Mutex::new(None);
22+
23+
fn main() {
24+
if std::env::var("USE_RESET").is_ok() {
25+
afl::fuzz_with_reset!(|data: &[u8]| { fuzz_body(data) }, || {
26+
*CACHE.lock().unwrap() = None;
27+
});
28+
} else {
29+
afl::fuzz!(|data: &[u8]| {
30+
fuzz_body(data);
31+
});
32+
}
33+
}
34+
35+
fn fuzz_body(data: &[u8]) {
36+
let mut cache = CACHE.lock().unwrap();
37+
if cache.is_none() {
38+
*cache = Some(data.to_vec());
39+
}
40+
drop(cache);
41+
assert!(!(data.len() > 2 && data[0] == b'x'), "crash");
42+
}

afl/src/lib.rs

Lines changed: 100 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -242,9 +242,39 @@ pub static mut __afl_sharedmem_fuzzing: i32 = 1;
242242
/// });
243243
/// # }
244244
/// ```
245-
pub fn fuzz<F>(hook: bool, mut closure: F)
245+
pub fn fuzz<F>(hook: bool, closure: F)
246246
where
247247
F: FnMut(&[u8]) + std::panic::RefUnwindSafe,
248+
{
249+
fuzz_with_reset(hook, closure, || {});
250+
}
251+
252+
/// Like [`fuzz()`], but calls a `reset` closure after each successful iteration.
253+
///
254+
/// This is useful when the fuzz target uses static state (e.g., `OnceLock`, `lazy_static`)
255+
/// that must be cleared between iterations in AFL++ persistent mode. Without resetting,
256+
/// code paths that run only on the first iteration cause AFL's stability metric to drop.
257+
///
258+
/// ```rust,no_run
259+
/// # extern crate afl;
260+
/// # use afl::fuzz_with_reset;
261+
/// # use std::sync::Mutex;
262+
/// # static CACHE: Mutex<Option<Vec<u8>>> = Mutex::new(None);
263+
/// # fn main() {
264+
/// fuzz_with_reset(true, |data| {
265+
/// let mut cache = CACHE.lock().unwrap();
266+
/// if cache.is_none() {
267+
/// *cache = Some(data.to_vec());
268+
/// }
269+
/// }, || {
270+
/// *CACHE.lock().unwrap() = None;
271+
/// });
272+
/// # }
273+
/// ```
274+
pub fn fuzz_with_reset<F, R>(hook: bool, mut closure: F, mut reset: R)
275+
where
276+
F: FnMut(&[u8]) + std::panic::RefUnwindSafe,
277+
R: FnMut(),
248278
{
249279
// this marker strings needs to be in the produced executable for
250280
// afl-fuzz to detect `persistent mode` and `defered mode`
@@ -281,8 +311,9 @@ where
281311
unsafe { __afl_manual_init() };
282312

283313
if unsafe { __afl_fuzz_ptr.is_null() } {
284-
// in-memory testcase delivery is not enabled
285-
// get buffer from AFL++ through stdin
314+
// In-memory testcase delivery is not enabled; the target is not running
315+
// in a persistent loop, so `reset` is not needed here.
316+
// Get buffer from AFL++ through stdin.
286317
let result = io::stdin().read_to_end(&mut input);
287318
if result.is_err() {
288319
return;
@@ -322,7 +353,9 @@ where
322353
// process before the stack frames are unwinded.
323354
std::process::abort();
324355
}
356+
325357
input.clear();
358+
reset();
326359
}
327360
}
328361
}
@@ -363,25 +396,72 @@ macro_rules! fuzz_nohook {
363396

364397
#[doc(hidden)]
365398
#[macro_export]
366-
macro_rules! __fuzz {
367-
($hook:expr, |$buf:ident| $body:expr) => {
368-
$crate::fuzz($hook, |$buf| $body);
399+
macro_rules! __reset_or_noop {
400+
() => {
401+
|| {}
369402
};
370-
($hook:expr, |$buf:ident: &[u8]| $body:expr) => {
371-
$crate::fuzz($hook, |$buf| $body);
403+
($reset:expr) => {
404+
$reset
372405
};
373-
($hook:expr, |$buf:ident: $dty: ty| $body:expr) => {
374-
$crate::fuzz($hook, |$buf| {
375-
let $buf: $dty = {
376-
let mut data = ::arbitrary::Unstructured::new($buf);
377-
if let Ok(d) = ::arbitrary::Arbitrary::arbitrary(&mut data).map_err(|_| "") {
378-
d
379-
} else {
380-
return;
381-
}
382-
};
406+
}
383407

384-
$body
385-
});
408+
#[doc(hidden)]
409+
#[macro_export]
410+
macro_rules! __fuzz {
411+
($hook:expr, |$buf:ident| $body:expr $(, $reset:expr)?) => {
412+
$crate::fuzz_with_reset($hook, |$buf| $body, $crate::__reset_or_noop!($($reset)?));
413+
};
414+
($hook:expr, |$buf:ident: &[u8]| $body:expr $(, $reset:expr)?) => {
415+
$crate::fuzz_with_reset($hook, |$buf| $body, $crate::__reset_or_noop!($($reset)?));
416+
};
417+
($hook:expr, |$buf:ident: $dty: ty| $body:expr $(, $reset:expr)?) => {
418+
$crate::fuzz_with_reset(
419+
$hook,
420+
|$buf| {
421+
let $buf: $dty = {
422+
let mut data = ::arbitrary::Unstructured::new($buf);
423+
if let Ok(d) = ::arbitrary::Arbitrary::arbitrary(&mut data).map_err(|_| "") {
424+
d
425+
} else {
426+
return;
427+
}
428+
};
429+
430+
$body
431+
},
432+
$crate::__reset_or_noop!($($reset)?),
433+
);
386434
};
387435
}
436+
437+
/// Like [`fuzz!`], but accepts a second closure that resets state after each iteration.
438+
///
439+
/// This is useful when the fuzz target uses static state (e.g., `OnceLock`, `lazy_static`)
440+
/// that must be cleared between iterations in AFL++ persistent mode.
441+
///
442+
/// ```rust,no_run
443+
/// # #[macro_use] extern crate afl;
444+
/// # use std::sync::Mutex;
445+
/// # static CACHE: Mutex<Option<Vec<u8>>> = Mutex::new(None);
446+
/// # fn main() {
447+
/// fuzz_with_reset!(|data: &[u8]| {
448+
/// let mut cache = CACHE.lock().unwrap();
449+
/// if cache.is_none() {
450+
/// *cache = Some(data.to_vec());
451+
/// }
452+
/// }, || {
453+
/// *CACHE.lock().unwrap() = None;
454+
/// });
455+
/// # }
456+
/// ```
457+
#[macro_export]
458+
macro_rules! fuzz_with_reset {
459+
( $($x:tt)* ) => { $crate::__fuzz!(true, $($x)*) }
460+
}
461+
462+
/// Like [`fuzz_with_reset!`], but panics that are caught inside the fuzzed code are not turned
463+
/// into crashes.
464+
#[macro_export]
465+
macro_rules! fuzz_with_reset_nohook {
466+
( $($x:tt)* ) => { $crate::__fuzz!(false, $($x)*) }
467+
}

cargo-afl/tests/integration.rs

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -100,32 +100,85 @@ fn integration_maze() {
100100
unreachable!();
101101
}
102102

103+
#[test]
104+
fn integration_fuzz_with_reset() {
105+
// Run without reset (expect low stability)
106+
let dir_no_reset = fuzz_example_with_envs("reset_demo", 15, &[]);
107+
108+
// Run with reset (expect high stability)
109+
let dir_with_reset = fuzz_example_with_envs("reset_demo", 15, &[("USE_RESET", "1")]);
110+
111+
let stability_no_reset = parse_stability(dir_no_reset.path());
112+
let stability_with_reset = parse_stability(dir_with_reset.path());
113+
114+
// On Linux/x86_64 we observe ~95% stability, on macOS/aarch64 ~85%
115+
// due to ARM's relaxed memory model affecting bitmap synchronization.
116+
let min_stability_expected = 80.0;
117+
118+
assert!(
119+
stability_no_reset < min_stability_expected,
120+
"Stability without reset ({stability_no_reset}%) should be below {min_stability_expected}%"
121+
);
122+
assert!(
123+
stability_with_reset > min_stability_expected,
124+
"Stability with reset ({stability_with_reset}%) should be above {min_stability_expected}%"
125+
);
126+
}
127+
103128
fn fuzz_example(name: &str, should_crash: bool) {
104-
let temp_dir = tempfile::TempDir::new().expect("Could not create temporary directory");
129+
let temp_dir = fuzz_example_with_envs(name, 5, &[("AFL_BENCH_UNTIL_CRASH", "1")]);
105130
let temp_dir_path = temp_dir.path();
131+
assert!(temp_dir_path.join("default").join("fuzzer_stats").is_file());
132+
let crashes = std::fs::read_dir(temp_dir_path.join("default").join("crashes"))
133+
.unwrap()
134+
.count();
135+
if should_crash {
136+
assert!(crashes >= 1);
137+
} else {
138+
assert_eq!(0, crashes);
139+
}
140+
}
141+
142+
fn fuzz_example_with_envs(
143+
name: &str,
144+
timeout_secs: u32,
145+
envs: &[(&str, &str)],
146+
) -> tempfile::TempDir {
147+
let temp_dir = tempfile::TempDir::new().expect("Could not create temporary directory");
106148
let _: ExitStatus = process::Command::new(cargo_afl_path())
107149
.arg("afl")
108150
.arg("fuzz")
109151
.arg("-i")
110152
.arg(input_path())
111153
.arg("-o")
112-
.arg(temp_dir_path)
113-
.args(["-V", "5"]) // 5 seconds
154+
.arg(temp_dir.path())
155+
.args(["-V", &timeout_secs.to_string()])
114156
.arg(examples_path(name))
115-
.env("AFL_BENCH_UNTIL_CRASH", "1")
116157
.env("AFL_NO_CRASH_README", "1")
117158
.env("AFL_NO_UI", "1")
159+
.envs(envs.iter().copied())
118160
.stdout(process::Stdio::inherit())
119161
.stderr(process::Stdio::inherit())
120162
.status()
121163
.expect("Could not run cargo afl fuzz");
122-
assert!(temp_dir_path.join("default").join("fuzzer_stats").is_file());
123-
let crashes = std::fs::read_dir(temp_dir_path.join("default").join("crashes"))
124-
.unwrap()
125-
.count();
126-
if should_crash {
127-
assert!(crashes >= 1);
128-
} else {
129-
assert_eq!(0, crashes);
164+
temp_dir
165+
}
166+
167+
fn parse_stability(output_dir: &path::Path) -> f64 {
168+
let stats_path = output_dir.join("default").join("fuzzer_stats");
169+
let contents = std::fs::read_to_string(&stats_path)
170+
.unwrap_or_else(|e| panic!("Failed to read {}: {e}", stats_path.display()));
171+
for line in contents.lines() {
172+
if let Some(value) = line.strip_prefix("stability") {
173+
let value = value
174+
.trim()
175+
.trim_start_matches(':')
176+
.trim()
177+
.trim_end_matches('%');
178+
return value
179+
.parse()
180+
.unwrap_or_else(|e| panic!("Failed to parse stability value '{value}': {e}"));
181+
}
130182
}
183+
panic!("No stability line found in {}", stats_path.display());
131184
}

0 commit comments

Comments
 (0)