This is a minimalist LibAFL fuzzer based on the official tutorial, designed to demonstrate LibAFL's core concepts. It automatically discovers inputs that cause panics.
use std::{path::PathBuf, ptr::write};
use libafl::monitors::SimpleMonitor;
use libafl::{
corpus::{InMemoryCorpus, OnDiskCorpus}, // Corpus
events::SimpleEventManager, // Event manager
executors::{ExitKind, InProcessExecutor}, // Executor
feedbacks::{CrashFeedback, MaxMapFeedback}, // Feedback mechanisms
fuzzer::{Fuzzer, StdFuzzer}, // Fuzzer
generators::RandPrintablesGenerator, // Initial input generator
inputs::{BytesInput, HasTargetBytes}, // Input types
mutators::{havoc_mutations::havoc_mutations,
scheduled::HavocScheduledMutator}, // Mutator
observers::ConstMapObserver, // Observer (coverage monitoring)
schedulers::QueueScheduler, // Scheduler
stages::mutational::StdMutationalStage, // Mutation stage
state::StdState, // State management
};
use libafl_bolts::{
current_nanos, nonnull_raw_mut, nonzero, rands::StdRand,
tuples::tuple_list, AsSlice,
};const SIGNALS_LEN: usize = 16;
static mut SIGNALS: [u8; SIGNALS_LEN] = [0; SIGNALS_LEN];
static mut SIGNALS_PTR: *mut u8 = &raw mut SIGNALS as _;
fn signals_set(idx: usize) {
unsafe { write(SIGNALS_PTR.add(idx), 1) };
}Purpose:
SIGNALSis a coverage map array used to track code execution paths- When different branches execute, set the corresponding index position to 1
signals_set()function safely updates the map
let mut harness = |input: &BytesInput| {
let target = input.target_bytes();
let buf = target.as_slice();
signals_set(0); // Enter function, set signal
if !buf.is_empty() && buf[0] == b'a' {
signals_set(1); // Path 1: first byte is 'a'
if buf.len() > 1 && buf[1] == b'b' {
signals_set(2); // Path 2: second byte is 'b'
if buf.len() > 2 && buf[2] == b'c' {
#[cfg(unix)]
panic!("Artificial bug triggered =)"); // Path 3: vulnerability found!
}
}
}
ExitKind::Ok // Normal exit
};Purpose:
- Harness is the target function for fuzzing
- Accepts
BytesInputtype input - Sets signals for coverage feedback
- Panics when input is
"abc" - Returns
ExitKind::Okindicating normal execution
let observer = unsafe {
ConstMapObserver::from_mut_ptr("signals", nonnull_raw_mut!(SIGNALS))
};Purpose:
- Create an observer to monitor the
SIGNALSarray - Track coverage changes during program execution
- Name "signals" is used for logging
Type: ConstMapObserver - Observer based on constant-size map
// Feedback 1: Coverage feedback (used to select interesting inputs)
let mut feedback = MaxMapFeedback::new(&observer);
// Feedback 2: Crash feedback (used to identify bugs)
let mut objective = CrashFeedback::new();Purpose:
-
MaxMapFeedback: Detects if new code paths are discovered- If new path found → Save input to corpus
- If no new path → Discard input
-
CrashFeedback: Detects program crashes- If panic/crash occurs → Save to objective corpus
- Used to find inputs triggering bugs
let mut state = StdState::new(
StdRand::with_seed(current_nanos()), // Random number generator
InMemoryCorpus::new(), // Main corpus (memory)
OnDiskCorpus::new(PathBuf::from("./crashes"))?, // Objective corpus (disk)
&mut feedback, // Coverage feedback state
&mut objective, // Crash feedback state
).unwrap();Purpose:
Stateis LibAFL's global state container- Stores all data that needs to be persisted
- Parameter explanations:
- RNG: Random number generator for mutations
- Corpus: Save discovered interesting inputs
- OnDiskCorpus: Where to save crash inputs
- Feedback state: Running state of feedback mechanism
- Objective state: Running state of objective feedback
let mon = SimpleMonitor::new(|s| println!("{s}"));
let mut mgr = SimpleEventManager::new(mon);Purpose:
-
SimpleMonitor: Display fuzzing progress- Output execution statistics, corpus size, coverage, etc.
-
SimpleEventManager: Handle fuzzer events- New corpus discovery events
- New coverage discovery events
- Target achieved events (like crashes)
let scheduler = QueueScheduler::new();
let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective);Purpose:
-
QueueScheduler: FIFO queue scheduling- Select inputs from corpus sequentially
-
StdFuzzer: Standard fuzzer- Combines scheduler, feedback, and objective
- Coordinates the entire fuzzing process
let mut executor = InProcessExecutor::new(
&mut harness, // Target function to execute
tuple_list!(observer), // Observer list
&mut fuzzer, // Fuzzer
&mut state, // State
&mut mgr, // Event manager
).expect("Failed to create the Executor");Purpose:
InProcessExecutor: Execute target function in the same process- Advantages: Fast, simple
- Disadvantages: Target function crash crashes the fuzzer
- Execution flow:
- Get input
- Call harness function
- Collect observer data
- Return execution result
let mut generator = RandPrintablesGenerator::new(nonzero!(32));
state.generate_initial_inputs(
&mut fuzzer,
&mut executor,
&mut generator,
&mut mgr,
8 // Generate 8 initial inputs
).expect("Failed to generate the initial corpus");Purpose:
-
RandPrintablesGenerator: Generate random printable character byte arrays- Maximum length: 32 bytes
-
Generate 8 initial inputs and add to corpus
-
These inputs are immediately executed to discover initial coverage
let mutator = HavocScheduledMutator::new(havoc_mutations());
let mut stages = tuple_list!(StdMutationalStage::new(mutator));Purpose:
-
HavocScheduledMutator: Havoc mutation strategy (AFL's classic method)- Includes: bit flip, byte flip, byte delete, byte insert, etc.
-
StdMutationalStage: Mutation stage- Mutates inputs and executes them
fuzzer.fuzz_loop(
&mut stages, // Mutation stage
&mut executor, // Executor
&mut state, // State
&mut mgr // Event manager
).expect("Error in the fuzzing loop");Purpose:
- Start the infinite fuzzing loop
- Each iteration:
- Scheduler selects an input from corpus
- Stage mutates the input
- Executor executes the mutated input
- Observer collects coverage information
- Feedback determines if interesting
- If interesting, add to corpus
graph TD
A["Start (main)"] --> B["Define Harness function"]
B --> C["Create Observer (coverage monitoring)"]
C --> D["Create Feedback (feedback mechanism)"]
D --> E["Create State (state management)"]
E --> F["Create Monitor & EventManager"]
F --> G["Create Scheduler & Fuzzer"]
G --> H["Create Executor"]
H --> I["Generate initial inputs"]
I --> J["Create Mutator & Stage"]
J --> K["Start fuzz_loop"]
K --> L["Select input from corpus"]
L --> M["Mutate input"]
M --> N["Execute Harness"]
N --> O["Collect coverage information"]
O --> P{"Found new path?"}
P -->|Yes| Q["Add to corpus"]
P -->|No| R["Discard input"]
Q --> S{"Triggered Crash?"}
R --> S
S -->|Yes| T["Save to crashes/"]
S -->|No| L
T --> U["Continue fuzzing"]
U --> L
| Component | Purpose | Example Type |
|---|---|---|
| Harness | Target function to test | Closure function |
| Observer | Monitor code coverage | ConstMapObserver |
| Feedback | Determine if input is interesting | MaxMapFeedback |
| Objective | Identify target events (like crashes) | CrashFeedback |
| State | Global state container | StdState |
| Corpus | Test case library | InMemoryCorpus, OnDiskCorpus |
| Scheduler | Select input to test | QueueScheduler |
| Executor | Execute target function | InProcessExecutor |
| Mutator | Mutate input | HavocScheduledMutator |
| Stage | Mutation and execution stage | StdMutationalStage |
| Monitor | Display progress | SimpleMonitor |
| EventManager | Handle events | SimpleEventManager |