diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index d498fa965e..ee96174f00 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -336,6 +336,7 @@ jobs: - ./fuzzers/backtrace_baby_fuzzers/forkserver_executor - ./fuzzers/backtrace_baby_fuzzers/c_code_with_inprocess_executor - ./fuzzers/backtrace_baby_fuzzers/rust_code_with_fork_executor + - ./fuzzers/libafl-fuzz runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 diff --git a/fuzzers/libafl-fuzz/Cargo.toml b/fuzzers/libafl-fuzz/Cargo.toml new file mode 100644 index 0000000000..a39745e561 --- /dev/null +++ b/fuzzers/libafl-fuzz/Cargo.toml @@ -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"] diff --git a/fuzzers/libafl-fuzz/Makefile.toml b/fuzzers/libafl-fuzz/Makefile.toml new file mode 100644 index 0000000000..eef2b1d66b --- /dev/null +++ b/fuzzers/libafl-fuzz/Makefile.toml @@ -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 +''' diff --git a/fuzzers/libafl-fuzz/README.md b/fuzzers/libafl-fuzz/README.md new file mode 100644 index 0000000000..4e1b3e3109 --- /dev/null +++ b/fuzzers/libafl-fuzz/README.md @@ -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 diff --git a/fuzzers/libafl-fuzz/src/afl_stats.rs b/fuzzers/libafl-fuzz/src/afl_stats.rs new file mode 100644 index 0000000000..988246586a --- /dev/null +++ b/fuzzers/libafl-fuzz/src/afl_stats.rs @@ -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 { + map_observer_handle: Handle, + 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 UsesState for AflStatsStage +where + E: UsesState, + EM: EventFirer, + Z: UsesState, +{ + type State = E::State; +} + +impl Stage for AflStatsStage +where + E: UsesState + HasObservers, + EM: EventFirer, + Z: UsesState + HasScheduler, + E::State: HasImported + + HasCorpus + + HasMetadata + + HasStartTime + + HasExecutions + + HasNamedMetadata + + Stoppable + + HasTestcase, + O: MapObserver, + C: AsRef + Named, + ::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::() + .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 { + Ok(true) + } + fn clear_progress(&mut self, _state: &mut Self::State) -> Result<(), Error> { + Ok(()) + } +} + +impl AflStatsStage +where + E: UsesState + HasObservers, + EM: EventFirer, + Z: UsesState, + E::State: HasImported + HasCorpus + HasMetadata + HasExecutions, + C: AsRef + 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<<::State as UsesInput>::Input>, + ) { + if testcase.has_metadata::() { + self.is_favored_size += 1; + } + } + + fn maybe_update_slowest_exec( + &mut self, + testcase: &Testcase<<::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<<::State as UsesInput>::Input>, + ) -> Result<(), Error> { + if let Ok(metadata) = testcase.metadata::() { + 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<<::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<<::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 = std::env::args().collect(); + Cow::Owned(args.join(" ")) +} diff --git a/fuzzers/libafl-fuzz/src/corpus.rs b/fuzzers/libafl-fuzz/src/corpus.rs new file mode 100644 index 0000000000..d6a862814d --- /dev/null +++ b/fuzzers/libafl-fuzz/src/corpus.rs @@ -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, + _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, + 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 { + 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, 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!")) +} diff --git a/fuzzers/libafl-fuzz/src/env_parser.rs b/fuzzers/libafl-fuzz/src/env_parser.rs new file mode 100644 index 0000000000..684b377176 --- /dev/null +++ b/fuzzers/libafl-fuzz/src/env_parser.rs @@ -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::()? * 60); + } else { + opt.foreign_sync_interval = Duration::from_secs(AFL_DEFAULT_FOREIGN_SYNC_INTERVAL); + } + Ok(()) +} + +fn parse_bool(val: &str) -> Result { + 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>, 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 { + 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; diff --git a/fuzzers/libafl-fuzz/src/executor.rs b/fuzzers/libafl-fuzz/src/executor.rs new file mode 100644 index 0000000000..725a8861a2 --- /dev/null +++ b/fuzzers/libafl-fuzz/src/executor.rs @@ -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 { + 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 + } + }) + }) +} diff --git a/fuzzers/libafl-fuzz/src/feedback/filepath.rs b/fuzzers/libafl-fuzz/src/feedback/filepath.rs new file mode 100644 index 0000000000..4e2326571c --- /dev/null +++ b/fuzzers/libafl-fuzz/src/feedback/filepath.rs @@ -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 +where + I: Input, + S: State, + F: FnMut(&mut S, &mut Testcase, &PathBuf) -> Result<(), Error>, +{ + /// Closure that returns the filename. + func: F, + /// The root output directory + out_dir: PathBuf, + phantomm: PhantomData<(I, S)>, +} + +impl CustomFilepathToTestcaseFeedback +where + I: Input, + S: State, + F: FnMut(&mut S, &mut Testcase, &PathBuf) -> Result<(), Error>, +{ + /// Create a new [`CustomFilepathToTestcaseFeedback`]. + pub fn new(func: F, out_dir: PathBuf) -> Self { + Self { + func, + out_dir, + phantomm: PhantomData, + } + } +} + +impl FeedbackFactory, T> + for CustomFilepathToTestcaseFeedback +where + I: Input, + S: State, + F: FnMut(&mut S, &mut Testcase, &PathBuf) -> Result<(), Error> + Clone, +{ + fn create_feedback(&self, _ctx: &T) -> CustomFilepathToTestcaseFeedback { + Self { + func: self.func.clone(), + phantomm: self.phantomm, + out_dir: self.out_dir.clone(), + } + } +} + +impl Named for CustomFilepathToTestcaseFeedback +where + I: Input, + S: State, + F: FnMut(&mut S, &mut Testcase, &PathBuf) -> Result<(), Error>, +{ + fn name(&self) -> &Cow<'static, str> { + static NAME: Cow<'static, str> = Cow::Borrowed("CustomFilepathToTestcaseFeedback"); + &NAME + } +} + +impl Debug for CustomFilepathToTestcaseFeedback +where + I: Input, + S: State, + F: FnMut(&mut S, &mut Testcase, &PathBuf) -> Result<(), Error>, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CustomFilepathToTestcaseFeedback") + .finish_non_exhaustive() + } +} + +impl Feedback for CustomFilepathToTestcaseFeedback +where + S: State, + F: FnMut(&mut S, &mut Testcase, &PathBuf) -> Result<(), Error>, + I: Input, +{ + #[allow(clippy::wrong_self_convention)] + #[inline] + fn is_interesting( + &mut self, + _state: &mut S, + _manager: &mut EM, + _input: &I, + _observers: &OT, + _exit_kind: &ExitKind, + ) -> Result + where + EM: EventFirer, + { + Ok(false) + } + + fn append_metadata( + &mut self, + state: &mut S, + _manager: &mut EM, + _observers: &OT, + testcase: &mut Testcase<::Input>, + ) -> Result<(), Error> + where + OT: ObserversTuple, + EM: EventFirer, + { + (self.func)(state, testcase, &self.out_dir)?; + Ok(()) + } + + #[cfg(feature = "track_hit_feedbacks")] + #[inline] + fn last_result(&self) -> Result { + Ok(false) + } +} diff --git a/fuzzers/libafl-fuzz/src/feedback/mod.rs b/fuzzers/libafl-fuzz/src/feedback/mod.rs new file mode 100644 index 0000000000..a7f4209910 --- /dev/null +++ b/fuzzers/libafl-fuzz/src/feedback/mod.rs @@ -0,0 +1,2 @@ +pub mod filepath; +pub mod seed; diff --git a/fuzzers/libafl-fuzz/src/feedback/seed.rs b/fuzzers/libafl-fuzz/src/feedback/seed.rs new file mode 100644 index 0000000000..6fd22d4b55 --- /dev/null +++ b/fuzzers/libafl-fuzz/src/feedback/seed.rs @@ -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 +where + A: Feedback, + S: State, +{ + /// Inner [`Feedback`] + pub inner: A, + ignore_timeouts: bool, + ignore_seed_issues: bool, + exit_on_seed_issues: bool, + phantom: PhantomData, + done_loading_seeds: bool, +} +impl SeedFeedback +where + A: Feedback, + 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 Feedback for SeedFeedback +where + A: Feedback, + S: State, +{ + fn init_state(&mut self, state: &mut S) -> Result<(), Error> { + self.inner.init_state(state)?; + Ok(()) + } + fn is_interesting( + &mut self, + state: &mut S, + manager: &mut EM, + input: &S::Input, + observers: &OT, + exit_kind: &ExitKind, + ) -> Result + where + EM: EventFirer, + OT: ObserversTuple, + { + 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( + &mut self, + state: &mut S, + manager: &mut EM, + observers: &OT, + testcase: &mut Testcase, + ) -> Result<(), Error> + where + OT: ObserversTuple, + EM: EventFirer, + { + 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 { + self.inner.last_result() + } + #[cfg(feature = "track_hit_feedbacks")] + fn append_hit_feedbacks(&self, list: &mut Vec>) -> Result<(), Error> { + if self.inner.last_result()? { + self.inner.append_hit_feedbacks(list)?; + } + Ok(()) + } +} + +impl Named for SeedFeedback +where + A: Feedback, + S: State, +{ + #[inline] + fn name(&self) -> &Cow<'static, str> { + static NAME: Cow<'static, str> = Cow::Borrowed("SeedFeedback"); + &NAME + } +} + +impl SeedFeedback +where + A: Feedback, + S: State, +{ + pub fn done_loading_seeds(&mut self) { + self.done_loading_seeds = true; + } +} diff --git a/fuzzers/libafl-fuzz/src/fuzzer.rs b/fuzzers/libafl-fuzz/src/fuzzer.rs new file mode 100644 index 0000000000..2945b34aab --- /dev/null +++ b/fuzzers/libafl-fuzz/src/fuzzer.rs @@ -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, StdRand, OnDiskCorpus>; + +pub fn run_client( + state: Option, + 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 + 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::::new(fuzzer_dir.join("queue"), 1000).unwrap(), + OnDiskCorpus::::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::().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 { + let testcase = state.current_testcase()?; + if testcase.scheduled_count() == 1 + || (opt.cmplog_only_new && testcase.has_metadata::()) + { + 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( + opt: &Opt, + fuzzer: &mut Z, + stages: &mut ST, + executor: &mut E, + state: &mut ::State, + mgr: &mut EM, +) -> Result<(), Error> +where + Z: Fuzzer, + E: UsesState, + EM: ProgressReporter + EventProcessor, + ST: StagesTuple, + ::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(()) +} diff --git a/fuzzers/libafl-fuzz/src/hooks.rs b/fuzzers/libafl-fuzz/src/hooks.rs new file mode 100644 index 0000000000..27b6183bc2 --- /dev/null +++ b/fuzzers/libafl-fuzz/src/hooks.rs @@ -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 EventManagerHook for LibAflFuzzEventHook +where + S: State + Stoppable, +{ + fn pre_exec( + &mut self, + state: &mut S, + _client_id: ClientId, + event: &Event, + ) -> Result { + 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 { + Ok(true) + } +} diff --git a/fuzzers/libafl-fuzz/src/main.rs b/fuzzers/libafl-fuzz/src/main.rs new file mode 100644 index 0000000000..f9e63b039b --- /dev/null +++ b/fuzzers/libafl-fuzz/src/main.rs @@ -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 " +)] +/// 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, + #[arg(short = 'c')] + cmplog_binary: Option, + #[arg(short = 'F', num_args = 32)] + foreign_sync_dirs: Vec, + // Environment + CLI variables + #[arg(short = 'G')] + max_input_len: Option, + #[arg(short = 'g')] + min_input_len: Option, + #[arg(short = 'Z')] + sequential_queue: bool, + // TODO: enforce + #[arg(short = 'm')] + memory_limit: Option, + // TODO: enforce + #[arg(short = 'V')] + fuzz_for_seconds: Option, + // 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, + #[clap(skip)] + map_size: Option, + #[clap(skip)] + ignore_timeouts: bool, + #[clap(skip)] + cur_input_dir: Option, + #[clap(skip)] + crash_exitcode: Option, + #[clap(skip)] + target_env: Option>, + #[clap(skip)] + cycle_schedules: bool, + #[clap(skip)] + cmplog_only_new: bool, + #[clap(skip)] + afl_preload: Option, + #[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, + #[clap(skip)] + broker_port: Option, + + // 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, + + #[clap(skip)] + foreign_sync_interval: Duration, + + // TODO: + #[clap(skip)] + frida_persistent_addr: Option, + #[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 { + 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'), + }) +} diff --git a/fuzzers/libafl-fuzz/src/mutational_stage.rs b/fuzzers/libafl-fuzz/src/mutational_stage.rs new file mode 100644 index 0000000000..cdd0af9730 --- /dev/null +++ b/fuzzers/libafl-fuzz/src/mutational_stage.rs @@ -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 { + StdMutational(SM, PhantomData<(S, I, M, EM, Z, E)>), + PowerMutational(P, PhantomData<(S, I, M, EM, Z, E)>), +} + +impl MutationalStage + for SupportedMutationalStages +where + E: UsesState, + EM: UsesState, + M: Mutator, + Z: Evaluator, + I: MutatedTransform + Clone + Input, + SM: MutationalStage, + P: MutationalStage, + S: State + 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 { + match self { + Self::StdMutational(m, _) => m.iterations(state), + Self::PowerMutational(p, _) => p.iterations(state), + } + } +} + +impl UsesState for SupportedMutationalStages +where + S: State + HasRand, +{ + type State = S; +} + +impl Named for SupportedMutationalStages +where + SM: Named, + P: Named, +{ + fn name(&self) -> &Cow<'static, str> { + match self { + Self::StdMutational(m, _) => m.name(), + Self::PowerMutational(p, _) => p.name(), + } + } +} + +impl Stage for SupportedMutationalStages +where + E: UsesState, + EM: UsesState, + M: Mutator, + Z: Evaluator, + I: MutatedTransform + Clone + Input, + SM: MutationalStage, + P: MutationalStage, + S: State + 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 { + 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), + } + } +} diff --git a/fuzzers/libafl-fuzz/src/scheduler.rs b/fuzzers/libafl-fuzz/src/scheduler.rs new file mode 100644 index 0000000000..375016dacc --- /dev/null +++ b/fuzzers/libafl-fuzz/src/scheduler.rs @@ -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 { + Queue(Q, PhantomData<(S, Q, W)>), + Weighted(W, PhantomData<(S, Q, W)>), +} + +impl UsesState for SupportedSchedulers +where + S: State + HasRand + HasCorpus + HasMetadata + HasTestcase, +{ + type State = S; +} + +impl RemovableScheduler for SupportedSchedulers +where + S: UsesInput + HasTestcase + HasMetadata + HasCorpus + HasRand + State, + Q: Scheduler + RemovableScheduler, + W: Scheduler + RemovableScheduler, +{ + fn on_remove( + &mut self, + state: &mut Self::State, + id: CorpusId, + testcase: &Option::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<::Input>, + ) -> Result<(), Error> { + match self { + Self::Queue(queue, _) => queue.on_replace(state, id, prev), + Self::Weighted(weighted, _) => weighted.on_replace(state, id, prev), + } + } +} + +impl Scheduler for SupportedSchedulers +where + S: UsesInput + HasTestcase + HasMetadata + HasCorpus + HasRand + State, + Q: Scheduler, + W: Scheduler, +{ + 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 { + match self { + Self::Queue(queue, _) => queue.next(state), + Self::Weighted(weighted, _) => weighted.next(state), + } + } + fn on_evaluation( + &mut self, + state: &mut Self::State, + input: &::Input, + observers: &OTB, + ) -> Result<(), Error> + where + OTB: ObserversTuple, + { + 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, + ) -> 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 HasQueueCycles for SupportedSchedulers +where + S: UsesInput + HasTestcase + HasMetadata + HasCorpus + HasRand + State, + Q: Scheduler + HasQueueCycles, + W: Scheduler + HasQueueCycles, +{ + fn queue_cycles(&self) -> u64 { + match self { + Self::Queue(queue, _) => queue.queue_cycles(), + Self::Weighted(weighted, _) => weighted.queue_cycles(), + } + } +} diff --git a/fuzzers/libafl-fuzz/test/seeds/init b/fuzzers/libafl-fuzz/test/seeds/init new file mode 100644 index 0000000000..573541ac97 --- /dev/null +++ b/fuzzers/libafl-fuzz/test/seeds/init @@ -0,0 +1 @@ +0 diff --git a/fuzzers/libafl-fuzz/test/seeds_cmplog/init b/fuzzers/libafl-fuzz/test/seeds_cmplog/init new file mode 100644 index 0000000000..a965e715f7 --- /dev/null +++ b/fuzzers/libafl-fuzz/test/seeds_cmplog/init @@ -0,0 +1 @@ +00000000000000000000000000000000 diff --git a/fuzzers/libafl-fuzz/test/test-cmplog.c b/fuzzers/libafl-fuzz/test/test-cmplog.c new file mode 100644 index 0000000000..0c91b21c45 --- /dev/null +++ b/fuzzers/libafl-fuzz/test/test-cmplog.c @@ -0,0 +1,38 @@ +#include +#include +#include +#include +#include +#include +#include + +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 + diff --git a/fuzzers/libafl-fuzz/test/test-instr.c b/fuzzers/libafl-fuzz/test/test-instr.c new file mode 100644 index 0000000000..285528932c --- /dev/null +++ b/fuzzers/libafl-fuzz/test/test-instr.c @@ -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 +#include +#include +#include +#include +#include +#include + +#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; + +} + diff --git a/fuzzers/libafl-fuzz/test/test.sh b/fuzzers/libafl-fuzz/test/test.sh new file mode 100755 index 0000000000..ecc982c166 --- /dev/null +++ b/fuzzers/libafl-fuzz/test/test.sh @@ -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 diff --git a/libafl/Cargo.toml b/libafl/Cargo.toml index ff97febc47..b5f4477d08 100644 --- a/libafl/Cargo.toml +++ b/libafl/Cargo.toml @@ -195,6 +195,8 @@ serial_test = { version = "3", optional = true, default-features = false, featur # Document all features of this crate (for `cargo doc`) document-features = { version = "0.2", optional = true } +# Optional +clap = {version = "4.5", optional = true} [target.'cfg(unix)'.dependencies] libc = "0.2" # For (*nix) libc diff --git a/libafl/src/schedulers/mod.rs b/libafl/src/schedulers/mod.rs index 11d27f7213..d501714dd3 100644 --- a/libafl/src/schedulers/mod.rs +++ b/libafl/src/schedulers/mod.rs @@ -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. /// It has hooks to corpus add/replace/remove to allow complex scheduling algorithms to collect data. pub trait Scheduler: UsesState diff --git a/libafl/src/schedulers/powersched.rs b/libafl/src/schedulers/powersched.rs index 7313b6a9fb..02f6439469 100644 --- a/libafl/src/schedulers/powersched.rs +++ b/libafl/src/schedulers/powersched.rs @@ -13,7 +13,7 @@ use crate::{ corpus::{Corpus, CorpusId, HasTestcase, Testcase}, inputs::UsesInput, observers::{MapObserver, ObserversTuple}, - schedulers::{AflScheduler, RemovableScheduler, Scheduler}, + schedulers::{AflScheduler, HasQueueCycles, RemovableScheduler, Scheduler}, state::{HasCorpus, State, UsesState}, Error, HasMetadata, }; @@ -157,6 +157,7 @@ impl SchedulerMetadata { /// The power schedule to use #[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] pub enum PowerSchedule { /// The `explore` power schedule EXPLORE, @@ -177,6 +178,7 @@ pub enum PowerSchedule { /// and here we DON'T actually calculate the power (we do it in the stage) #[derive(Clone, Debug)] pub struct PowerQueueScheduler { + queue_cycles: u64, strat: PowerSchedule, map_observer_handle: Handle, last_hash: usize, @@ -236,6 +238,17 @@ where } } +impl HasQueueCycles for PowerQueueScheduler +where + S: HasCorpus + HasMetadata + HasTestcase + State, + O: MapObserver, + C: AsRef, +{ + fn queue_cycles(&self) -> u64 { + self.queue_cycles + } +} + impl Scheduler for PowerQueueScheduler where S: HasCorpus + HasMetadata + HasTestcase + State, @@ -270,8 +283,9 @@ where if let Some(next) = state.corpus().next(*cur) { next } else { + self.queue_cycles += 1; let psmeta = state.metadata_mut::()?; - psmeta.set_queue_cycles(psmeta.queue_cycles() + 1); + psmeta.set_queue_cycles(self.queue_cycles()); state.corpus().first().unwrap() } } @@ -309,6 +323,7 @@ where state.add_metadata::(SchedulerMetadata::new(Some(strat))); } PowerQueueScheduler { + queue_cycles: 0, strat, map_observer_handle: map_observer.handle(), last_hash: 0, diff --git a/libafl/src/schedulers/queue.rs b/libafl/src/schedulers/queue.rs index f78bec50d8..ad0bad693c 100644 --- a/libafl/src/schedulers/queue.rs +++ b/libafl/src/schedulers/queue.rs @@ -5,7 +5,7 @@ use core::marker::PhantomData; use crate::{ corpus::{Corpus, CorpusId, HasTestcase}, - schedulers::{RemovableScheduler, Scheduler}, + schedulers::{HasQueueCycles, RemovableScheduler, Scheduler}, state::{HasCorpus, State, UsesState}, Error, }; @@ -13,6 +13,8 @@ use crate::{ /// Walk the corpus in a queue-like fashion #[derive(Debug, Clone)] pub struct QueueScheduler { + queue_cycles: u64, + runs_in_current_cycle: u64, phantom: PhantomData, } @@ -55,6 +57,12 @@ where .map(|id| state.corpus().next(id)) .flatten() .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))?; Ok(id) } @@ -66,6 +74,8 @@ impl QueueScheduler { #[must_use] pub fn new() -> Self { Self { + runs_in_current_cycle: 0, + queue_cycles: 0, phantom: PhantomData, } } @@ -77,6 +87,15 @@ impl Default for QueueScheduler { } } +impl HasQueueCycles for QueueScheduler +where + S: HasCorpus + HasTestcase + State, +{ + fn queue_cycles(&self) -> u64 { + self.queue_cycles + } +} + #[cfg(test)] #[cfg(feature = "std")] mod tests { diff --git a/libafl/src/schedulers/weighted.rs b/libafl/src/schedulers/weighted.rs index 8c56931392..8cb56139ec 100644 --- a/libafl/src/schedulers/weighted.rs +++ b/libafl/src/schedulers/weighted.rs @@ -19,7 +19,7 @@ use crate::{ schedulers::{ powersched::{PowerSchedule, SchedulerMetadata}, testcase_score::{CorpusWeightTestcaseScore, TestcaseScore}, - AflScheduler, RemovableScheduler, Scheduler, + AflScheduler, HasQueueCycles, RemovableScheduler, Scheduler, }, state::{HasCorpus, HasRand, State, UsesState}, Error, HasMetadata, @@ -100,6 +100,7 @@ pub struct WeightedScheduler { strat: Option, map_observer_handle: Handle, last_hash: usize, + queue_cycles: u64, phantom: PhantomData<(F, O, S)>, /// Cycle `PowerSchedule` on completion of every queue cycle. cycle_schedules: bool, @@ -128,6 +129,7 @@ where strat, map_observer_handle: map_observer.handle(), last_hash: 0, + queue_cycles: 0, table_invalidated: true, cycle_schedules: false, phantom: PhantomData, @@ -307,6 +309,18 @@ where } } +impl HasQueueCycles for WeightedScheduler +where + F: TestcaseScore, + O: MapObserver, + S: HasCorpus + HasMetadata + HasRand + HasTestcase + State, + C: AsRef + Named, +{ + fn queue_cycles(&self) -> u64 { + self.queue_cycles + } +} + impl Scheduler for WeightedScheduler where F: TestcaseScore, @@ -369,8 +383,9 @@ where // Update depth if runs_in_current_cycle >= corpus_counts { + self.queue_cycles += 1; let psmeta = state.metadata_mut::()?; - psmeta.set_queue_cycles(psmeta.queue_cycles() + 1); + psmeta.set_queue_cycles(self.queue_cycles()); if self.cycle_schedules { self.cycle_schedule(psmeta)?; } diff --git a/libafl/src/stages/sync.rs b/libafl/src/stages/sync.rs index 0210ee107a..ee73f0305e 100644 --- a/libafl/src/stages/sync.rs +++ b/libafl/src/stages/sync.rs @@ -1,7 +1,7 @@ //! The [`SyncFromDiskStage`] is a stage that imports inputs from disk for e.g. sync with AFL use alloc::borrow::{Cow, ToOwned}; -use core::marker::PhantomData; +use core::{marker::PhantomData, time::Duration}; use std::{ fs, path::{Path, PathBuf}, @@ -33,7 +33,7 @@ use crate::{ #[derive(Serialize, Deserialize, Debug)] pub struct SyncFromDiskMetadata { /// The last time the sync was done - pub last_time: SystemTime, + pub last_time: Duration, /// The paths that are left to sync pub left_to_sync: Vec, } @@ -43,7 +43,7 @@ libafl_bolts::impl_serdeany!(SyncFromDiskMetadata); impl SyncFromDiskMetadata { /// Create a new [`struct@SyncFromDiskMetadata`] #[must_use] - pub fn new(last_time: SystemTime, left_to_sync: Vec) -> Self { + pub fn new(last_time: Duration, left_to_sync: Vec) -> Self { Self { last_time, left_to_sync, @@ -58,8 +58,9 @@ pub const SYNC_FROM_DISK_STAGE_NAME: &str = "sync"; #[derive(Debug)] pub struct SyncFromDiskStage { name: Cow<'static, str>, - sync_dir: PathBuf, + sync_dirs: Vec, load_callback: CB, + interval: Duration, phantom: PhantomData<(E, EM, Z)>, } @@ -92,54 +93,51 @@ where state: &mut Self::State, manager: &mut EM, ) -> Result<(), Error> { - log::debug!("Syncing from disk: {:?}", self.sync_dir); let last = state .metadata_map() .get::() .map(|m| m.last_time); - if let (Some(max_time), mut new_files) = self.load_from_directory(None, &last)? { - if last.is_none() { - state - .metadata_map_mut() - .insert(SyncFromDiskMetadata::new(max_time, new_files)); - } else { - state - .metadata_map_mut() - .get_mut::() - .unwrap() - .last_time = max_time; - state - .metadata_map_mut() - .get_mut::() - .unwrap() - .left_to_sync - .append(&mut new_files); + if let Some(last) = last { + if (last + self.interval) < current_time() { + return Ok(()); } } - if let Some(sync_from_disk_metadata) = - state.metadata_map_mut().get_mut::() - { - // Iterate over the paths of files left to sync. - // By keeping track of these files, we ensure that no file is missed during synchronization, - // even in the event of a target restart. - 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_map_mut() - .get_mut::() - .unwrap() - .left_to_sync - .retain(|p| p != &path); - log::debug!("Evaluating: {:?}", path); - fuzzer.evaluate_input(state, executor, manager, input)?; - } + let max_time = match last { + None => None, + Some(last) => Some(last + self.interval), + }; + let new_max_time = max_time.unwrap_or(current_time()); + + let mut new_files = vec![]; + for dir in &self.sync_dirs { + log::debug!("Syncing from dir: {:?}", dir); + let new_dir_files = self.load_from_directory(dir, &max_time)?; + new_files.extend(new_dir_files); + } + *state.metadata_mut::().unwrap() = SyncFromDiskMetadata { + last_time: new_max_time, + left_to_sync: new_files, + }; + let sync_from_disk_metadata = state.metadata_mut::().unwrap(); + // Iterate over the paths of files left to sync. + // By keeping track of these files, we ensure that no file is missed during synchronization, + // even in the event of a target restart. + 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::() + .unwrap() + .left_to_sync + .retain(|p| p != &path); + log::debug!("Evaluating: {:?}", path); + fuzzer.evaluate_input(state, executor, manager, input)?; } #[cfg(feature = "introspection")] @@ -164,28 +162,24 @@ where impl SyncFromDiskStage { /// Creates a new [`SyncFromDiskStage`] #[must_use] - pub fn new(sync_dir: PathBuf, load_callback: CB, name: &str) -> Self { + pub fn new(sync_dirs: Vec, load_callback: CB, interval: Duration, name: &str) -> Self { Self { name: Cow::Owned(SYNC_FROM_DISK_STAGE_NAME.to_owned() + ":" + name), phantom: PhantomData, - sync_dir, + sync_dirs, + interval, load_callback, } } + #[allow(clippy::only_used_in_recursion)] fn load_from_directory( &self, - path: Option, - last: &Option, - ) -> Result<(Option, Vec), Error> { - let mut max_time = None; + path: &PathBuf, + last: &Option, + ) -> Result, Error> { let mut left_to_sync = Vec::::new(); - let in_dir = match path { - Some(p) => p, - None => self.sync_dir.clone(), - }; - - for entry in fs::read_dir(in_dir)? { + for entry in fs::read_dir(path)? { let entry = entry?; let path = entry.path(); let attributes = fs::metadata(&path); @@ -198,26 +192,21 @@ impl SyncFromDiskStage { if attr.is_file() && attr.len() > 0 { if let Ok(time) = attr.modified() { - if let Some(l) = last { - if time.duration_since(*l).is_err() || time == *l { + if let Some(last) = last { + if time.duration_since(SystemTime::UNIX_EPOCH).unwrap() < *last { continue; } } - max_time = Some(max_time.map_or(time, |t: SystemTime| t.max(time))); log::info!("Syncing file: {:?}", path); left_to_sync.push(path.clone()); } } else if attr.is_dir() { - let (dir_max_time, dir_left_to_sync) = - 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))); - } + let dir_left_to_sync = self.load_from_directory(&entry.path(), last)?; 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 #[must_use] - pub fn with_from_file(sync_dir: PathBuf) -> Self { + pub fn with_from_file(sync_dirs: Vec, interval: Duration) -> Self { fn load_callback( _: &mut Z, _: &mut S, @@ -242,8 +231,9 @@ where Input::from_file(p) } Self { + interval, name: Cow::Borrowed(SYNC_FROM_DISK_STAGE_NAME), - sync_dir, + sync_dirs, load_callback: load_callback::<_, _>, phantom: PhantomData, } diff --git a/libafl_bolts/src/os/mod.rs b/libafl_bolts/src/os/mod.rs index 5160bbb50b..1c4d83862c 100644 --- a/libafl_bolts/src/os/mod.rs +++ b/libafl_bolts/src/os/mod.rs @@ -115,6 +115,28 @@ pub fn dup(fd: RawFd) -> Result { } } +// 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 { + use core::mem; + use std::io; + + use libc::{rusage, RUSAGE_CHILDREN}; + + let rss = unsafe { + let mut rusage = mem::MaybeUninit::::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 /// /// # Safety