Introduce libafl-fuzz (#2362)

* Introduce libafl-fuzz

* fix corpus file path

* simplify SeedFeedback

* fix incorrect comment

* add clap::ValueEnum to PowerSchedule as an optional derive if clap is enabled

* UnixShMemProvider replaced with StdShMemProvider for libafl-fuzz

* remove io_error_more feature constraint

* libafl-fuzz: make Ok(()) unreachable in CentralizedLauncher

* libafl-fuzz: make harness_input_stdin to harness_input_type with &'static

* libafl-fuzz: move each feedback to it's own file

* make run_fuzzer_with_stage into a function.
use CachedOnDiskCorpus instead of OnDiskCorpus for Corpora
remove utils file

* remove unecessary clone

* libafl-fuzz: cleanup AFLStatsStage

* move peak_rss_mb to libafl_bolts

* parse envs by hand

* add sensible defaults for map size and broker port

* fix test.sh and corpus_id padding

* add Makefile.toml

* libafl-fuzz update test suite

* libafl-fuzz: clippy

* rename peak_rss_mb to peak_rss_mb_children

* remove minor version constraint for clap

* libafl-fuzz: fix ELF check and instrumentation check in check_binary

* libafl-fuzz: improve Makefile.toml

* simplify fuzzer and cleanup typos

* libafl-fuzz: load corpus entries in a multicore fashion

* libafl-fuzz: create output dir if not exists (non-racey)

* libafl-fuzz: add sequential scheduling support
libafl-fuzz: add cmplog options
libafl-fuzz: add test-cmplog.c to CI

* rename peak_rss_mb_children to peak_rss_mb_child_processes

* fix race condition in SyncFromDiskStage, add interval based checking and support for multiple directories.
libafl-fuzz: add support for syncing with foreign fuzzers

* update README

* implement AflScheduler for QueueScheduler.
Add queue_cycles field to
AflScheduler

* libafl-fuzz: remove dependecy on SchedulerMetadata for AflStatsStage

* clippy

* remove queue_cycles from AflScheduler into int's own trait.
libafl-fuzz: simplify map observer tracking

* clippy

* libafl-fuzz: disable cmplog check in CI for now

* add missing constraints for libafl_qemu executor

* clippy

* libafl-fuzz: improve Makefile
libafl-fuzz: clippy

* libafl-fuzz: misc

* misc typos, beautify

---------

Co-authored-by: Dongjia "toka" Zhang <tokazerkje@outlook.com>
Co-authored-by: Dominik Maier <domenukk@gmail.com>
This commit is contained in:
Aarnav 2024-07-16 02:10:20 +02:00 committed by GitHub
parent e8de3591bf
commit aa21815a97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 2844 additions and 72 deletions

View File

@ -336,6 +336,7 @@ jobs:
- ./fuzzers/backtrace_baby_fuzzers/forkserver_executor - ./fuzzers/backtrace_baby_fuzzers/forkserver_executor
- ./fuzzers/backtrace_baby_fuzzers/c_code_with_inprocess_executor - ./fuzzers/backtrace_baby_fuzzers/c_code_with_inprocess_executor
- ./fuzzers/backtrace_baby_fuzzers/rust_code_with_fork_executor - ./fuzzers/backtrace_baby_fuzzers/rust_code_with_fork_executor
- ./fuzzers/libafl-fuzz
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3

View File

@ -0,0 +1,19 @@
[package]
name = "libafl-fuzz"
version = "0.0.1"
edition = "2021"
[dependencies]
clap = { version = "4.5", features = ["derive", "env"] }
env_logger = "0.11.3"
libafl = { path = "../../libafl", features = ["std", "derive", "track_hit_feedbacks", "clap", "errors_backtrace"]}
libafl_bolts = { path = "../../libafl_bolts", features = ["std", "errors_backtrace"]}
libafl_targets = { path = "../../libafl_targets"}
memmap2 = "0.9.4"
nix = {version = "0.29", features = ["fs"]}
regex = "1.10.5"
serde = { version = "1.0.117", features = ["derive"] }
[features]
default = ["track_hit_feedbacks"]
track_hit_feedbacks = ["libafl/track_hit_feedbacks"]

View File

@ -0,0 +1,66 @@
[env]
PROJECT_DIR = { script = ["pwd"] }
CARGO_TARGET_DIR = { value = "${PROJECT_DIR}/target", condition = { env_not_set = ["CARGO_TARGET_DIR"] } }
PROFILE = { value = "release", condition = {env_not_set = ["PROFILE"]} }
PROFILE_DIR = {value = "release", condition = {env_not_set = ["PROFILE_DIR"] }}
FUZZER_NAME = 'libafl-fuzz'
FUZZER = '${CARGO_TARGET_DIR}/${PROFILE_DIR}/${FUZZER_NAME}'
LLVM_CONFIG = {value = "llvm-config-18", condition = {env_not_set = ["LLVM_CONFIG"] }}
AFL_VERSION = "4.21c"
AFL_DIR_NAME= {value = "./AFLplusplus-${AFL_VERSION}"}
AFL_CC_PATH= {value = "${AFL_DIR_NAME}/afl-clang-fast"}
[tasks.build_afl]
script_runner="@shell"
script='''
if [ ! -d "$AFL_DIR_NAME" ]; then
if [ -f "v${AFL_VERSION}.zip" ]; then
rm v${AFL_VERSION}.zip
fi
wget https://github.com/AFLplusplus/AFLplusplus/archive/refs/tags/v${AFL_VERSION}.zip
unzip v${AFL_VERSION}.zip
cd ${AFL_DIR_NAME}
LLVM_CONFIG=${LLVM_CONFIG} make
cd ..
fi
'''
# Test
[tasks.test]
linux_alias = "test_unix"
mac_alias = "test_unix"
windows_alias = "unsupported"
[tasks.test_unix]
script_runner="@shell"
script='''
cargo build --profile ${PROFILE}
AFL_PATH=${AFL_DIR_NAME} ${AFL_CC_PATH} ./test/test-instr.c -o ./test/out-instr
AFL_CORES=1 AFL_STATS_INTERVAL=1 timeout 5 ${FUZZER} -i ./test/seeds -o ./test/output ./test/out-instr || true
test -n "$( ls ./test/output/fuzzer_main/queue/id:000002* 2>/dev/null )" || exit 1
test -n "$( ls ./test/output/fuzzer_main/fuzzer_stats 2>/dev/null )" || exit 1
test -n "$( ls ./test/output/fuzzer_main/plot_data 2>/dev/null )" || exit 1
test -d "./test/output/fuzzer_main/hangs" || exit 1
test -d "./test/output/fuzzer_main/crashes" || exit 1
# cmplog TODO: AFL_BENCH_UNTIL_CRASH=1 instead of timeout 15s
#AFL_LLVM_CMPLOG=1 AFL_PATH=${AFL_DIR_NAME} ${AFL_CC_PATH} ./test/test-cmplog.c -o ./test/out-cmplog
#AFL_CORES=1 timeout 15 ${FUZZER} -Z -l 3 -m 0 -V30 -i ./test/seeds_cmplog -o ./test/cmplog-output -c ./test/out-cmplog ./test/out-cmplog >>errors 2>&1
#test -n "$( ls ./test/cmplog-output/fuzzer_main/crashes/id:000000* ./test/cmplog-output/hangs/id:000000* 2>/dev/null )" || exit 1
'''
dependencies = ["build_afl"]
[tasks.clean]
linux_alias = "clean_unix"
mac_alias = "clean_unix"
windows_alias = "unsupported"
[tasks.clean_unix]
script_runner="@shell"
script='''
rm -rf AFLplusplus-${AFL_VERSION}
rm v${AFL_VERSION}.zip
rm -rf ./test/out-instr
rm -rf ./test/output
'''

View File

@ -0,0 +1,70 @@
Rewrite of afl-fuzz in Rust.
# TODO
- [x] AFL_HANG_TMOUT
- [x] AFL_NO_AUTODICT
- [x] AFL_MAP_SIZE
- [x] AFL_KILL_SIGNAL
- [x] AFL_BENCH_JUST_ONE
- [x] AFL_DEBUG_CHILD
- [x] AFL_PERSISTENT
- [x] AFL_IGNORE_TIMEOUTS
- [x] AFL_EXIT_ON_SEED_ISSUES
- [x] AFL_BENCH_UNTIL_CRASH
- [x] AFL_TMPDIR
- [x] AFL_CRASH_EXITCODE
- [x] AFL_TARGET_ENV
- [x] AFL_IGNORE_SEED_PROBLEMS (renamed to AFL_IGNORE_SEED_ISSUES)
- [x] AFL_CRASH_EXITCODE
- [x] AFL_INPUT_LEN_MIN
- [x] AFL_INPUT_LEN_MAX
- [x] AFL_CYCLE_SCHEDULES
- [x] AFL_CMPLOG_ONLY_NEW
- [x] AFL_PRELOAD
- [x] AFL_SKIP_BIN_CHECK
- [x] AFL_NO_STARTUP_CALIBRATION (this is default in libafl, not sure if this needs to be changed?)
- [x] AFL_FUZZER_STATS_UPDATE_INTERVAL
- [x] AFL_DEFER_FORKSRV
- [x] AFL_NO_WARN_INSTABILITY (we don't warn anyways, we should maybe?)
- [x] AFL_SYNC_TIME
- [ ] AFL_FINAL_SYNC
- [x] AFL_AUTORESUME
- [ ] AFL_CRASHING_SEEDS_AS_NEW_CRASH
- [ ] AFL_IGNORE_UNKNOWN_ENVS
- [ ] AFL_NO_UI
- [ ] AFL_PIZZA_MODE :)
- [ ] AFL_EXIT_WHEN_DONE
- [ ] AFL_EXIT_ON_TIME
- [ ] AFL_NO_AFFINITY
- [ ] AFL_FORKSERVER_KILL_SIGNAL
- [ ] AFL_EXPAND_HAVOC_NOW
- [ ] AFL_NO_FORKSRV
- [ ] AFL_FORKSRV_INIT_TMOUT
- [ ] AFL_TRY_AFFINITY
- [ ] AFL_FAST_CAL
- [ ] AFL_NO_CRASH_README
- [ ] AFL_KEEP_TIMEOUTS
- [ ] AFL_PERSISTENT_RECORD
- [ ] AFL_TESTCACHE_SIZE
- [ ] AFL_NO_ARITH
- [ ] AFL_DISABLE_TRIM
- [ ] AFL_MAX_DET_EXTRAS
- [ ] AFL_IGNORE_PROBLEMS
- [ ] AFL_IGNORE_PROBLEMS_COVERAGE
- [ ] AFL_STATSD_TAGS_FLAVOR
- [ ] AFL_STATSD
- [ ] AFL_STATSD_PORT
- [ ] AFL_STATSD_HOST
- [ ] AFL_IMPORT
- [x] AFL_IMPORT_FIRST (implicit)
- [ ] AFL_SHUFFLE_QUEUE
- [ ] AFL_CUSTOM_QEMU_BIN
- [ ] AFL_PATH
- [ ] AFL_CUSTOM_MUTATOR_LIBRARY
- [ ] AFL_CUSTOM_MUTATOR_ONLY
- [ ] AFL_PYTHON_MODULE
- [ ] AFL_DEBUG
- [ ] AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES
- [ ] AFL_DUMB_FORKSRV
- [ ] AFL_EARLY_FORKSERVER
- [ ] AFL_NO_SNAPSHOT

View File

@ -0,0 +1,602 @@
use core::{marker::PhantomData, time::Duration};
use std::{
borrow::Cow,
fmt::Display,
fs::{File, OpenOptions},
io::{BufRead, BufReader, Write},
path::PathBuf,
process,
};
use libafl::{
corpus::{Corpus, HasCurrentCorpusId, HasTestcase, SchedulerTestcaseMetadata, Testcase},
events::EventFirer,
executors::HasObservers,
inputs::UsesInput,
observers::MapObserver,
schedulers::{minimizer::IsFavoredMetadata, HasQueueCycles, Scheduler},
stages::{calibrate::UnstableEntriesMetadata, Stage},
state::{HasCorpus, HasExecutions, HasImported, HasStartTime, Stoppable, UsesState},
Error, HasMetadata, HasNamedMetadata, HasScheduler,
};
use libafl_bolts::{
current_time,
os::peak_rss_mb_child_processes,
tuples::{Handle, Handled, MatchNameRef},
Named,
};
use crate::{fuzzer::fuzzer_target_mode, Opt};
/// The [`AflStatsStage`] is a Stage that calculates and writes
/// AFL++'s `fuzzer_stats` and `plot_data` information.
#[derive(Debug, Clone)]
pub struct AflStatsStage<C, O, E, EM, Z> {
map_observer_handle: Handle<C>,
fuzzer_dir: PathBuf,
start_time: u64,
// the number of testcases that have been fuzzed
has_fuzzed_size: usize,
// the number of "favored" testcases
is_favored_size: usize,
// the last time that we report all stats
last_report_time: Duration,
// the interval at which we report all stats
stats_report_interval: Duration,
pid: u32,
slowest_exec: Duration,
max_depth: u64,
cycles_done: u64,
saved_crashes: u64,
saved_hangs: u64,
last_find: Duration,
last_hang: Duration,
last_crash: Duration,
exec_timeout: u64,
execs_at_last_objective: u64,
cycles_wo_finds: u64,
/// banner text (e.g., the target name)
afl_banner: Cow<'static, str>,
/// the version of libafl-fuzz used
afl_version: Cow<'static, str>,
/// default, persistent, qemu, unicorn, non-instrumented
target_mode: Cow<'static, str>,
/// full command line used for the fuzzing session
command_line: Cow<'static, str>,
phantom: PhantomData<(C, O, E, EM, Z)>,
}
#[derive(Debug, Clone)]
pub struct AFLFuzzerStats<'a> {
/// unix time indicating the start time of afl-fuzz
start_time: u64,
/// unix time corresponding to the last interval
last_update: u64,
/// run time in seconds to the last update of this file
run_time: u64,
/// process id of the fuzzer process
fuzzer_pid: u32,
/// queue cycles completed so far
cycles_done: u64,
/// number of queue cycles without any new paths found
cycles_wo_find: u64,
/// longest time in seconds no new path was found
time_wo_finds: u64,
/// TODO
fuzz_time: u64,
/// TODO
calibration_time: u64,
/// TODO
sync_time: u64,
/// TODO
trim_time: u64,
/// number of fuzzer executions attempted (what does attempted mean here?)
execs_done: u64,
/// overall number of execs per second
execs_per_sec: u64,
/// TODO
execs_ps_last_min: u64,
/// total number of entries in the queue
corpus_count: usize,
/// number of queue entries that are favored
corpus_favored: usize,
/// number of entries discovered through local fuzzing
corpus_found: usize,
/// number of entries imported from other instances
corpus_imported: usize,
/// number of levels in the generated data set
max_depth: u64,
/// currently processed entry number
cur_item: usize,
/// number of favored entries still waiting to be fuzzed
pending_favs: usize,
/// number of all entries waiting to be fuzzed
pending_total: usize,
/// number of test cases showing variable behavior
corpus_variable: u64,
/// percentage of bitmap bytes that behave consistently
stability: f64,
/// percentage of edge coverage found in the map so far,
bitmap_cvg: f64,
/// number of unique crashes recorded
saved_crashes: u64,
/// number of unique hangs encountered
saved_hangs: u64,
/// seconds since the last find was found
last_find: Duration,
/// seconds since the last crash was found
last_crash: Duration,
/// seconds since the last hang was found
last_hang: Duration,
/// execs since the last crash was found
execs_since_crash: u64,
/// the -t command line value
exec_timeout: u64,
/// real time of the slowest execution in ms
slowest_exec_ms: u128,
/// max rss usage reached during fuzzing in MB
peak_rss_mb: i64,
/// TODO
cpu_affinity: i64,
/// how many edges have been found
edges_found: u64,
/// TODO:
total_edges: u64,
/// how many edges are non-deterministic
var_byte_count: usize,
/// TODO:
havoc_expansion: usize,
/// TODO:
auto_dict_entries: usize,
/// TODO:
testcache_size: usize,
/// TODO:
testcache_count: usize,
/// TODO:
testcache_evict: usize,
/// banner text (e.g., the target name)
afl_banner: &'a Cow<'static, str>,
/// the version of AFL++ used
afl_version: &'a Cow<'static, str>,
/// default, persistent, qemu, unicorn, non-instrumented
target_mode: &'a Cow<'static, str>,
/// full command line used for the fuzzing session
command_line: &'a str,
}
#[derive(Debug, Clone)]
pub struct AFLPlotData<'a> {
relative_time: &'a u64,
cycles_done: &'a u64,
cur_item: &'a usize,
corpus_count: &'a usize,
pending_total: &'a usize,
pending_favs: &'a usize,
/// Note: renamed `map_size` -> `total_edges` for consistency with `fuzzer_stats`
total_edges: &'a u64,
saved_crashes: &'a u64,
saved_hangs: &'a u64,
max_depth: &'a u64,
execs_per_sec: &'a u64,
/// Note: renamed `total_execs` -> `execs_done` for consistency with `fuzzer_stats`
execs_done: &'a u64,
edges_found: &'a u64,
}
impl<C, O, E, EM, Z> UsesState for AflStatsStage<C, O, E, EM, Z>
where
E: UsesState,
EM: EventFirer<State = E::State>,
Z: UsesState<State = E::State>,
{
type State = E::State;
}
impl<C, O, E, EM, Z> Stage<E, EM, Z> for AflStatsStage<C, O, E, EM, Z>
where
E: UsesState + HasObservers,
EM: EventFirer<State = E::State>,
Z: UsesState<State = E::State> + HasScheduler,
E::State: HasImported
+ HasCorpus
+ HasMetadata
+ HasStartTime
+ HasExecutions
+ HasNamedMetadata
+ Stoppable
+ HasTestcase,
O: MapObserver,
C: AsRef<O> + Named,
<Z as HasScheduler>::Scheduler: Scheduler + HasQueueCycles,
{
fn perform(
&mut self,
fuzzer: &mut Z,
executor: &mut E,
state: &mut E::State,
_manager: &mut EM,
) -> Result<(), Error> {
let Some(corpus_idx) = state.current_corpus_id()? else {
return Err(Error::illegal_state(
"state is not currently processing a corpus index",
));
};
let testcase = state.corpus().get(corpus_idx)?.borrow();
// NOTE: scheduled_count represents the amount of fuzzing iterations a
// testcase has had. Since this stage is kept at the very end of stage list,
// the entry would have been fuzzed already (and should contain IsFavoredMetadata) but would have a scheduled count of zero
// since the scheduled count is incremented after all stages have been run.
if testcase.scheduled_count() == 0 {
// New testcase!
self.cycles_wo_finds = 0;
self.update_last_find();
self.maybe_update_last_crash(&testcase, state);
self.maybe_update_last_hang(&testcase, state);
self.update_has_fuzzed_size();
self.maybe_update_is_favored_size(&testcase);
}
self.maybe_update_slowest_exec(&testcase);
self.maybe_update_max_depth(&testcase)?;
// See if we actually need to run the stage, if not, avoid dynamic value computation.
if !self.check_interval() {
return Ok(());
}
let corpus_size = state.corpus().count();
let total_executions = *state.executions();
let scheduler = fuzzer.scheduler();
let queue_cycles = scheduler.queue_cycles();
self.maybe_update_cycles(queue_cycles);
self.maybe_update_cycles_wo_finds(queue_cycles);
let observers = executor.observers();
let map_observer = observers
.get(&self.map_observer_handle)
.ok_or_else(|| Error::key_not_found("invariant: MapObserver not found".to_string()))?
.as_ref();
let filled_entries_in_map = map_observer.count_bytes();
let map_size = map_observer.usable_count();
let unstable_entries_metadata = state
.metadata_map()
.get::<UnstableEntriesMetadata>()
.unwrap();
let unstable_entries_in_map = unstable_entries_metadata.unstable_entries().len();
let stats = AFLFuzzerStats {
start_time: self.start_time,
last_update: self.last_report_time.as_secs(),
run_time: self.last_report_time.as_secs() - self.start_time,
fuzzer_pid: self.pid,
cycles_done: queue_cycles,
cycles_wo_find: self.cycles_wo_finds,
fuzz_time: 0, // TODO
calibration_time: 0, // TODO
sync_time: 0, // TODO
trim_time: 0, // TODO
execs_done: total_executions,
execs_per_sec: *state.executions(), // TODO
execs_ps_last_min: *state.executions(), // TODO
max_depth: self.max_depth,
corpus_count: corpus_size,
corpus_favored: corpus_size - self.is_favored_size,
corpus_found: corpus_size - state.imported(),
corpus_imported: *state.imported(),
cur_item: corpus_idx.into(),
pending_total: corpus_size - self.has_fuzzed_size,
pending_favs: 0, // TODO
time_wo_finds: (current_time() - self.last_find).as_secs(),
corpus_variable: 0,
stability: self.calculate_stability(unstable_entries_in_map, filled_entries_in_map),
#[allow(clippy::cast_precision_loss)]
bitmap_cvg: (filled_entries_in_map as f64 / map_size as f64) * 100.0,
saved_crashes: self.saved_crashes,
saved_hangs: self.saved_hangs,
last_find: self.last_find,
last_hang: self.last_hang,
last_crash: self.last_crash,
execs_since_crash: total_executions - self.execs_at_last_objective,
exec_timeout: self.exec_timeout, // TODO
slowest_exec_ms: self.slowest_exec.as_millis(),
peak_rss_mb: peak_rss_mb_child_processes()?,
cpu_affinity: 0, // TODO
total_edges: map_size as u64,
edges_found: filled_entries_in_map,
var_byte_count: unstable_entries_metadata.unstable_entries().len(),
havoc_expansion: 0, // TODO
auto_dict_entries: 0, // TODO
testcache_size: 0,
testcache_count: 0,
testcache_evict: 0,
afl_banner: &self.afl_banner,
afl_version: &self.afl_version,
target_mode: &self.target_mode,
command_line: &self.command_line,
};
let plot_data = AFLPlotData {
corpus_count: &stats.corpus_count,
cur_item: &stats.cur_item,
cycles_done: &stats.cycles_done,
edges_found: &stats.edges_found,
total_edges: &stats.total_edges,
execs_per_sec: &stats.execs_per_sec,
pending_total: &stats.pending_total,
pending_favs: &stats.pending_favs,
max_depth: &stats.max_depth,
relative_time: &stats.run_time,
saved_hangs: &stats.saved_hangs,
saved_crashes: &stats.saved_crashes,
execs_done: &stats.execs_done,
};
self.write_fuzzer_stats(&stats)?;
self.write_plot_data(&plot_data)?;
Ok(())
}
fn should_restart(&mut self, _state: &mut Self::State) -> Result<bool, Error> {
Ok(true)
}
fn clear_progress(&mut self, _state: &mut Self::State) -> Result<(), Error> {
Ok(())
}
}
impl<C, O, E, EM, Z> AflStatsStage<C, O, E, EM, Z>
where
E: UsesState + HasObservers,
EM: EventFirer<State = E::State>,
Z: UsesState<State = E::State>,
E::State: HasImported + HasCorpus + HasMetadata + HasExecutions,
C: AsRef<O> + Named,
O: MapObserver,
{
/// create a new instance of the [`AflStatsStage`]
#[allow(clippy::too_many_arguments)]
#[must_use]
pub fn new(opt: &Opt, fuzzer_dir: PathBuf, map_observer: &C) -> Self {
Self::create_plot_data_file(&fuzzer_dir).unwrap();
Self::create_fuzzer_stats_file(&fuzzer_dir).unwrap();
Self {
map_observer_handle: map_observer.handle(),
start_time: current_time().as_secs(),
stats_report_interval: Duration::from_secs(opt.stats_interval),
has_fuzzed_size: 0,
is_favored_size: 0,
cycles_done: 0,
cycles_wo_finds: 0,
execs_at_last_objective: 0,
last_crash: current_time(),
last_find: current_time(),
last_hang: current_time(),
max_depth: 0,
saved_hangs: 0,
saved_crashes: 0,
slowest_exec: Duration::from_secs(0),
last_report_time: current_time(),
pid: process::id(),
exec_timeout: opt.hang_timeout,
target_mode: fuzzer_target_mode(opt),
afl_banner: Cow::Owned(opt.executable.display().to_string()),
afl_version: Cow::Borrowed("libafl-fuzz-0.0.1"),
command_line: get_run_cmdline(),
fuzzer_dir,
phantom: PhantomData,
}
}
fn create_plot_data_file(fuzzer_dir: &PathBuf) -> Result<(), Error> {
let path = fuzzer_dir.join("plot_data");
if path.exists() {
// check if it contains any data
let file = File::open(path)?;
if BufReader::new(file).lines().next().is_none() {
std::fs::write(fuzzer_dir.join("plot_data"), AFLPlotData::get_header())?;
}
} else {
std::fs::write(fuzzer_dir.join("plot_data"), AFLPlotData::get_header())?;
}
Ok(())
}
fn create_fuzzer_stats_file(fuzzer_dir: &PathBuf) -> Result<(), Error> {
let path = fuzzer_dir.join("fuzzer_stats");
if !path.exists() {
OpenOptions::new().append(true).create(true).open(path)?;
}
Ok(())
}
fn write_fuzzer_stats(&self, stats: &AFLFuzzerStats) -> Result<(), Error> {
std::fs::write(self.fuzzer_dir.join("fuzzer_stats"), stats.to_string())?;
Ok(())
}
fn write_plot_data(&self, plot_data: &AFLPlotData) -> Result<(), Error> {
let mut file = OpenOptions::new()
.append(true)
.open(self.fuzzer_dir.join("plot_data"))?;
writeln!(file, "{plot_data}")?;
Ok(())
}
fn maybe_update_is_favored_size(
&mut self,
testcase: &Testcase<<<E as UsesState>::State as UsesInput>::Input>,
) {
if testcase.has_metadata::<IsFavoredMetadata>() {
self.is_favored_size += 1;
}
}
fn maybe_update_slowest_exec(
&mut self,
testcase: &Testcase<<<E as UsesState>::State as UsesInput>::Input>,
) {
if let Some(exec_time) = testcase.exec_time() {
if exec_time > &self.slowest_exec {
self.slowest_exec = *exec_time;
}
}
}
fn update_has_fuzzed_size(&mut self) {
self.has_fuzzed_size += 1;
}
fn maybe_update_max_depth(
&mut self,
testcase: &Testcase<<<E as UsesState>::State as UsesInput>::Input>,
) -> Result<(), Error> {
if let Ok(metadata) = testcase.metadata::<SchedulerTestcaseMetadata>() {
if metadata.depth() > self.max_depth {
self.max_depth = metadata.depth();
}
} else {
return Err(Error::illegal_state(
"testcase must have scheduler metdata?",
));
}
Ok(())
}
fn update_last_find(&mut self) {
self.last_find = current_time();
}
fn maybe_update_last_crash(
&mut self,
testcase: &Testcase<<<E as UsesState>::State as UsesInput>::Input>,
state: &E::State,
) {
if testcase
.hit_objectives()
.contains(&Cow::Borrowed("CrashFeedback"))
{
self.last_crash = current_time();
self.execs_at_last_objective = *state.executions();
}
}
fn maybe_update_last_hang(
&mut self,
testcase: &Testcase<<<E as UsesState>::State as UsesInput>::Input>,
state: &E::State,
) {
if testcase
.hit_objectives()
.contains(&Cow::Borrowed("TimeoutFeedback"))
{
self.last_hang = current_time();
self.execs_at_last_objective = *state.executions();
}
}
fn check_interval(&mut self) -> bool {
let cur = current_time();
if cur.checked_sub(self.last_report_time).unwrap_or_default() > self.stats_report_interval {
self.last_report_time = cur;
return true;
}
false
}
fn maybe_update_cycles(&mut self, queue_cycles: u64) {
if queue_cycles > self.cycles_done {
self.cycles_done += 1;
}
}
fn maybe_update_cycles_wo_finds(&mut self, queue_cycles: u64) {
if queue_cycles > self.cycles_done && self.last_find < current_time() {
self.cycles_wo_finds += 1;
}
}
#[allow(clippy::cast_precision_loss)]
#[allow(clippy::unused_self)]
fn calculate_stability(&self, unstable_entries: usize, filled_entries: u64) -> f64 {
((filled_entries as f64 - unstable_entries as f64) / filled_entries as f64) * 100.0
}
}
impl Display for AFLPlotData<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{},", self.relative_time)?;
write!(f, "{},", self.cycles_done)?;
write!(f, "{},", self.cur_item)?;
write!(f, "{},", self.corpus_count)?;
write!(f, "{},", self.pending_total)?;
write!(f, "{},", self.pending_favs)?;
write!(f, "{},", self.total_edges)?;
write!(f, "{},", self.saved_crashes)?;
write!(f, "{},", self.saved_hangs)?;
write!(f, "{},", self.max_depth)?;
write!(f, "{},", self.execs_per_sec)?;
write!(f, "{},", self.execs_done)?;
write!(f, "{}", self.edges_found)?;
Ok(())
}
}
impl AFLPlotData<'_> {
fn get_header() -> String {
"# relative_time, cycles_done, cur_item, corpus_count, pending_total, pending_favs, total_edges, saved_crashes, saved_hangs, max_depth, execs_per_sec, execs_done, edges_found".to_string()
}
}
impl Display for AFLFuzzerStats<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "start_time : {}", &self.start_time)?;
writeln!(f, "start_time : {}", &self.start_time)?;
writeln!(f, "last_update : {}", &self.last_update)?;
writeln!(f, "run_time : {}", &self.run_time)?;
writeln!(f, "fuzzer_pid : {}", &self.fuzzer_pid)?;
writeln!(f, "cycles_done : {}", &self.cycles_done)?;
writeln!(f, "cycles_wo_find : {}", &self.cycles_wo_find)?;
writeln!(f, "time_wo_finds : {}", &self.time_wo_finds)?;
writeln!(f, "fuzz_time : {}", &self.fuzz_time)?;
writeln!(f, "calibration_time : {}", &self.calibration_time)?;
writeln!(f, "sync_time : {}", &self.sync_time)?;
writeln!(f, "trim_time : {}", &self.trim_time)?;
writeln!(f, "execs_done : {}", &self.execs_done)?;
writeln!(f, "execs_per_sec : {}", &self.execs_per_sec)?;
writeln!(f, "execs_ps_last_min : {}", &self.execs_ps_last_min)?;
writeln!(f, "corpus_count : {}", &self.corpus_count)?;
writeln!(f, "corpus_favored : {}", &self.corpus_favored)?;
writeln!(f, "corpus_found : {}", &self.corpus_found)?;
writeln!(f, "corpus_imported : {}", &self.corpus_imported)?;
writeln!(f, "max_depth : {}", &self.max_depth)?;
writeln!(f, "cur_item : {}", &self.cur_item)?;
writeln!(f, "pending_favs : {}", &self.pending_favs)?;
writeln!(f, "pending_total : {}", &self.pending_total)?;
writeln!(f, "corpus_variable : {}", &self.corpus_variable)?;
writeln!(f, "stability : {:.2}%", &self.stability)?;
writeln!(f, "bitmap_cvg : {:.2}%", &self.bitmap_cvg)?;
writeln!(f, "saved_crashes : {}", &self.saved_crashes)?;
writeln!(f, "saved_hangs : {}", &self.saved_hangs)?;
writeln!(f, "last_find : {}", &self.last_find.as_secs())?;
writeln!(f, "last_crash : {}", &self.last_crash.as_secs())?;
writeln!(f, "last_hang : {}", &self.last_hang.as_secs())?;
writeln!(f, "execs_since_crash : {}", &self.execs_since_crash)?;
writeln!(f, "exec_timeout : {}", &self.exec_timeout)?;
writeln!(f, "slowest_exec_ms : {}", &self.slowest_exec_ms)?;
writeln!(f, "peak_rss_mb : {}", &self.peak_rss_mb)?;
writeln!(f, "cpu_affinity : {}", &self.cpu_affinity)?;
writeln!(f, "edges_found : {}", &self.edges_found)?;
writeln!(f, "total_edges : {}", &self.total_edges)?;
writeln!(f, "var_byte_count : {}", &self.var_byte_count)?;
writeln!(f, "havoc_expansion : {}", &self.havoc_expansion)?;
writeln!(f, "auto_dict_entries : {}", &self.auto_dict_entries)?;
writeln!(f, "testcache_size : {}", &self.testcache_size)?;
writeln!(f, "testcache_count : {}", &self.testcache_count)?;
writeln!(f, "testcache_evict : {}", &self.testcache_evict)?;
writeln!(f, "afl_banner : {}", self.afl_banner)?;
writeln!(f, "afl_version : {}", self.afl_version)?;
writeln!(f, "target_mode : {}", self.target_mode)?;
writeln!(f, "command_line : {}", self.command_line)?;
Ok(())
}
}
/// Get the command used to invoke libafl-fuzz
pub fn get_run_cmdline() -> Cow<'static, str> {
let args: Vec<String> = std::env::args().collect();
Cow::Owned(args.join(" "))
}

View File

@ -0,0 +1,189 @@
use std::{
borrow::Cow,
fs::File,
io::{self, BufRead, BufReader},
path::{Path, PathBuf},
};
use libafl::{
corpus::{Corpus, Testcase},
inputs::BytesInput,
state::{HasCorpus, HasExecutions, HasStartTime},
Error,
};
use libafl_bolts::current_time;
use nix::{
errno::Errno,
fcntl::{Flock, FlockArg},
};
use crate::{fuzzer::LibaflFuzzState, OUTPUT_GRACE};
pub fn generate_base_filename(state: &mut LibaflFuzzState) -> String {
let is_seed = state.must_load_initial_inputs();
let id = state.corpus().peek_free_id().0;
let name = if is_seed {
// TODO set orig filename
format!("id:{id:0>6},time:0,execs:0,orig:TODO",)
} else {
// TODO: change hardcoded values of op (operation aka stage_name) & rep (amount of stacked mutations applied)
let src = if let Some(parent_id) = state.corpus().current() {
parent_id.0
} else {
0
};
let execs = *state.executions();
let time = (current_time() - *state.start_time()).as_secs();
format!("id:{id:0>6},src:{src:0>6},time:{time},execs:{execs},op:havoc,rep:0",)
};
name
}
pub fn set_corpus_filepath(
state: &mut LibaflFuzzState,
testcase: &mut Testcase<BytesInput>,
_fuzzer_dir: &PathBuf,
) -> Result<(), Error> {
let mut name = generate_base_filename(state);
if testcase.hit_feedbacks().contains(&Cow::Borrowed("edges")) {
name = format!("{name},+cov");
}
*testcase.filename_mut() = Some(name);
// We don't need to set the path since everything goes into one dir unlike with Objectives
Ok(())
}
pub fn set_solution_filepath(
state: &mut LibaflFuzzState,
testcase: &mut Testcase<BytesInput>,
output_dir: &PathBuf,
) -> Result<(), Error> {
// sig:0SIGNAL
// TODO: verify if 0 time if objective found during seed loading
let mut filename = generate_base_filename(state);
let mut dir = "crashes";
if testcase
.hit_objectives()
.contains(&Cow::Borrowed("TimeoutFeedback"))
{
filename = format!("{filename},+tout");
dir = "hangs";
}
*testcase.file_path_mut() = Some(output_dir.join(dir).join(&filename));
*testcase.filename_mut() = Some(filename);
Ok(())
}
fn parse_time_line(line: &str) -> Result<u64, Error> {
line.split(": ")
.last()
.ok_or(Error::illegal_state("invalid stats file"))?
.parse()
.map_err(|_| Error::illegal_state("invalid stats file"))
}
pub fn check_autoresume(
fuzzer_dir: &Path,
intial_inputs: &PathBuf,
auto_resume: bool,
) -> Result<Flock<File>, Error> {
if !fuzzer_dir.exists() {
std::fs::create_dir(fuzzer_dir)?;
}
// lock the fuzzer dir
let fuzzer_dir_fd = File::open(fuzzer_dir)?;
let file = match Flock::lock(fuzzer_dir_fd, FlockArg::LockExclusiveNonblock) {
Ok(l) => l,
Err(err) => match err.1 {
Errno::EWOULDBLOCK => return Err(Error::illegal_state(
"Looks like the job output directory is being actively used by another instance",
)),
_ => {
return Err(Error::last_os_error(
format!("Error creating lock for output dir: exit code {}", err.1).as_str(),
))
}
},
};
// Check if we have an existing fuzzed fuzzer_dir
let stats_file = fuzzer_dir.join("fuzzer_stats");
if stats_file.exists() {
let file = File::open(&stats_file).unwrap();
let reader = BufReader::new(file);
let mut start_time: u64 = 0;
let mut last_update: u64 = 0;
for (index, line) in reader.lines().enumerate() {
match index {
// first line is start_time
0 => {
start_time = parse_time_line(&line?).unwrap();
}
// second_line is last_update
1 => {
last_update = parse_time_line(&line?).unwrap();
}
_ => break,
}
}
if !auto_resume && last_update.saturating_sub(start_time) > OUTPUT_GRACE * 60 {
return Err(Error::illegal_state("The job output directory already exists and contains results! use AFL_AUTORESUME=true or provide \"-\" for -i "));
}
}
if auto_resume {
// TODO: once the queue stuff is implemented finish the rest of the function
// see afl-fuzz-init.c line 1898 onwards. Gotta copy and delete shit
// No usable test cases in './output/default/_resume'
} else {
let queue_dir = fuzzer_dir.join("queue");
let hangs_dir = fuzzer_dir.join("hangs");
let crashes_dir = fuzzer_dir.join("crashes");
// Create our (sub) directories for Objectives & Corpus
create_dir_if_not_exists(&crashes_dir).expect("should be able to create crashes dir");
create_dir_if_not_exists(&hangs_dir).expect("should be able to create hangs dir");
create_dir_if_not_exists(&queue_dir).expect("should be able to create queue dir");
// Copy all our seeds to queue
for file in std::fs::read_dir(intial_inputs)? {
let path = file?.path();
std::fs::copy(
&path,
queue_dir.join(path.file_name().ok_or(Error::illegal_state(format!(
"file {} in input directory does not have a filename",
path.display()
)))?),
)?;
}
}
Ok(file)
}
pub fn create_dir_if_not_exists(path: &PathBuf) -> std::io::Result<()> {
if path.is_file() {
return Err(io::Error::new(
// TODO: change this to ErrorKind::NotADirectory
// when stabilitzed https://github.com/rust-lang/rust/issues/86442
io::ErrorKind::Other,
format!("{} expected a directory; got a file", path.display()),
));
}
match std::fs::create_dir(path) {
Ok(()) => Ok(()),
Err(err) => {
if matches!(err.kind(), io::ErrorKind::AlreadyExists) {
Ok(())
} else {
Err(err)
}
}
}
}
pub fn remove_main_node_file(output_dir: &PathBuf) -> Result<(), Error> {
for entry in std::fs::read_dir(output_dir)?.filter_map(std::result::Result::ok) {
let path = entry.path();
if path.is_dir() && path.join("is_main_node").exists() {
std::fs::remove_file(path.join("is_main_node"))?;
return Ok(());
}
}
Err(Error::illegal_state("main node's directory not found!"))
}

View File

@ -0,0 +1,155 @@
use std::{collections::HashMap, path::PathBuf, time::Duration};
use libafl::Error;
use libafl_bolts::core_affinity::Cores;
use crate::Opt;
pub fn parse_envs(opt: &mut Opt) -> Result<(), Error> {
if let Ok(res) = std::env::var("AFL_CORES") {
opt.cores = Some(Cores::from_cmdline(&res)?);
} else {
return Err(Error::illegal_argument("Missing AFL_CORES"));
}
if let Ok(res) = std::env::var("AFL_INPUT_LEN_MAX") {
opt.max_input_len = Some(res.parse()?);
}
if let Ok(res) = std::env::var("AFL_INPUT_LEN_MIN") {
opt.min_input_len = Some(res.parse()?);
}
if let Ok(res) = std::env::var("AFL_BENCH_JUST_ONE") {
opt.bench_just_one = parse_bool(&res)?;
}
if let Ok(res) = std::env::var("AFL_BENCH_UNTIL_CRASH") {
opt.bench_until_crash = parse_bool(&res)?;
}
if let Ok(res) = std::env::var("AFL_HANG_TMOUT") {
opt.hang_timeout = res.parse()?;
} else {
opt.hang_timeout = 100;
}
if let Ok(res) = std::env::var("AFL_DEBUG_CHILD") {
opt.debug_child = parse_bool(&res)?;
}
if let Ok(res) = std::env::var("AFL_PERSISTENT") {
opt.is_persistent = parse_bool(&res)?;
}
if let Ok(res) = std::env::var("AFL_NO_AUTODICT") {
opt.no_autodict = parse_bool(&res)?;
}
if let Ok(res) = std::env::var("AFL_MAP_SIZE") {
let map_size = res.parse()?;
validate_map_size(map_size)?;
opt.map_size = Some(map_size);
};
if let Ok(res) = std::env::var("AFL_IGNORE_TIMEOUT") {
opt.ignore_timeouts = parse_bool(&res)?;
}
if let Ok(res) = std::env::var("AFL_TMPDIR") {
opt.cur_input_dir = Some(PathBuf::from(res));
}
if let Ok(res) = std::env::var("AFL_CRASH_EXITCODE") {
opt.crash_exitcode = Some(res.parse()?);
}
if let Ok(res) = std::env::var("AFL_TARGET_ENV") {
opt.target_env = parse_target_env(&res)?;
}
if let Ok(res) = std::env::var("AFL_CYCLE_SCHEDULES") {
opt.cycle_schedules = parse_bool(&res)?;
}
if let Ok(res) = std::env::var("AFL_CMPLOG_ONLY_NEW") {
opt.cmplog_only_new = parse_bool(&res)?;
}
if let Ok(res) = std::env::var("AFL_PRELOAD") {
opt.afl_preload = Some(res);
}
if let Ok(res) = std::env::var("AFL_SKIP_BIN_CHECK") {
opt.skip_bin_check = parse_bool(&res)?;
}
if let Ok(res) = std::env::var("AFL_AUTORESUME") {
opt.auto_resume = parse_bool(&res)?;
}
if let Ok(res) = std::env::var("AFL_DEFER_FORKSRV") {
opt.defer_forkserver = parse_bool(&res)?;
}
if let Ok(res) = std::env::var("AFL_FUZZER_STATS_UPDATE_INTERVAL") {
opt.stats_interval = res.parse()?;
}
if let Ok(res) = std::env::var("AFL_BROKER_PORT") {
opt.broker_port = Some(res.parse()?);
}
if let Ok(res) = std::env::var("AFL_EXIT_ON_SEED_ISSUES") {
opt.exit_on_seed_issues = parse_bool(&res)?;
}
if let Ok(res) = std::env::var("AFL_IGNORE_SEED_ISSUES") {
opt.ignore_seed_issues = parse_bool(&res)?;
}
if let Ok(res) = std::env::var("AFL_CRASHING_SEED_AS_NEW_CRASH") {
opt.crash_seed_as_new_crash = parse_bool(&res)?;
}
if let Ok(res) = std::env::var("AFL_FRIDA_PERSISTENT_ADDR") {
opt.frida_persistent_addr = Some(res);
}
if let Ok(res) = std::env::var("AFL_QEMU_CUSTOM_BIN") {
opt.qemu_custom_bin = parse_bool(&res)?;
}
if let Ok(res) = std::env::var("AFL_CS_CUSTOM_BIN") {
opt.cs_custom_bin = parse_bool(&res)?;
}
if let Ok(res) = std::env::var("AFL_KILL_SIGNAL") {
opt.kill_signal = Some(res.parse()?);
}
if let Ok(res) = std::env::var("AFL_KILL_SIGNAL") {
opt.kill_signal = Some(res.parse()?);
}
if let Ok(res) = std::env::var("AFL_SYNC_TIME") {
opt.foreign_sync_interval = Duration::from_secs(res.parse::<u64>()? * 60);
} else {
opt.foreign_sync_interval = Duration::from_secs(AFL_DEFAULT_FOREIGN_SYNC_INTERVAL);
}
Ok(())
}
fn parse_bool(val: &str) -> Result<bool, Error> {
match val {
"1" => Ok(true),
"0" => Ok(false),
_ => Err(Error::illegal_argument(
"boolean values must be either 1 for true or 0 for false",
)),
}
}
/// parse `AFL_TARGET_ENV`; expects: FOO=BAR TEST=ASD
fn parse_target_env(s: &str) -> Result<Option<HashMap<String, String>>, Error> {
let env_regex = regex::Regex::new(r"([^\s=]+)\s*=\s*([^\s]+)").unwrap();
let mut target_env = HashMap::new();
for vars in env_regex.captures_iter(s) {
target_env.insert(
vars.get(1)
.ok_or(Error::illegal_argument("invalid AFL_TARGET_ENV format"))?
.as_str()
.to_string(),
vars.get(2)
.ok_or(Error::illegal_argument("invalid AFL_TARGET_ENV format"))?
.as_str()
.to_string(),
);
}
Ok(Some(target_env))
}
fn validate_map_size(map_size: usize) -> Result<usize, Error> {
if map_size > AFL_MAP_SIZE_MIN && map_size < AFL_MAP_SIZE_MAX {
Ok(map_size)
} else {
Err(Error::illegal_argument(format!(
"AFL_MAP_SIZE not in range {AFL_MAP_SIZE_MIN} (2 ^ 3) - {AFL_MAP_SIZE_MAX} (2 ^ 30)",
)))
}
}
const AFL_MAP_SIZE_MIN: usize = usize::pow(2, 3);
const AFL_MAP_SIZE_MAX: usize = usize::pow(2, 30);
const AFL_DEFAULT_FOREIGN_SYNC_INTERVAL: u64 = 20 * 60;
pub const AFL_DEFAULT_MAP_SIZE: usize = 65536;

View File

@ -0,0 +1,176 @@
use std::{
fs::File,
os::{linux::fs::MetadataExt, unix::fs::PermissionsExt},
path::{Path, PathBuf},
};
use libafl::Error;
use memmap2::{Mmap, MmapOptions};
use crate::{Opt, DEFER_SIG, PERSIST_SIG};
// TODO better error messages and logging
pub fn check_binary(opt: &mut Opt, shmem_env_var: &str) -> Result<(), Error> {
println!("Validating target binary...");
let bin_path;
// check if it is a file path
if opt.executable.components().count() == 1 {
// check $PATH for the binary.
if let Some(full_bin_path) = find_executable_in_path(&opt.executable) {
opt.executable = full_bin_path;
bin_path = &opt.executable;
} else {
return Err(Error::illegal_argument(format!(
"Program '{}' not found or not executable",
opt.executable.display()
)));
}
} else {
bin_path = &opt.executable;
#[cfg(target_os = "linux")]
{
if opt.nyx_mode {
if !bin_path.is_symlink() && bin_path.is_dir() {
let config_file = bin_path.join("config.ron");
if !config_file.is_symlink() && config_file.is_file() {
return Ok(());
}
}
return Err(Error::illegal_argument(
format!(
"Directory '{}' not found, or is a symlink or is not a nyx share directory",
bin_path.display()
)
.as_str(),
));
}
}
}
let metadata = bin_path.metadata()?;
let is_reg = !bin_path.is_symlink() && !bin_path.is_dir();
let bin_size = metadata.st_size();
let is_executable = metadata.permissions().mode() & 0o111 != 0;
if !is_reg || !is_executable || bin_size < 4 {
return Err(Error::illegal_argument(format!(
"Program '{}' not found or not executable",
bin_path.display()
)));
}
if opt.skip_bin_check
|| opt.use_wine
|| opt.unicorn_mode
|| (opt.qemu_mode && opt.qemu_custom_bin)
|| (opt.forkserver_cs && opt.cs_custom_bin)
|| opt.non_instrumented_mode
{
return Ok(());
}
let file = File::open(bin_path)?;
let mmap = unsafe { MmapOptions::new().map(&file)? };
// check if it's a shell script
if mmap[0..1] == [0x43, 0x41] {
// TODO: finish error message
return Err(Error::illegal_argument(
"Oops, the target binary looks like a shell script.",
));
}
// check if the binary is an ELF file
if mmap[0..4] != [0x7f, 0x45, 0x4c, 0x46] {
return Err(Error::illegal_argument(format!(
"Program '{}' is not an ELF binary",
bin_path.display()
)));
}
#[cfg(all(target_os = "macos", not(target_arch = "arm")))]
{
if (mmap[0] != 0xCF || mmap[1] != 0xFA || mmap[2] != 0xED)
&& (mmap[0] != 0xCA || mmap[1] != 0xFE || mmap[2] != 0xBA)
{
return Err(Error::illegal_argument(format!(
"Program '{}' is not a 64-bit or universal Mach-O binary",
bin_path.display()
)));
}
}
let check_instrumentation = !opt.qemu_mode
&& !opt.frida_mode
&& !opt.unicorn_mode
&& !opt.forkserver_cs
&& !opt.non_instrumented_mode;
#[cfg(target_os = "linux")]
let check_instrumentation = check_instrumentation && !opt.nyx_mode;
if check_instrumentation && !is_instrumented(&mmap, shmem_env_var) {
return Err(Error::illegal_argument(
"target binary is not instrumented correctly",
));
}
if opt.forkserver_cs || opt.qemu_mode || opt.frida_mode && is_instrumented(&mmap, shmem_env_var)
{
return Err(Error::illegal_argument("Instrumentation found in -Q mode"));
}
if mmap_has_substr(&mmap, "__asan_init")
|| mmap_has_substr(&mmap, "__lsan_init")
|| mmap_has_substr(&mmap, "__lsan_init")
{
opt.uses_asan = true;
}
if mmap_has_substr(&mmap, PERSIST_SIG) {
opt.is_persistent = true;
} else if opt.is_persistent {
println!("persistent mode enforced");
} else if opt.frida_persistent_addr.is_some() {
println!("FRIDA persistent mode configuration options detected");
}
if opt.frida_mode || mmap_has_substr(&mmap, DEFER_SIG) {
println!("deferred forkserver binary detected");
opt.defer_forkserver = true;
} else if opt.defer_forkserver {
println!("defer forkserver enforced");
}
Ok(())
// Safety: unmap() is called on Mmap object Drop
}
fn mmap_has_substr(mmap: &Mmap, sub_str: &str) -> bool {
let mmap_len = mmap.len();
let substr_len = sub_str.len();
if mmap_len < substr_len {
return false;
}
for i in 0..(mmap_len - substr_len) {
if &mmap[i..i + substr_len] == sub_str.as_bytes() {
return true;
}
}
false
}
fn is_instrumented(mmap: &Mmap, shmem_env_var: &str) -> bool {
mmap_has_substr(mmap, shmem_env_var)
}
fn find_executable_in_path(executable: &Path) -> Option<PathBuf> {
std::env::var_os("PATH").and_then(|paths| {
std::env::split_paths(&paths).find_map(|dir| {
let full_path = dir.join(executable);
if full_path.is_file() {
Some(full_path)
} else {
None
}
})
})
}

View File

@ -0,0 +1,136 @@
use std::{
borrow::Cow,
fmt::{Debug, Formatter},
marker::PhantomData,
path::PathBuf,
};
use libafl::{
corpus::Testcase,
events::EventFirer,
executors::ExitKind,
feedbacks::{Feedback, FeedbackFactory},
inputs::Input,
observers::ObserversTuple,
state::State,
};
use libafl_bolts::{Error, Named};
use serde::{Deserialize, Serialize};
/// A [`CustomFilepathToTestcaseFeedback`] takes a closure which can set the file name and path for the testcase.
/// Is never interesting (use with an OR).
/// Note: If used as part of the `Objective` chain, then it will only apply to testcases which are
/// `Objectives`, vice versa for `Feedback`.
#[derive(Serialize, Deserialize)]
pub struct CustomFilepathToTestcaseFeedback<F, I, S>
where
I: Input,
S: State<Input = I>,
F: FnMut(&mut S, &mut Testcase<I>, &PathBuf) -> Result<(), Error>,
{
/// Closure that returns the filename.
func: F,
/// The root output directory
out_dir: PathBuf,
phantomm: PhantomData<(I, S)>,
}
impl<F, I, S> CustomFilepathToTestcaseFeedback<F, I, S>
where
I: Input,
S: State<Input = I>,
F: FnMut(&mut S, &mut Testcase<I>, &PathBuf) -> Result<(), Error>,
{
/// Create a new [`CustomFilepathToTestcaseFeedback`].
pub fn new(func: F, out_dir: PathBuf) -> Self {
Self {
func,
out_dir,
phantomm: PhantomData,
}
}
}
impl<F, I, S, T> FeedbackFactory<CustomFilepathToTestcaseFeedback<F, I, S>, T>
for CustomFilepathToTestcaseFeedback<F, I, S>
where
I: Input,
S: State<Input = I>,
F: FnMut(&mut S, &mut Testcase<I>, &PathBuf) -> Result<(), Error> + Clone,
{
fn create_feedback(&self, _ctx: &T) -> CustomFilepathToTestcaseFeedback<F, I, S> {
Self {
func: self.func.clone(),
phantomm: self.phantomm,
out_dir: self.out_dir.clone(),
}
}
}
impl<F, I, S> Named for CustomFilepathToTestcaseFeedback<F, I, S>
where
I: Input,
S: State<Input = I>,
F: FnMut(&mut S, &mut Testcase<I>, &PathBuf) -> Result<(), Error>,
{
fn name(&self) -> &Cow<'static, str> {
static NAME: Cow<'static, str> = Cow::Borrowed("CustomFilepathToTestcaseFeedback");
&NAME
}
}
impl<F, I, S> Debug for CustomFilepathToTestcaseFeedback<F, I, S>
where
I: Input,
S: State<Input = I>,
F: FnMut(&mut S, &mut Testcase<I>, &PathBuf) -> Result<(), Error>,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CustomFilepathToTestcaseFeedback")
.finish_non_exhaustive()
}
}
impl<F, I, S> Feedback<S> for CustomFilepathToTestcaseFeedback<F, I, S>
where
S: State<Input = I>,
F: FnMut(&mut S, &mut Testcase<S::Input>, &PathBuf) -> Result<(), Error>,
I: Input,
{
#[allow(clippy::wrong_self_convention)]
#[inline]
fn is_interesting<EM, OT>(
&mut self,
_state: &mut S,
_manager: &mut EM,
_input: &I,
_observers: &OT,
_exit_kind: &ExitKind,
) -> Result<bool, Error>
where
EM: EventFirer<State = S>,
{
Ok(false)
}
fn append_metadata<EM, OT>(
&mut self,
state: &mut S,
_manager: &mut EM,
_observers: &OT,
testcase: &mut Testcase<<S>::Input>,
) -> Result<(), Error>
where
OT: ObserversTuple<S>,
EM: EventFirer<State = S>,
{
(self.func)(state, testcase, &self.out_dir)?;
Ok(())
}
#[cfg(feature = "track_hit_feedbacks")]
#[inline]
fn last_result(&self) -> Result<bool, Error> {
Ok(false)
}
}

View File

@ -0,0 +1,2 @@
pub mod filepath;
pub mod seed;

View File

@ -0,0 +1,151 @@
use std::{borrow::Cow, marker::PhantomData};
use libafl::{
corpus::Testcase, events::EventFirer, executors::ExitKind, feedbacks::Feedback,
observers::ObserversTuple, state::State, Error,
};
use libafl_bolts::Named;
use crate::Opt;
/// A wrapper feedback used to determine actions for initial seeds.
/// Handles `AFL_EXIT_ON_SEED_ISSUES`, `AFL_IGNORE_SEED_ISSUES` & default afl-fuzz behavior
/// then, essentially becomes benign
#[allow(clippy::module_name_repetitions)]
#[derive(Debug)]
pub struct SeedFeedback<A, S>
where
A: Feedback<S>,
S: State,
{
/// Inner [`Feedback`]
pub inner: A,
ignore_timeouts: bool,
ignore_seed_issues: bool,
exit_on_seed_issues: bool,
phantom: PhantomData<S>,
done_loading_seeds: bool,
}
impl<A, S> SeedFeedback<A, S>
where
A: Feedback<S>,
S: State,
{
pub fn new(inner: A, opt: &Opt) -> Self {
Self {
inner,
ignore_timeouts: opt.ignore_seed_issues,
ignore_seed_issues: opt.ignore_seed_issues,
exit_on_seed_issues: opt.exit_on_seed_issues,
phantom: PhantomData,
done_loading_seeds: false,
}
}
}
impl<A, S> Feedback<S> for SeedFeedback<A, S>
where
A: Feedback<S>,
S: State,
{
fn init_state(&mut self, state: &mut S) -> Result<(), Error> {
self.inner.init_state(state)?;
Ok(())
}
fn is_interesting<EM, OT>(
&mut self,
state: &mut S,
manager: &mut EM,
input: &S::Input,
observers: &OT,
exit_kind: &ExitKind,
) -> Result<bool, Error>
where
EM: EventFirer<State = S>,
OT: ObserversTuple<S>,
{
if !self.done_loading_seeds {
match exit_kind {
ExitKind::Timeout => {
if !self.ignore_timeouts {
if !self.ignore_seed_issues || self.exit_on_seed_issues {
return Err(Error::invalid_corpus(
"input led to a timeout; use AFL_IGNORE_SEED_ISSUES=true",
));
}
return Ok(false);
}
}
ExitKind::Crash => {
if self.exit_on_seed_issues {
return Err(Error::invalid_corpus("input let to a crash; either omit AFL_EXIT_ON_SEED_ISSUES or set it to false."));
}
// We regard all crashes as uninteresting during seed loading
return Ok(false);
}
_ => {}
}
}
let is_interesting = self
.inner
.is_interesting(state, manager, input, observers, exit_kind)?;
Ok(is_interesting)
}
/// Append to the testcase the generated metadata in case of a new corpus item
#[inline]
fn append_metadata<EM, OT>(
&mut self,
state: &mut S,
manager: &mut EM,
observers: &OT,
testcase: &mut Testcase<S::Input>,
) -> Result<(), Error>
where
OT: ObserversTuple<S>,
EM: EventFirer<State = S>,
{
self.inner
.append_metadata(state, manager, observers, testcase)?;
Ok(())
}
/// Discard the stored metadata in case that the testcase is not added to the corpus
#[inline]
fn discard_metadata(&mut self, state: &mut S, input: &S::Input) -> Result<(), Error> {
self.inner.discard_metadata(state, input)?;
Ok(())
}
#[cfg(feature = "track_hit_feedbacks")]
fn last_result(&self) -> Result<bool, Error> {
self.inner.last_result()
}
#[cfg(feature = "track_hit_feedbacks")]
fn append_hit_feedbacks(&self, list: &mut Vec<Cow<'static, str>>) -> Result<(), Error> {
if self.inner.last_result()? {
self.inner.append_hit_feedbacks(list)?;
}
Ok(())
}
}
impl<S, A> Named for SeedFeedback<A, S>
where
A: Feedback<S>,
S: State,
{
#[inline]
fn name(&self) -> &Cow<'static, str> {
static NAME: Cow<'static, str> = Cow::Borrowed("SeedFeedback");
&NAME
}
}
impl<S, A> SeedFeedback<A, S>
where
A: Feedback<S>,
S: State,
{
pub fn done_loading_seeds(&mut self) {
self.done_loading_seeds = true;
}
}

View File

@ -0,0 +1,438 @@
use std::{borrow::Cow, marker::PhantomData, path::PathBuf, time::Duration};
use libafl::{
corpus::{CachedOnDiskCorpus, Corpus, OnDiskCorpus},
events::{
CentralizedEventManager, EventManagerHooksTuple, EventProcessor,
LlmpRestartingEventManager, ProgressReporter,
},
executors::forkserver::{ForkserverExecutor, ForkserverExecutorBuilder},
feedback_and, feedback_or, feedback_or_fast,
feedbacks::{ConstFeedback, CrashFeedback, MaxMapFeedback, TimeFeedback, TimeoutFeedback},
fuzzer::StdFuzzer,
inputs::BytesInput,
mutators::{
scheduled::havoc_mutations, tokens_mutations, AFLppRedQueen, StdScheduledMutator, Tokens,
},
observers::{CanTrack, HitcountsMapObserver, StdMapObserver, TimeObserver},
schedulers::{powersched::PowerSchedule, QueueScheduler, StdWeightedScheduler},
stages::{
mutational::MultiMutationalStage, CalibrationStage, ColorizationStage, IfStage,
StagesTuple, StdMutationalStage, StdPowerMutationalStage, SyncFromDiskStage,
},
state::{
HasCorpus, HasCurrentTestcase, HasExecutions, HasLastReportTime, HasStartTime, StdState,
UsesState,
},
Error, Fuzzer, HasFeedback, HasMetadata, SerdeAny,
};
use libafl_bolts::{
core_affinity::CoreId,
current_nanos, current_time,
fs::get_unique_std_input_file,
ownedref::OwnedRefMut,
rands::StdRand,
shmem::{ShMem, ShMemProvider, StdShMemProvider},
tuples::{tuple_list, Handled, Merge},
AsSliceMut,
};
use libafl_targets::{cmps::AFLppCmpLogMap, AFLppCmpLogObserver, AFLppCmplogTracingStage};
use serde::{Deserialize, Serialize};
use crate::{
afl_stats::AflStatsStage,
corpus::{set_corpus_filepath, set_solution_filepath},
env_parser::AFL_DEFAULT_MAP_SIZE,
feedback::{filepath::CustomFilepathToTestcaseFeedback, seed::SeedFeedback},
mutational_stage::SupportedMutationalStages,
scheduler::SupportedSchedulers,
Opt, AFL_DEFAULT_INPUT_LEN_MAX, AFL_DEFAULT_INPUT_LEN_MIN, SHMEM_ENV_VAR,
};
pub type LibaflFuzzState =
StdState<BytesInput, CachedOnDiskCorpus<BytesInput>, StdRand, OnDiskCorpus<BytesInput>>;
pub fn run_client<EMH, SP>(
state: Option<LibaflFuzzState>,
mut restarting_mgr: CentralizedEventManager<
LlmpRestartingEventManager<(), LibaflFuzzState, SP>,
EMH,
LibaflFuzzState,
SP,
>,
fuzzer_dir: &PathBuf,
core_id: CoreId,
opt: &Opt,
is_main_node: bool,
) -> Result<(), Error>
where
EMH: EventManagerHooksTuple<LibaflFuzzState> + Copy + Clone,
SP: ShMemProvider,
{
// Create the shared memory map for comms with the forkserver
let mut shmem_provider = StdShMemProvider::new().unwrap();
let mut shmem = shmem_provider
.new_shmem(opt.map_size.unwrap_or(AFL_DEFAULT_MAP_SIZE))
.unwrap();
shmem.write_to_env(SHMEM_ENV_VAR).unwrap();
let shmem_buf = shmem.as_slice_mut();
// Create an observation channel to keep track of edges hit.
let edges_observer = unsafe {
HitcountsMapObserver::new(StdMapObserver::new("edges", shmem_buf)).track_indices()
};
// Create a MapFeedback for coverage guided fuzzin'
let map_feedback = MaxMapFeedback::new(&edges_observer);
// Create the CalibrationStage; used to measure the stability of an input.
// We run the stage only if we are NOT doing sequential scheduling.
let calibration = IfStage::new(
|_, _, _, _| Ok(!opt.sequential_queue),
tuple_list!(CalibrationStage::new(&map_feedback)),
);
// Create a AFLStatsStage;
let afl_stats_stage = AflStatsStage::new(opt, fuzzer_dir.clone(), &edges_observer);
// Create an observation channel to keep track of the execution time.
let time_observer = TimeObserver::new("time");
/*
* Feedback to decide if the Input is "corpus worthy"
* We only check if it gives new coverage.
* The `TimeFeedback` is used to annotate the testcase with it's exec time.
* The `CustomFilepathToTestcaseFeedback is used to adhere to AFL++'s corpus format.
* The `Seedfeedback` is used during seed loading to adhere to AFL++'s handling of seeds
*/
let mut feedback = SeedFeedback::new(
feedback_or!(
map_feedback,
TimeFeedback::new(&time_observer),
CustomFilepathToTestcaseFeedback::new(set_corpus_filepath, fuzzer_dir.clone())
),
opt,
);
/*
* Feedback to decide if the Input is "solution worthy".
* We check if it's a crash or a timeout (if we are configured to consider timeouts)
* The `CustomFilepathToTestcaseFeedback is used to adhere to AFL++'s corpus format.
* The `MaxMapFeedback` saves objectives only if they hit new edges
* */
let mut objective = feedback_or!(
feedback_and!(
feedback_or_fast!(
CrashFeedback::new(),
feedback_and!(
ConstFeedback::new(!opt.ignore_timeouts),
TimeoutFeedback::new()
)
),
MaxMapFeedback::with_name("edges_objective", &edges_observer)
),
CustomFilepathToTestcaseFeedback::new(set_solution_filepath, fuzzer_dir.clone())
);
// Initialize our State if necessary
let mut state = state.unwrap_or_else(|| {
StdState::new(
StdRand::with_seed(current_nanos()),
// TODO: configure testcache size
CachedOnDiskCorpus::<BytesInput>::new(fuzzer_dir.join("queue"), 1000).unwrap(),
OnDiskCorpus::<BytesInput>::new(fuzzer_dir.clone()).unwrap(),
&mut feedback,
&mut objective,
)
.unwrap()
});
// Create our Mutational Stage.
// We can either have a simple MutationalStage (for Queue scheduling)
// Or one that utilizes scheduling metadadata (Weighted Random scheduling)
let mutation = StdScheduledMutator::new(havoc_mutations().merge(tokens_mutations()));
let mutational_stage = if opt.sequential_queue {
SupportedMutationalStages::StdMutational(StdMutationalStage::new(mutation), PhantomData)
} else {
SupportedMutationalStages::PowerMutational(
StdPowerMutationalStage::new(mutation),
PhantomData,
)
};
let strategy = opt.power_schedule.unwrap_or(PowerSchedule::EXPLORE);
// Create our ColorizationStage
let colorization = ColorizationStage::new(&edges_observer);
// Create our Scheduler
// Our scheduler can either be a Queue
// Or a "Weighted Random" which prioritizes entries that take less time and hit more edges
let scheduler;
if opt.sequential_queue {
scheduler = SupportedSchedulers::Queue(QueueScheduler::new(), PhantomData);
} else {
let mut weighted_scheduler =
StdWeightedScheduler::with_schedule(&mut state, &edges_observer, Some(strategy));
if opt.cycle_schedules {
weighted_scheduler = weighted_scheduler.cycling_scheduler();
}
// TODO: Go back to IndexesLenTimeMinimizerScheduler once AflScheduler is implemented for it.
scheduler = SupportedSchedulers::Weighted(weighted_scheduler, PhantomData);
}
// Create our Fuzzer
let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective);
// Create the base Executor
let mut executor = base_executor(opt, &mut shmem_provider);
// Set a custom exit code to be interpreted as a Crash if configured.
if let Some(crash_exitcode) = opt.crash_exitcode {
executor = executor.crash_exitcode(crash_exitcode);
}
// Enable autodict if configured
let mut tokens = Tokens::new();
if !opt.no_autodict {
executor = executor.autotokens(&mut tokens);
};
// Set a custom directory for the current Input if configured;
// May be used to provide a ram-disk etc..
if let Some(cur_input_dir) = &opt.cur_input_dir {
if opt.harness_input_type.is_none() {
return Err(Error::illegal_argument(
"cannot use AFL_TMPDIR with stdin input type.",
));
}
executor = executor.arg_input_file(cur_input_dir.join(get_unique_std_input_file()));
}
// Finalize and build our Executor
let mut executor = executor
.build(tuple_list!(time_observer, edges_observer))
.unwrap();
// Load our seeds.
if state.must_load_initial_inputs() {
state
.load_initial_inputs_multicore(
&mut fuzzer,
&mut executor,
&mut restarting_mgr,
&[fuzzer_dir.join("queue")],
&core_id,
opt.cores.as_ref().expect("invariant; should never occur"),
)
.unwrap_or_else(|err| panic!("Failed to load initial corpus! {err:?}"));
println!("We imported {} inputs from disk.", state.corpus().count());
}
// We set IsInitialCorpusEntry as metadata for all initial testcases.
// Used in Cmplog stage if AFL_CMPLOG_ONLY_NEW.
if opt.cmplog_only_new {
for id in state.corpus().ids() {
let testcase = state.corpus().get(id).expect("should be present in Corpus");
testcase
.borrow_mut()
.add_metadata(IsInitialCorpusEntryMetadata {});
}
}
// Add the tokens to State
state.add_metadata(tokens);
// Set the start time of our Fuzzer
*state.start_time_mut() = current_time();
// Tell [`SeedFeedback`] that we're done loading seeds; rendering it benign.
fuzzer.feedback_mut().done_loading_seeds();
// Set LD_PRELOAD (Linux) && DYLD_INSERT_LIBRARIES (OSX) for target.
if let Some(preload_env) = &opt.afl_preload {
std::env::set_var("LD_PRELOAD", preload_env);
std::env::set_var("DYLD_INSERT_LIBRARIES", preload_env);
}
// Create a Sync stage to sync from foreign fuzzers
let sync_stage = IfStage::new(
|_, _, _, _| Ok(is_main_node && !opt.foreign_sync_dirs.is_empty()),
tuple_list!(SyncFromDiskStage::with_from_file(
opt.foreign_sync_dirs.clone(),
opt.foreign_sync_interval
)),
);
// Create a CmpLog executor if configured.
if let Some(ref cmplog_binary) = opt.cmplog_binary {
// The CmpLog map shared between the CmpLog observer and CmpLog executor
let mut cmplog_shmem = shmem_provider.uninit_on_shmem::<AFLppCmpLogMap>().unwrap();
// Let the Forkserver know the CmpLog shared memory map ID.
cmplog_shmem.write_to_env("__AFL_CMPLOG_SHM_ID").unwrap();
let cmpmap = unsafe { OwnedRefMut::from_shmem(&mut cmplog_shmem) };
// Create the CmpLog observer.
let cmplog_observer = AFLppCmpLogObserver::new("cmplog", cmpmap, true);
let cmplog_ref = cmplog_observer.handle();
// Create the CmpLog executor.
// Cmplog has 25% execution overhead so we give it double the timeout
let cmplog_executor = base_executor(opt, &mut shmem_provider)
.timeout(Duration::from_millis(opt.hang_timeout * 2))
.program(cmplog_binary)
.build(tuple_list!(cmplog_observer))
.unwrap();
// Create the CmpLog tracing stage.
let tracing = AFLppCmplogTracingStage::new(cmplog_executor, cmplog_ref);
// Create a randomic Input2State stage
let rq = MultiMutationalStage::new(AFLppRedQueen::with_cmplog_options(true, true));
// Create an IfStage and wrap the CmpLog stages in it.
// We run cmplog on the second fuzz run of the testcase.
// This stage checks if the testcase has been fuzzed more than twice, if so do not run cmplog.
// We also check if it is an initial corpus testcase
// and if run with AFL_CMPLOG_ONLY_NEW, then we avoid cmplog.
let cb = |_fuzzer: &mut _,
_executor: &mut _,
state: &mut LibaflFuzzState,
_event_manager: &mut _|
-> Result<bool, Error> {
let testcase = state.current_testcase()?;
if testcase.scheduled_count() == 1
|| (opt.cmplog_only_new && testcase.has_metadata::<IsInitialCorpusEntryMetadata>())
{
return Ok(false);
}
Ok(true)
};
let cmplog = IfStage::new(cb, tuple_list!(colorization, tracing, rq));
// The order of the stages matter!
let mut stages = tuple_list!(
calibration,
cmplog,
mutational_stage,
afl_stats_stage,
sync_stage
);
// Run our fuzzer; WITH CmpLog
run_fuzzer_with_stages(
opt,
&mut fuzzer,
&mut stages,
&mut executor,
&mut state,
&mut restarting_mgr,
)?;
} else {
// The order of the stages matter!
let mut stages = tuple_list!(calibration, mutational_stage, afl_stats_stage, sync_stage);
// Run our fuzzer; NO CmpLog
run_fuzzer_with_stages(
opt,
&mut fuzzer,
&mut stages,
&mut executor,
&mut state,
&mut restarting_mgr,
)?;
}
Ok(())
// TODO: serialize state when exiting.
}
fn base_executor<'a>(
opt: &'a Opt,
shmem_provider: &'a mut StdShMemProvider,
) -> ForkserverExecutorBuilder<'a, StdShMemProvider> {
let mut executor = ForkserverExecutor::builder()
.program(opt.executable.clone())
.shmem_provider(shmem_provider)
.coverage_map_size(opt.map_size.unwrap_or(AFL_DEFAULT_MAP_SIZE))
.debug_child(opt.debug_child)
.is_persistent(opt.is_persistent)
.is_deferred_frksrv(opt.defer_forkserver)
.min_input_size(opt.min_input_len.unwrap_or(AFL_DEFAULT_INPUT_LEN_MIN))
.max_input_size(opt.max_input_len.unwrap_or(AFL_DEFAULT_INPUT_LEN_MAX))
.timeout(Duration::from_millis(opt.hang_timeout));
if let Some(target_env) = &opt.target_env {
executor = executor.envs(target_env);
}
if let Some(kill_signal) = opt.kill_signal {
executor = executor.kill_signal(kill_signal);
}
if let Some(harness_input_type) = &opt.harness_input_type {
executor = executor.parse_afl_cmdline([harness_input_type]);
}
executor
}
pub fn fuzzer_target_mode(opt: &Opt) -> Cow<'static, str> {
let mut res = String::new();
if opt.unicorn_mode {
res = format!("{res}unicorn ");
}
if opt.qemu_mode {
res = format!("{res}qemu ");
}
if opt.forkserver_cs {
res = format!("{res}coresight ");
}
if opt.no_forkserver {
res = format!("{res}no_fsrv ");
}
if opt.crash_mode {
res = format!("{res}crash ");
}
if opt.is_persistent {
res = format!("{res}persistent ");
}
// TODO: not always shmem_testcase
res = format!("{res}shmem_testcase ");
if opt.defer_forkserver {
res = format!("{res}deferred ");
}
if !(opt.unicorn_mode
|| opt.qemu_mode
|| opt.forkserver_cs
|| opt.non_instrumented_mode
|| opt.no_forkserver
|| opt.crash_mode
|| opt.is_persistent
|| opt.defer_forkserver)
{
res = format!("{res}default");
}
Cow::Owned(res)
}
#[derive(Debug, Serialize, Deserialize, SerdeAny)]
pub struct IsInitialCorpusEntryMetadata {}
pub fn run_fuzzer_with_stages<Z, ST, E, EM>(
opt: &Opt,
fuzzer: &mut Z,
stages: &mut ST,
executor: &mut E,
state: &mut <Z as UsesState>::State,
mgr: &mut EM,
) -> Result<(), Error>
where
Z: Fuzzer<E, EM, ST>,
E: UsesState<State = Z::State>,
EM: ProgressReporter<State = Z::State> + EventProcessor<E, Z>,
ST: StagesTuple<E, EM, Z::State, Z>,
<Z as UsesState>::State: HasLastReportTime + HasExecutions + HasMetadata,
{
if opt.bench_just_one {
fuzzer.fuzz_loop_for(stages, executor, state, mgr, 1)?;
} else {
fuzzer.fuzz_loop(stages, executor, state, mgr)?;
}
Ok(())
}

View File

@ -0,0 +1,38 @@
use libafl::{
events::{Event, EventManagerHook},
state::{State, Stoppable},
Error,
};
use libafl_bolts::ClientId;
#[derive(Clone, Copy)]
pub struct LibAflFuzzEventHook {
exit_on_solution: bool,
}
impl LibAflFuzzEventHook {
pub fn new(exit_on_solution: bool) -> Self {
Self { exit_on_solution }
}
}
impl<S> EventManagerHook<S> for LibAflFuzzEventHook
where
S: State + Stoppable,
{
fn pre_exec(
&mut self,
state: &mut S,
_client_id: ClientId,
event: &Event<S::Input>,
) -> Result<bool, Error> {
if self.exit_on_solution && matches!(event, Event::Objective { .. }) {
// TODO: dump state
state.request_stop();
}
Ok(true)
}
fn post_exec(&mut self, _state: &mut S, _client_id: ClientId) -> Result<bool, Error> {
Ok(true)
}
}

View File

@ -0,0 +1,279 @@
#![deny(clippy::pedantic)]
#![allow(clippy::unsafe_derive_deserialize)]
#![allow(clippy::ptr_arg)]
#![allow(clippy::unnecessary_wraps)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::similar_names)]
#![allow(clippy::too_many_lines)]
#![allow(clippy::struct_excessive_bools)]
use std::{collections::HashMap, path::PathBuf, time::Duration};
mod afl_stats;
mod env_parser;
mod feedback;
mod mutational_stage;
mod scheduler;
use clap::Parser;
use corpus::{check_autoresume, create_dir_if_not_exists, remove_main_node_file};
mod corpus;
mod executor;
mod fuzzer;
mod hooks;
use env_parser::parse_envs;
use fuzzer::run_client;
use libafl::{
events::{CentralizedLauncher, EventConfig},
monitors::MultiMonitor,
schedulers::powersched::PowerSchedule,
Error,
};
use libafl_bolts::{
core_affinity::{CoreId, Cores},
shmem::{ShMemProvider, StdShMemProvider},
};
use nix::sys::signal::Signal;
const AFL_DEFAULT_INPUT_LEN_MAX: usize = 1_048_576;
const AFL_DEFAULT_INPUT_LEN_MIN: usize = 1;
const OUTPUT_GRACE: u64 = 25;
pub const AFL_DEFAULT_BROKER_PORT: u16 = 1337;
const PERSIST_SIG: &str = "##SIG_AFL_PERSISTENT##";
const DEFER_SIG: &str = "##SIG_AFL_DEFER_FORKSRV##";
const SHMEM_ENV_VAR: &str = "__AFL_SHM_ID";
static AFL_HARNESS_FILE_INPUT: &str = "@@";
#[allow(clippy::too_many_lines)]
fn main() {
env_logger::init();
let mut opt = Opt::parse();
parse_envs(&mut opt).expect("invalid configuration");
executor::check_binary(&mut opt, SHMEM_ENV_VAR).expect("binary to be valid");
// Create the shared memory map provider for LLMP
let shmem_provider = StdShMemProvider::new().unwrap();
// Create our Monitor
let monitor = MultiMonitor::new(|s| println!("{s}"));
opt.auto_resume = if opt.auto_resume {
true
} else {
opt.input_dir.as_os_str() == "-"
};
create_dir_if_not_exists(&opt.output_dir).expect("could not create output directory");
// TODO: we need to think about the fuzzer naming scheme since they can be configured in
// different ways (ASAN/mutators) etc.... and how to autoresume appropriately.
// Currently we do AFL style resume with hardcoded names.
// Currently, we will error if we don't find our assigned dir.
// This will also not work if we use core 1-8 and then later, 16-24
// since fuzzer names are using core_ids
match CentralizedLauncher::builder()
.shmem_provider(shmem_provider)
.configuration(EventConfig::from_name("default"))
.monitor(monitor)
.main_run_client(|state: Option<_>, mgr: _, core_id: CoreId| {
println!("run primary client on core {}", core_id.0);
let fuzzer_dir = opt.output_dir.join("fuzzer_main");
check_autoresume(&fuzzer_dir, &opt.input_dir, opt.auto_resume).unwrap();
let res = run_client(state, mgr, &fuzzer_dir, core_id, &opt, true);
let _ = remove_main_node_file(&fuzzer_dir);
res
})
.secondary_run_client(|state: Option<_>, mgr: _, core_id: CoreId| {
println!("run secondary client on core {}", core_id.0);
let fuzzer_dir = opt
.output_dir
.join(format!("fuzzer_secondary_{}", core_id.0));
check_autoresume(&fuzzer_dir, &opt.input_dir, opt.auto_resume).unwrap();
run_client(state, mgr, &fuzzer_dir, core_id, &opt, false)
})
.cores(&opt.cores.clone().expect("invariant; should never occur"))
.broker_port(opt.broker_port.unwrap_or(AFL_DEFAULT_BROKER_PORT))
.build()
.launch()
{
Ok(()) => unreachable!(),
Err(Error::ShuttingDown) => println!("Fuzzing stopped by user. Good bye."),
Err(err) => panic!("Failed to run launcher: {err:?}"),
};
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Parser, Clone)]
#[command(
name = "afl-fuzz",
about = "afl-fuzz, now with LibAFL!",
author = "aarnav <aarnavbos@gmail.com>"
)]
/// The Configuration
struct Opt {
executable: PathBuf,
#[arg(value_parser = validate_harness_input_stdin)]
harness_input_type: Option<&'static str>,
// NOTE: afl-fuzz does not accept multiple input directories
#[arg(short = 'i')]
input_dir: PathBuf,
#[arg(short = 'o')]
output_dir: PathBuf,
#[arg(short = 'p')]
power_schedule: Option<PowerSchedule>,
#[arg(short = 'c')]
cmplog_binary: Option<PathBuf>,
#[arg(short = 'F', num_args = 32)]
foreign_sync_dirs: Vec<PathBuf>,
// Environment + CLI variables
#[arg(short = 'G')]
max_input_len: Option<usize>,
#[arg(short = 'g')]
min_input_len: Option<usize>,
#[arg(short = 'Z')]
sequential_queue: bool,
// TODO: enforce
#[arg(short = 'm')]
memory_limit: Option<usize>,
// TODO: enforce
#[arg(short = 'V')]
fuzz_for_seconds: Option<usize>,
// Environment Variables
#[clap(skip)]
bench_just_one: bool,
#[clap(skip)]
bench_until_crash: bool,
#[clap(skip)]
hang_timeout: u64,
#[clap(skip)]
debug_child: bool,
#[clap(skip)]
is_persistent: bool,
#[clap(skip)]
no_autodict: bool,
#[clap(skip)]
kill_signal: Option<Signal>,
#[clap(skip)]
map_size: Option<usize>,
#[clap(skip)]
ignore_timeouts: bool,
#[clap(skip)]
cur_input_dir: Option<PathBuf>,
#[clap(skip)]
crash_exitcode: Option<i8>,
#[clap(skip)]
target_env: Option<HashMap<String, String>>,
#[clap(skip)]
cycle_schedules: bool,
#[clap(skip)]
cmplog_only_new: bool,
#[clap(skip)]
afl_preload: Option<String>,
#[clap(skip)]
auto_resume: bool,
#[clap(skip)]
skip_bin_check: bool,
#[clap(skip)]
defer_forkserver: bool,
/// in seconds
#[clap(skip)]
stats_interval: u64,
// New Environment Variables
#[clap(skip)]
cores: Option<Cores>,
#[clap(skip)]
broker_port: Option<u16>,
// Seed config
#[clap(skip)]
exit_on_seed_issues: bool,
// renamed from IGNORE_SEED_PROBLEMS
#[clap(skip)]
ignore_seed_issues: bool,
#[clap(skip)]
crash_seed_as_new_crash: bool,
// Cmplog config
// TODO: actually use this config
#[arg(short='l', value_parser=parse_cmplog_args)]
cmplog_opts: Option<CmplogOpts>,
#[clap(skip)]
foreign_sync_interval: Duration,
// TODO:
#[clap(skip)]
frida_persistent_addr: Option<String>,
#[clap(skip)]
qemu_custom_bin: bool,
#[clap(skip)]
cs_custom_bin: bool,
#[clap(skip)]
use_wine: bool,
#[clap(skip)]
uses_asan: bool,
#[clap(skip)]
frida_mode: bool,
#[clap(skip)]
qemu_mode: bool,
#[cfg(target_os = "linux")]
#[clap(skip)]
nyx_mode: bool,
#[clap(skip)]
unicorn_mode: bool,
#[clap(skip)]
forkserver_cs: bool,
#[clap(skip)]
no_forkserver: bool,
#[clap(skip)]
crash_mode: bool,
#[clap(skip)]
non_instrumented_mode: bool,
}
fn validate_harness_input_stdin(s: &str) -> Result<&'static str, String> {
if s != "@@" {
return Err("Unknown harness input type. Use \"@@\" for file, omit for stdin ".to_string());
}
Ok(AFL_HARNESS_FILE_INPUT)
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct CmplogOpts {
file_size: CmplogFileSize,
arith_solving: bool,
transform_solving: bool,
exterme_transform_solving: bool,
random_colorization: bool,
}
#[derive(Debug, Clone)]
pub enum CmplogFileSize {
Small,
Larger,
All,
}
impl From<&str> for CmplogFileSize {
fn from(value: &str) -> Self {
if value.contains('1') {
Self::Small
} else if value.contains('3') {
Self::All
} else {
Self::Larger
}
}
}
fn parse_cmplog_args(s: &str) -> Result<CmplogOpts, String> {
Ok(CmplogOpts {
file_size: s.into(),
arith_solving: s.contains('A'),
transform_solving: s.contains('T'),
exterme_transform_solving: s.contains('X'),
random_colorization: s.contains('R'),
})
}

View File

@ -0,0 +1,116 @@
use std::{borrow::Cow, marker::PhantomData};
use libafl::{
inputs::Input,
mutators::Mutator,
stages::{mutational::MutatedTransform, MutationalStage, Stage},
state::{HasCorpus, HasRand, State, UsesState},
Error, Evaluator, HasNamedMetadata,
};
use libafl_bolts::Named;
#[derive(Debug)]
pub enum SupportedMutationalStages<S, SM, P, E, EM, M, I, Z> {
StdMutational(SM, PhantomData<(S, I, M, EM, Z, E)>),
PowerMutational(P, PhantomData<(S, I, M, EM, Z, E)>),
}
impl<S, SM, P, E, EM, M, I, Z> MutationalStage<E, EM, I, M, Z>
for SupportedMutationalStages<S, SM, P, E, EM, M, I, Z>
where
E: UsesState<State = S>,
EM: UsesState<State = S>,
M: Mutator<I, S>,
Z: Evaluator<E, EM, State = S>,
I: MutatedTransform<S::Input, S> + Clone + Input,
SM: MutationalStage<E, EM, I, M, Z, State = S>,
P: MutationalStage<E, EM, I, M, Z, State = S>,
S: State<Input = I> + HasRand + HasCorpus + HasNamedMetadata,
{
/// The mutator, added to this stage
#[inline]
fn mutator(&self) -> &M {
match self {
Self::StdMutational(m, _) => m.mutator(),
Self::PowerMutational(p, _) => p.mutator(),
}
}
/// The list of mutators, added to this stage (as mutable ref)
#[inline]
fn mutator_mut(&mut self) -> &mut M {
match self {
Self::StdMutational(m, _) => m.mutator_mut(),
Self::PowerMutational(p, _) => p.mutator_mut(),
}
}
/// Gets the number of iterations as a random number
fn iterations(&self, state: &mut S) -> Result<usize, Error> {
match self {
Self::StdMutational(m, _) => m.iterations(state),
Self::PowerMutational(p, _) => p.iterations(state),
}
}
}
impl<S, SM, P, E, EM, M, I, Z> UsesState for SupportedMutationalStages<S, SM, P, E, EM, M, I, Z>
where
S: State + HasRand,
{
type State = S;
}
impl<S, SM, P, E, EM, M, I, Z> Named for SupportedMutationalStages<S, SM, P, E, EM, M, I, Z>
where
SM: Named,
P: Named,
{
fn name(&self) -> &Cow<'static, str> {
match self {
Self::StdMutational(m, _) => m.name(),
Self::PowerMutational(p, _) => p.name(),
}
}
}
impl<S, SM, P, E, EM, M, I, Z> Stage<E, EM, Z> for SupportedMutationalStages<S, SM, P, E, EM, M, I, Z>
where
E: UsesState<State = S>,
EM: UsesState<State = S>,
M: Mutator<I, S>,
Z: Evaluator<E, EM, State = S>,
I: MutatedTransform<S::Input, S> + Clone + Input,
SM: MutationalStage<E, EM, I, M, Z, State = S>,
P: MutationalStage<E, EM, I, M, Z, State = S>,
S: State<Input = I> + HasRand + HasCorpus + HasNamedMetadata,
{
#[inline]
#[allow(clippy::let_and_return)]
fn perform(
&mut self,
fuzzer: &mut Z,
executor: &mut E,
state: &mut S,
manager: &mut EM,
) -> Result<(), Error> {
match self {
Self::StdMutational(m, _) => m.perform(fuzzer, executor, state, manager),
Self::PowerMutational(p, _) => p.perform(fuzzer, executor, state, manager),
}
}
fn should_restart(&mut self, state: &mut S) -> Result<bool, Error> {
match self {
Self::StdMutational(m, _) => m.should_restart(state),
Self::PowerMutational(p, _) => p.should_restart(state),
}
}
fn clear_progress(&mut self, state: &mut S) -> Result<(), Error> {
match self {
Self::StdMutational(m, _) => m.clear_progress(state),
Self::PowerMutational(p, _) => p.clear_progress(state),
}
}
}

View File

@ -0,0 +1,114 @@
use std::marker::PhantomData;
use libafl::{
corpus::{CorpusId, HasTestcase, Testcase},
inputs::UsesInput,
observers::ObserversTuple,
schedulers::{HasQueueCycles, RemovableScheduler, Scheduler},
state::{HasCorpus, HasRand, State, UsesState},
Error, HasMetadata,
};
pub enum SupportedSchedulers<S, Q, W> {
Queue(Q, PhantomData<(S, Q, W)>),
Weighted(W, PhantomData<(S, Q, W)>),
}
impl<S, Q, W> UsesState for SupportedSchedulers<S, Q, W>
where
S: State + HasRand + HasCorpus + HasMetadata + HasTestcase,
{
type State = S;
}
impl<S, Q, W> RemovableScheduler for SupportedSchedulers<S, Q, W>
where
S: UsesInput + HasTestcase + HasMetadata + HasCorpus + HasRand + State,
Q: Scheduler<State = S> + RemovableScheduler,
W: Scheduler<State = S> + RemovableScheduler,
{
fn on_remove(
&mut self,
state: &mut Self::State,
id: CorpusId,
testcase: &Option<Testcase<<Self::State as UsesInput>::Input>>,
) -> Result<(), Error> {
match self {
Self::Queue(queue, _) => queue.on_remove(state, id, testcase),
Self::Weighted(weighted, _) => weighted.on_remove(state, id, testcase),
}
}
fn on_replace(
&mut self,
state: &mut Self::State,
id: CorpusId,
prev: &Testcase<<Self::State as UsesInput>::Input>,
) -> Result<(), Error> {
match self {
Self::Queue(queue, _) => queue.on_replace(state, id, prev),
Self::Weighted(weighted, _) => weighted.on_replace(state, id, prev),
}
}
}
impl<S, Q, W> Scheduler for SupportedSchedulers<S, Q, W>
where
S: UsesInput + HasTestcase + HasMetadata + HasCorpus + HasRand + State,
Q: Scheduler<State = S>,
W: Scheduler<State = S>,
{
fn on_add(&mut self, state: &mut Self::State, id: CorpusId) -> Result<(), Error> {
match self {
Self::Queue(queue, _) => queue.on_add(state, id),
Self::Weighted(weighted, _) => weighted.on_add(state, id),
}
}
/// Gets the next entry in the queue
fn next(&mut self, state: &mut Self::State) -> Result<CorpusId, Error> {
match self {
Self::Queue(queue, _) => queue.next(state),
Self::Weighted(weighted, _) => weighted.next(state),
}
}
fn on_evaluation<OTB>(
&mut self,
state: &mut Self::State,
input: &<Self::State as UsesInput>::Input,
observers: &OTB,
) -> Result<(), Error>
where
OTB: ObserversTuple<Self::State>,
{
match self {
Self::Queue(queue, _) => queue.on_evaluation(state, input, observers),
Self::Weighted(weighted, _) => weighted.on_evaluation(state, input, observers),
}
}
fn set_current_scheduled(
&mut self,
state: &mut Self::State,
next_id: Option<CorpusId>,
) -> Result<(), Error> {
match self {
Self::Queue(queue, _) => queue.set_current_scheduled(state, next_id),
Self::Weighted(weighted, _) => weighted.set_current_scheduled(state, next_id),
}
}
}
impl<S, Q, W> HasQueueCycles for SupportedSchedulers<S, Q, W>
where
S: UsesInput + HasTestcase + HasMetadata + HasCorpus + HasRand + State,
Q: Scheduler<State = S> + HasQueueCycles,
W: Scheduler<State = S> + HasQueueCycles,
{
fn queue_cycles(&self) -> u64 {
match self {
Self::Queue(queue, _) => queue.queue_cycles(),
Self::Weighted(weighted, _) => weighted.queue_cycles(),
}
}
}

View File

@ -0,0 +1 @@
0

View File

@ -0,0 +1 @@
00000000000000000000000000000000

View File

@ -0,0 +1,38 @@
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdarg.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
int LLVMFuzzerTestOneInput(const uint8_t *buf, size_t i) {
if (i < 15) return -1;
if (buf[0] != 'A') return 0;
int *icmp = (int *)(buf + 1);
if (*icmp != 0x69694141) return 0;
if (memcmp(buf + 5, "1234EF", 6) == 0) abort();
return 0;
}
#ifdef __AFL_COMPILER
int main(int argc, char *argv[]) {
unsigned char buf[1024];
ssize_t i;
while (__AFL_LOOP(1000)) {
i = read(0, (char *)buf, sizeof(buf) - 1);
if (i > 0) buf[i] = 0;
LLVMFuzzerTestOneInput(buf, i);
}
return 0;
}
#endif

View File

@ -0,0 +1,83 @@
/*
american fuzzy lop++ - a trivial program to test the build
--------------------------------------------------------
Originally written by Michal Zalewski
Copyright 2014 Google Inc. All rights reserved.
Copyright 2019-2024 AFLplusplus Project. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://www.apache.org/licenses/LICENSE-2.0
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#ifdef TEST_SHARED_OBJECT
#define main main_exported
#endif
int main(int argc, char **argv) {
int fd = 0, cnt;
char buff[8];
char *buf = buff;
// we support command line parameter and stdin
if (argc == 2) {
buf = argv[1];
} else {
if (argc >= 3 && strcmp(argv[1], "-f") == 0) {
if ((fd = open(argv[2], O_RDONLY)) < 0) {
fprintf(stderr, "Error: unable to open %s\n", argv[2]);
exit(-1);
}
}
if ((cnt = read(fd, buf, sizeof(buf) - 1)) < 1) {
printf("Hum?\n");
return 1;
}
buf[cnt] = 0;
}
if (getenv("AFL_DEBUG")) fprintf(stderr, "test-instr: %s\n", buf);
// we support three input cases (plus a 4th if stdin is used but there is no
// input)
switch (buf[0]) {
case '0':
printf("Looks like a zero to me!\n");
break;
case '1':
printf("Pretty sure that is a one!\n");
break;
default:
printf("Neither one or zero? How quaint!\n");
break;
}
return 0;
}

View File

@ -0,0 +1,25 @@
#!/bin/bash
export AFL_DIR_NAME="./AFLplusplus-stable"
export AFL_CC_PATH="$AFL_DIR_NAME/afl-clang-fast"
export LIBAFL_FUZZ_PATH="../target/release/libafl-fuzz"
export LLVM_CONFIG="llvm-config-18"
if [ ! -d "$AFL_DIR_NAME" ]; then
wget https://github.com/AFLplusplus/AFLplusplus/archive/refs/heads/stable.zip
unzip stable.zip
cd $AFL_DIR_NAME
LLVM_CONFIG=$LLVM_CONFIG make
cd ..
fi
cargo build --release
AFL_PATH=$AFL_DIR_NAME $AFL_CC_PATH $AFL_DIR_NAME/test-instr.c -o out-instr
AFL_CORES=1 LLVM_CONFIG=llvm-config-18 AFL_STATS_INTERVAL=1 AFL_NUM_CORES=1 timeout 5 $LIBAFL_FUZZ_PATH -i ./seeds -o ./output $(pwd)/out-instr
test -n "$( ls output/fuzzer_main/queue/id:000002* 2>/dev/null )" || exit 1
test -n "$( ls output/fuzzer_main/fuzzer_stats 2>/dev/null )" || exit 1
test -n "$( ls output/fuzzer_main/plot_data 2>/dev/null )" || exit 1
test -n "$( ls output/fuzzer_main/crashe2s 2>/dev/null )" || exit 1
test -n "$( ls output/fuzzer_main/hangs 2>/dev/null )" || exit 1

View File

@ -195,6 +195,8 @@ serial_test = { version = "3", optional = true, default-features = false, featur
# Document all features of this crate (for `cargo doc`) # Document all features of this crate (for `cargo doc`)
document-features = { version = "0.2", optional = true } document-features = { version = "0.2", optional = true }
# Optional
clap = {version = "4.5", optional = true}
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
libc = "0.2" # For (*nix) libc libc = "0.2" # For (*nix) libc

View File

@ -161,6 +161,15 @@ where
} }
} }
/// Trait for Schedulers which track queue cycles
pub trait HasQueueCycles: Scheduler
where
Self::State: HasCorpus,
{
/// The amount of cycles the scheduler has completed.
fn queue_cycles(&self) -> u64;
}
/// The scheduler define how the fuzzer requests a testcase from the corpus. /// The scheduler define how the fuzzer requests a testcase from the corpus.
/// It has hooks to corpus add/replace/remove to allow complex scheduling algorithms to collect data. /// It has hooks to corpus add/replace/remove to allow complex scheduling algorithms to collect data.
pub trait Scheduler: UsesState pub trait Scheduler: UsesState

View File

@ -13,7 +13,7 @@ use crate::{
corpus::{Corpus, CorpusId, HasTestcase, Testcase}, corpus::{Corpus, CorpusId, HasTestcase, Testcase},
inputs::UsesInput, inputs::UsesInput,
observers::{MapObserver, ObserversTuple}, observers::{MapObserver, ObserversTuple},
schedulers::{AflScheduler, RemovableScheduler, Scheduler}, schedulers::{AflScheduler, HasQueueCycles, RemovableScheduler, Scheduler},
state::{HasCorpus, State, UsesState}, state::{HasCorpus, State, UsesState},
Error, HasMetadata, Error, HasMetadata,
}; };
@ -157,6 +157,7 @@ impl SchedulerMetadata {
/// The power schedule to use /// The power schedule to use
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)] #[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
pub enum PowerSchedule { pub enum PowerSchedule {
/// The `explore` power schedule /// The `explore` power schedule
EXPLORE, EXPLORE,
@ -177,6 +178,7 @@ pub enum PowerSchedule {
/// and here we DON'T actually calculate the power (we do it in the stage) /// and here we DON'T actually calculate the power (we do it in the stage)
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct PowerQueueScheduler<C, O, S> { pub struct PowerQueueScheduler<C, O, S> {
queue_cycles: u64,
strat: PowerSchedule, strat: PowerSchedule,
map_observer_handle: Handle<C>, map_observer_handle: Handle<C>,
last_hash: usize, last_hash: usize,
@ -236,6 +238,17 @@ where
} }
} }
impl<C, O, S> HasQueueCycles for PowerQueueScheduler<C, O, S>
where
S: HasCorpus + HasMetadata + HasTestcase + State,
O: MapObserver,
C: AsRef<O>,
{
fn queue_cycles(&self) -> u64 {
self.queue_cycles
}
}
impl<C, O, S> Scheduler for PowerQueueScheduler<C, O, S> impl<C, O, S> Scheduler for PowerQueueScheduler<C, O, S>
where where
S: HasCorpus + HasMetadata + HasTestcase + State, S: HasCorpus + HasMetadata + HasTestcase + State,
@ -270,8 +283,9 @@ where
if let Some(next) = state.corpus().next(*cur) { if let Some(next) = state.corpus().next(*cur) {
next next
} else { } else {
self.queue_cycles += 1;
let psmeta = state.metadata_mut::<SchedulerMetadata>()?; let psmeta = state.metadata_mut::<SchedulerMetadata>()?;
psmeta.set_queue_cycles(psmeta.queue_cycles() + 1); psmeta.set_queue_cycles(self.queue_cycles());
state.corpus().first().unwrap() state.corpus().first().unwrap()
} }
} }
@ -309,6 +323,7 @@ where
state.add_metadata::<SchedulerMetadata>(SchedulerMetadata::new(Some(strat))); state.add_metadata::<SchedulerMetadata>(SchedulerMetadata::new(Some(strat)));
} }
PowerQueueScheduler { PowerQueueScheduler {
queue_cycles: 0,
strat, strat,
map_observer_handle: map_observer.handle(), map_observer_handle: map_observer.handle(),
last_hash: 0, last_hash: 0,

View File

@ -5,7 +5,7 @@ use core::marker::PhantomData;
use crate::{ use crate::{
corpus::{Corpus, CorpusId, HasTestcase}, corpus::{Corpus, CorpusId, HasTestcase},
schedulers::{RemovableScheduler, Scheduler}, schedulers::{HasQueueCycles, RemovableScheduler, Scheduler},
state::{HasCorpus, State, UsesState}, state::{HasCorpus, State, UsesState},
Error, Error,
}; };
@ -13,6 +13,8 @@ use crate::{
/// Walk the corpus in a queue-like fashion /// Walk the corpus in a queue-like fashion
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct QueueScheduler<S> { pub struct QueueScheduler<S> {
queue_cycles: u64,
runs_in_current_cycle: u64,
phantom: PhantomData<S>, phantom: PhantomData<S>,
} }
@ -55,6 +57,12 @@ where
.map(|id| state.corpus().next(id)) .map(|id| state.corpus().next(id))
.flatten() .flatten()
.unwrap_or_else(|| state.corpus().first().unwrap()); .unwrap_or_else(|| state.corpus().first().unwrap());
self.runs_in_current_cycle += 1;
// TODO deal with corpus_counts decreasing due to removals
if self.runs_in_current_cycle >= state.corpus().count() as u64 {
self.queue_cycles += 1;
}
self.set_current_scheduled(state, Some(id))?; self.set_current_scheduled(state, Some(id))?;
Ok(id) Ok(id)
} }
@ -66,6 +74,8 @@ impl<S> QueueScheduler<S> {
#[must_use] #[must_use]
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
runs_in_current_cycle: 0,
queue_cycles: 0,
phantom: PhantomData, phantom: PhantomData,
} }
} }
@ -77,6 +87,15 @@ impl<S> Default for QueueScheduler<S> {
} }
} }
impl<S> HasQueueCycles for QueueScheduler<S>
where
S: HasCorpus + HasTestcase + State,
{
fn queue_cycles(&self) -> u64 {
self.queue_cycles
}
}
#[cfg(test)] #[cfg(test)]
#[cfg(feature = "std")] #[cfg(feature = "std")]
mod tests { mod tests {

View File

@ -19,7 +19,7 @@ use crate::{
schedulers::{ schedulers::{
powersched::{PowerSchedule, SchedulerMetadata}, powersched::{PowerSchedule, SchedulerMetadata},
testcase_score::{CorpusWeightTestcaseScore, TestcaseScore}, testcase_score::{CorpusWeightTestcaseScore, TestcaseScore},
AflScheduler, RemovableScheduler, Scheduler, AflScheduler, HasQueueCycles, RemovableScheduler, Scheduler,
}, },
state::{HasCorpus, HasRand, State, UsesState}, state::{HasCorpus, HasRand, State, UsesState},
Error, HasMetadata, Error, HasMetadata,
@ -100,6 +100,7 @@ pub struct WeightedScheduler<C, F, O, S> {
strat: Option<PowerSchedule>, strat: Option<PowerSchedule>,
map_observer_handle: Handle<C>, map_observer_handle: Handle<C>,
last_hash: usize, last_hash: usize,
queue_cycles: u64,
phantom: PhantomData<(F, O, S)>, phantom: PhantomData<(F, O, S)>,
/// Cycle `PowerSchedule` on completion of every queue cycle. /// Cycle `PowerSchedule` on completion of every queue cycle.
cycle_schedules: bool, cycle_schedules: bool,
@ -128,6 +129,7 @@ where
strat, strat,
map_observer_handle: map_observer.handle(), map_observer_handle: map_observer.handle(),
last_hash: 0, last_hash: 0,
queue_cycles: 0,
table_invalidated: true, table_invalidated: true,
cycle_schedules: false, cycle_schedules: false,
phantom: PhantomData, phantom: PhantomData,
@ -307,6 +309,18 @@ where
} }
} }
impl<C, F, O, S> HasQueueCycles for WeightedScheduler<C, F, O, S>
where
F: TestcaseScore<S>,
O: MapObserver,
S: HasCorpus + HasMetadata + HasRand + HasTestcase + State,
C: AsRef<O> + Named,
{
fn queue_cycles(&self) -> u64 {
self.queue_cycles
}
}
impl<C, F, O, S> Scheduler for WeightedScheduler<C, F, O, S> impl<C, F, O, S> Scheduler for WeightedScheduler<C, F, O, S>
where where
F: TestcaseScore<S>, F: TestcaseScore<S>,
@ -369,8 +383,9 @@ where
// Update depth // Update depth
if runs_in_current_cycle >= corpus_counts { if runs_in_current_cycle >= corpus_counts {
self.queue_cycles += 1;
let psmeta = state.metadata_mut::<SchedulerMetadata>()?; let psmeta = state.metadata_mut::<SchedulerMetadata>()?;
psmeta.set_queue_cycles(psmeta.queue_cycles() + 1); psmeta.set_queue_cycles(self.queue_cycles());
if self.cycle_schedules { if self.cycle_schedules {
self.cycle_schedule(psmeta)?; self.cycle_schedule(psmeta)?;
} }

View File

@ -1,7 +1,7 @@
//! The [`SyncFromDiskStage`] is a stage that imports inputs from disk for e.g. sync with AFL //! The [`SyncFromDiskStage`] is a stage that imports inputs from disk for e.g. sync with AFL
use alloc::borrow::{Cow, ToOwned}; use alloc::borrow::{Cow, ToOwned};
use core::marker::PhantomData; use core::{marker::PhantomData, time::Duration};
use std::{ use std::{
fs, fs,
path::{Path, PathBuf}, path::{Path, PathBuf},
@ -33,7 +33,7 @@ use crate::{
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct SyncFromDiskMetadata { pub struct SyncFromDiskMetadata {
/// The last time the sync was done /// The last time the sync was done
pub last_time: SystemTime, pub last_time: Duration,
/// The paths that are left to sync /// The paths that are left to sync
pub left_to_sync: Vec<PathBuf>, pub left_to_sync: Vec<PathBuf>,
} }
@ -43,7 +43,7 @@ libafl_bolts::impl_serdeany!(SyncFromDiskMetadata);
impl SyncFromDiskMetadata { impl SyncFromDiskMetadata {
/// Create a new [`struct@SyncFromDiskMetadata`] /// Create a new [`struct@SyncFromDiskMetadata`]
#[must_use] #[must_use]
pub fn new(last_time: SystemTime, left_to_sync: Vec<PathBuf>) -> Self { pub fn new(last_time: Duration, left_to_sync: Vec<PathBuf>) -> Self {
Self { Self {
last_time, last_time,
left_to_sync, left_to_sync,
@ -58,8 +58,9 @@ pub const SYNC_FROM_DISK_STAGE_NAME: &str = "sync";
#[derive(Debug)] #[derive(Debug)]
pub struct SyncFromDiskStage<CB, E, EM, Z> { pub struct SyncFromDiskStage<CB, E, EM, Z> {
name: Cow<'static, str>, name: Cow<'static, str>,
sync_dir: PathBuf, sync_dirs: Vec<PathBuf>,
load_callback: CB, load_callback: CB,
interval: Duration,
phantom: PhantomData<(E, EM, Z)>, phantom: PhantomData<(E, EM, Z)>,
} }
@ -92,54 +93,51 @@ where
state: &mut Self::State, state: &mut Self::State,
manager: &mut EM, manager: &mut EM,
) -> Result<(), Error> { ) -> Result<(), Error> {
log::debug!("Syncing from disk: {:?}", self.sync_dir);
let last = state let last = state
.metadata_map() .metadata_map()
.get::<SyncFromDiskMetadata>() .get::<SyncFromDiskMetadata>()
.map(|m| m.last_time); .map(|m| m.last_time);
if let (Some(max_time), mut new_files) = self.load_from_directory(None, &last)? { if let Some(last) = last {
if last.is_none() { if (last + self.interval) < current_time() {
state return Ok(());
.metadata_map_mut()
.insert(SyncFromDiskMetadata::new(max_time, new_files));
} else {
state
.metadata_map_mut()
.get_mut::<SyncFromDiskMetadata>()
.unwrap()
.last_time = max_time;
state
.metadata_map_mut()
.get_mut::<SyncFromDiskMetadata>()
.unwrap()
.left_to_sync
.append(&mut new_files);
} }
} }
if let Some(sync_from_disk_metadata) = let max_time = match last {
state.metadata_map_mut().get_mut::<SyncFromDiskMetadata>() None => None,
{ Some(last) => Some(last + self.interval),
// Iterate over the paths of files left to sync. };
// By keeping track of these files, we ensure that no file is missed during synchronization, let new_max_time = max_time.unwrap_or(current_time());
// even in the event of a target restart.
let to_sync = sync_from_disk_metadata.left_to_sync.clone(); let mut new_files = vec![];
log::debug!("Number of files to sync: {:?}", to_sync.len()); for dir in &self.sync_dirs {
for path in to_sync { log::debug!("Syncing from dir: {:?}", dir);
let input = (self.load_callback)(fuzzer, state, &path)?; let new_dir_files = self.load_from_directory(dir, &max_time)?;
// Removing each path from the `left_to_sync` Vec before evaluating new_files.extend(new_dir_files);
// prevents duplicate processing and ensures that each file is evaluated only once. This approach helps }
// avoid potential infinite loops that may occur if a file is an objective. *state.metadata_mut::<SyncFromDiskMetadata>().unwrap() = SyncFromDiskMetadata {
state last_time: new_max_time,
.metadata_map_mut() left_to_sync: new_files,
.get_mut::<SyncFromDiskMetadata>() };
.unwrap() let sync_from_disk_metadata = state.metadata_mut::<SyncFromDiskMetadata>().unwrap();
.left_to_sync // Iterate over the paths of files left to sync.
.retain(|p| p != &path); // By keeping track of these files, we ensure that no file is missed during synchronization,
log::debug!("Evaluating: {:?}", path); // even in the event of a target restart.
fuzzer.evaluate_input(state, executor, manager, input)?; let to_sync = sync_from_disk_metadata.left_to_sync.clone();
} log::debug!("Number of files to sync: {:?}", to_sync.len());
for path in to_sync {
let input = (self.load_callback)(fuzzer, state, &path)?;
// Removing each path from the `left_to_sync` Vec before evaluating
// prevents duplicate processing and ensures that each file is evaluated only once. This approach helps
// avoid potential infinite loops that may occur if a file is an objective.
state
.metadata_mut::<SyncFromDiskMetadata>()
.unwrap()
.left_to_sync
.retain(|p| p != &path);
log::debug!("Evaluating: {:?}", path);
fuzzer.evaluate_input(state, executor, manager, input)?;
} }
#[cfg(feature = "introspection")] #[cfg(feature = "introspection")]
@ -164,28 +162,24 @@ where
impl<CB, E, EM, Z> SyncFromDiskStage<CB, E, EM, Z> { impl<CB, E, EM, Z> SyncFromDiskStage<CB, E, EM, Z> {
/// Creates a new [`SyncFromDiskStage`] /// Creates a new [`SyncFromDiskStage`]
#[must_use] #[must_use]
pub fn new(sync_dir: PathBuf, load_callback: CB, name: &str) -> Self { pub fn new(sync_dirs: Vec<PathBuf>, load_callback: CB, interval: Duration, name: &str) -> Self {
Self { Self {
name: Cow::Owned(SYNC_FROM_DISK_STAGE_NAME.to_owned() + ":" + name), name: Cow::Owned(SYNC_FROM_DISK_STAGE_NAME.to_owned() + ":" + name),
phantom: PhantomData, phantom: PhantomData,
sync_dir, sync_dirs,
interval,
load_callback, load_callback,
} }
} }
#[allow(clippy::only_used_in_recursion)]
fn load_from_directory( fn load_from_directory(
&self, &self,
path: Option<PathBuf>, path: &PathBuf,
last: &Option<SystemTime>, last: &Option<Duration>,
) -> Result<(Option<SystemTime>, Vec<PathBuf>), Error> { ) -> Result<Vec<PathBuf>, Error> {
let mut max_time = None;
let mut left_to_sync = Vec::<PathBuf>::new(); let mut left_to_sync = Vec::<PathBuf>::new();
let in_dir = match path { for entry in fs::read_dir(path)? {
Some(p) => p,
None => self.sync_dir.clone(),
};
for entry in fs::read_dir(in_dir)? {
let entry = entry?; let entry = entry?;
let path = entry.path(); let path = entry.path();
let attributes = fs::metadata(&path); let attributes = fs::metadata(&path);
@ -198,26 +192,21 @@ impl<CB, E, EM, Z> SyncFromDiskStage<CB, E, EM, Z> {
if attr.is_file() && attr.len() > 0 { if attr.is_file() && attr.len() > 0 {
if let Ok(time) = attr.modified() { if let Ok(time) = attr.modified() {
if let Some(l) = last { if let Some(last) = last {
if time.duration_since(*l).is_err() || time == *l { if time.duration_since(SystemTime::UNIX_EPOCH).unwrap() < *last {
continue; continue;
} }
} }
max_time = Some(max_time.map_or(time, |t: SystemTime| t.max(time)));
log::info!("Syncing file: {:?}", path); log::info!("Syncing file: {:?}", path);
left_to_sync.push(path.clone()); left_to_sync.push(path.clone());
} }
} else if attr.is_dir() { } else if attr.is_dir() {
let (dir_max_time, dir_left_to_sync) = let dir_left_to_sync = self.load_from_directory(&entry.path(), last)?;
self.load_from_directory(Some(entry.path()), last)?;
if let Some(time) = dir_max_time {
max_time = Some(max_time.map_or(time, |t: SystemTime| t.max(time)));
}
left_to_sync.extend(dir_left_to_sync); left_to_sync.extend(dir_left_to_sync);
} }
} }
Ok((max_time, left_to_sync)) Ok(left_to_sync)
} }
} }
@ -233,7 +222,7 @@ where
{ {
/// Creates a new [`SyncFromDiskStage`] invoking `Input::from_file` to load inputs /// Creates a new [`SyncFromDiskStage`] invoking `Input::from_file` to load inputs
#[must_use] #[must_use]
pub fn with_from_file(sync_dir: PathBuf) -> Self { pub fn with_from_file(sync_dirs: Vec<PathBuf>, interval: Duration) -> Self {
fn load_callback<S: UsesInput, Z>( fn load_callback<S: UsesInput, Z>(
_: &mut Z, _: &mut Z,
_: &mut S, _: &mut S,
@ -242,8 +231,9 @@ where
Input::from_file(p) Input::from_file(p)
} }
Self { Self {
interval,
name: Cow::Borrowed(SYNC_FROM_DISK_STAGE_NAME), name: Cow::Borrowed(SYNC_FROM_DISK_STAGE_NAME),
sync_dir, sync_dirs,
load_callback: load_callback::<_, _>, load_callback: load_callback::<_, _>,
phantom: PhantomData, phantom: PhantomData,
} }

View File

@ -115,6 +115,28 @@ pub fn dup(fd: RawFd) -> Result<RawFd, Error> {
} }
} }
// Derived from https://github.com/RustPython/RustPython/blob/7996a10116681e9f85eda03413d5011b805e577f/stdlib/src/resource.rs#L113
// LICENSE: MIT https://github.com/RustPython/RustPython/commit/37355d612a451fba7fef8f13a1b9fdd51310b37e
/// Get the peak rss (Resident Set Size) of the all child processes
/// that have terminated and been waited for
#[cfg(all(unix, feature = "std"))]
pub fn peak_rss_mb_child_processes() -> Result<i64, Error> {
use core::mem;
use std::io;
use libc::{rusage, RUSAGE_CHILDREN};
let rss = unsafe {
let mut rusage = mem::MaybeUninit::<rusage>::uninit();
if libc::getrusage(RUSAGE_CHILDREN, rusage.as_mut_ptr()) == -1 {
Err(io::Error::last_os_error())
} else {
Ok(rusage.assume_init())
}
}?;
Ok(rss.ru_maxrss >> 10)
}
/// "Safe" wrapper around dup2 /// "Safe" wrapper around dup2
/// ///
/// # Safety /// # Safety