use candid::Principal;
use chrono::Local;
use ic_management_canister_types::CanisterId;
use libafl::feedback_or;
use libafl::feedbacks::{ExitKindFeedback, TimeoutFeedback};
use pocket_ic::PocketIc;
use std::fs::{self, File};
use std::io::{Read, Write as IoWrite};
use std::path::PathBuf;
use std::sync::Arc;
struct SessionInfo {
name: String,
corpus_dir: PathBuf,
input_dir: PathBuf,
crashes_dir: PathBuf,
rng_seed: u64,
}
static SESSION_INFO: std::sync::OnceLock<SessionInfo> = std::sync::OnceLock::new();
fn print_session_info() {
if let Some(info) = SESSION_INFO.get() {
eprintln!("\n### Fuzzer session summary ###");
eprintln!(" Fuzzer: {}", info.name);
eprintln!(" Seed corpus: {}", info.corpus_dir.display());
eprintln!(" Input dir: {}", info.input_dir.display());
eprintln!(" Crashes dir: {}", info.crashes_dir.display());
eprintln!(" RNG seed: {}", info.rng_seed);
eprintln!("#############################");
}
}
use crate::custom::feedback::oom_exit_kind::OomLogic;
use crate::custom::mutator::candid::{CandidParserMutator, CandidTypeDefArgs};
use crate::libafl::{
Evaluator,
corpus::CachedOnDiskCorpus,
events::SimpleEventManager,
executors::{ExitKind, inprocess::InProcessExecutor},
feedbacks::{CrashFeedback, map::AflMapFeedback},
fuzzer::{Fuzzer, StdFuzzer},
inputs::BytesInput,
mutators::{HavocScheduledMutator, havoc_mutations},
observers::{
CanTrack,
map::{StdMapObserver, hitcount_map::HitcountsMapObserver},
},
schedulers::{
IndexesLenTimeMinimizerScheduler, StdWeightedScheduler, powersched::PowerSchedule,
},
stages::{AflStatsStage, CalibrationStage, StdPowerMutationalStage},
state::StdState,
};
use crate::libafl::monitors::SimpleMonitor;
use crate::libafl_bolts::{current_nanos, rands::StdRand, tuples::tuple_list};
use crate::constants::{COVERAGE_FN_EXPORT_NAME, INSTRUCTION_COUNT_FN_EXPORT_NAME};
use crate::custom::observer::instruction_count::INSTRUCTION_MAP;
use crate::fuzzer::FuzzerState;
#[derive(Debug, Clone, Default)]
pub struct InstructionConfig {
pub enabled: bool,
pub max_instruction_count: Option<u64>,
}
pub trait FuzzerOrchestrator: AsRef<FuzzerState> + AsMut<FuzzerState> {
fn init(&mut self);
fn setup(&self) {}
fn execute(&self, input: BytesInput) -> ExitKind;
fn get_state_machine(&self) -> Arc<PocketIc> {
self.as_ref().get_state_machine()
}
fn get_coverage_canister_id(&self) -> CanisterId {
self.as_ref().get_coverage_canister_id()
}
fn input_dir(&self) -> PathBuf {
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR is not set");
let input_dir = PathBuf::from(out_dir)
.join("artifacts")
.join(self.as_ref().name())
.join(Local::now().format("%Y%m%d_%H%M").to_string())
.join("input");
fs::create_dir_all(&input_dir)
.unwrap_or_else(|e| panic!("Failed to create input directory {input_dir:?}: {e}"));
println!("Input directory: {input_dir:?}");
input_dir
}
fn crashes_dir(&self) -> PathBuf {
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR is not set");
let crashes_dir = PathBuf::from(out_dir)
.join("artifacts")
.join(self.as_ref().name())
.join(Local::now().format("%Y%m%d_%H%M").to_string())
.join("crashes");
fs::create_dir_all(&crashes_dir)
.unwrap_or_else(|e| panic!("Failed to create crashes directory {crashes_dir:?}: {e}"));
println!("Crashes directory: {crashes_dir:?}");
crashes_dir
}
fn corpus_dir(&self) -> PathBuf;
#[allow(static_mut_refs)]
fn set_coverage_map(&self) {
let test = self.get_state_machine();
let result = test.update_call(
self.get_coverage_canister_id(),
Principal::anonymous(),
COVERAGE_FN_EXPORT_NAME,
vec![],
);
if let Ok(result) = result {
unsafe { crate::instrumentation::COVERAGE_MAP.copy_from_slice(&result) };
}
}
fn get_coverage_map(&self) -> &'static mut [u8] {
unsafe { crate::instrumentation::COVERAGE_MAP }
}
fn get_candid_args() -> Option<CandidTypeDefArgs> {
None
}
fn instruction_config() -> InstructionConfig {
InstructionConfig::default()
}
#[allow(static_mut_refs)]
fn set_instruction_count(&self, input: &BytesInput) -> bool {
let test = self.get_state_machine();
let result = test.query_call(
self.get_coverage_canister_id(),
Principal::anonymous(),
INSTRUCTION_COUNT_FN_EXPORT_NAME,
vec![],
);
if let Ok(result) = result
&& result.len() >= 8
{
let instructions = u64::from_le_bytes(result[0..8].try_into().unwrap());
let mut map = unsafe { INSTRUCTION_MAP.borrow_mut() };
if instructions > map.max_instructions {
let prev = map.max_instructions;
map.increased = true;
map.max_instructions = instructions;
let input_bytes: Vec<u8> = input.clone().into();
let input_len = input_bytes.len();
let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S");
let corpus_dir = self.corpus_dir();
let corpus_file = corpus_dir.join(format!("max_instructions_{instructions}"));
if let Ok(mut f) = File::create(&corpus_file) {
let _ = f.write_all(&input_bytes);
}
let hex_preview: String = input_bytes
.iter()
.take(64)
.map(|b| format!("{b:02x}"))
.collect();
let truncated = if input_len > 64 { "..." } else { "" };
let log_line = format!(
"[instructions] NEW MAX | timestamp: {timestamp} | instructions: {instructions} (prev: {prev}) | input_len: {input_len} | hex: {hex_preview}{truncated} | corpus_file: {}",
corpus_file.display()
);
println!("{log_line}");
let log_path = corpus_dir.join("instruction_log.txt");
if let Ok(mut f) = fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
{
let _ = writeln!(f, "{log_line}");
}
} else {
map.increased = false;
}
map.current_instructions = instructions;
if let Some(threshold) = Self::instruction_config().max_instruction_count
&& instructions > threshold
{
return true;
}
}
false
}
#[allow(static_mut_refs)]
fn run(&mut self) {
self.init();
let inst_config = Self::instruction_config();
let mut harness = |input: &BytesInput| {
self.setup();
let result = self.execute(input.clone());
self.set_coverage_map();
if inst_config.enabled && self.set_instruction_count(input) {
return ExitKind::Crash;
}
result
};
let hitcount_map_observer = HitcountsMapObserver::new(unsafe {
StdMapObserver::new("coverage_map", self.get_coverage_map())
})
.track_indices();
let afl_map_feedback = AflMapFeedback::new(&hitcount_map_observer);
let candid_enabled = Self::get_candid_args().is_some();
match (inst_config.enabled, candid_enabled) {
(true, true) => {
use crate::custom::feedback::instruction_count::InstructionCountFeedback;
use crate::custom::observer::instruction_count::INSTRUCTION_COUNT_OBSERVER_NAME;
use crate::libafl::observers::RefCellValueObserver;
use crate::libafl_bolts::ownedref::OwnedRef;
use std::ptr::addr_of;
let instruction_count_observer = unsafe {
RefCellValueObserver::new(
INSTRUCTION_COUNT_OBSERVER_NAME,
OwnedRef::from_ptr(addr_of!(INSTRUCTION_MAP)),
)
};
let candid_mutator = CandidParserMutator::new(Self::get_candid_args());
let feedback =
feedback_or!(afl_map_feedback.clone(), InstructionCountFeedback::new());
run_fuzzing_loop!(
self,
&mut harness,
hitcount_map_observer,
(instruction_count_observer),
(StdPowerMutationalStage::new(candid_mutator)),
afl_map_feedback,
feedback
);
}
(true, false) => {
use crate::custom::feedback::instruction_count::InstructionCountFeedback;
use crate::custom::observer::instruction_count::INSTRUCTION_COUNT_OBSERVER_NAME;
use crate::libafl::observers::RefCellValueObserver;
use crate::libafl_bolts::ownedref::OwnedRef;
use std::ptr::addr_of;
let instruction_count_observer = unsafe {
RefCellValueObserver::new(
INSTRUCTION_COUNT_OBSERVER_NAME,
OwnedRef::from_ptr(addr_of!(INSTRUCTION_MAP)),
)
};
let feedback =
feedback_or!(afl_map_feedback.clone(), InstructionCountFeedback::new());
run_fuzzing_loop!(
self,
&mut harness,
hitcount_map_observer,
(instruction_count_observer),
(),
afl_map_feedback,
feedback
);
}
(false, true) => {
let candid_mutator = CandidParserMutator::new(Self::get_candid_args());
let feedback = afl_map_feedback.clone();
run_fuzzing_loop!(
self,
&mut harness,
hitcount_map_observer,
(),
(StdPowerMutationalStage::new(candid_mutator)),
afl_map_feedback,
feedback
);
}
(false, false) => {
let feedback = afl_map_feedback.clone();
run_fuzzing_loop!(
self,
&mut harness,
hitcount_map_observer,
(),
(),
afl_map_feedback,
feedback
);
}
}
}
fn test_one_input(&mut self, bytes: Vec<u8>) {
self.init();
self.setup();
let result = self.execute(BytesInput::new(bytes));
println!("Execution result: {result:?}");
}
}
#[macro_export]
macro_rules! run_fuzzing_loop {
($self:expr, $harness:expr, $map_observer:expr, ($($extra_observer:expr),*), ($($extra_stage:expr),*), $afl_map_feedback:expr, $feedback:expr) => {{
let map_observer = $map_observer;
let afl_map_feedback = $afl_map_feedback;
let mut feedback = $feedback;
let calibration_stage = CalibrationStage::new(&afl_map_feedback);
let crash_feedback = CrashFeedback::new();
let timeout_feedback = TimeoutFeedback::new();
let oom_feedback: ExitKindFeedback<OomLogic> = ExitKindFeedback::new();
let mut objective = feedback_or!(crash_feedback, timeout_feedback, oom_feedback);
let stats_stage = AflStatsStage::builder()
.map_feedback(&afl_map_feedback)
.build()
.unwrap();
let rng_seed = current_nanos();
let input_dir = $self.input_dir();
let crashes_dir = $self.crashes_dir();
let corpus_dir = $self.corpus_dir();
let _ = SESSION_INFO.set(SessionInfo {
name: $self.as_ref().name().to_string(),
corpus_dir: corpus_dir.clone(),
input_dir: input_dir.clone(),
crashes_dir: crashes_dir.clone(),
rng_seed,
});
print_session_info();
let default_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
print_session_info();
default_hook(info);
}));
let _ = ctrlc::set_handler(move || {
print_session_info();
std::process::exit(130);
});
let mut state = StdState::new(
StdRand::with_seed(rng_seed),
CachedOnDiskCorpus::new(input_dir, 512).unwrap(),
CachedOnDiskCorpus::new(crashes_dir, 512).unwrap(),
&mut feedback,
&mut objective,
)
.unwrap();
let weighted = StdWeightedScheduler::with_schedule(
&mut state,
&map_observer,
Some(PowerSchedule::fast()),
);
let scheduler = IndexesLenTimeMinimizerScheduler::new(&map_observer, weighted);
let mon = SimpleMonitor::new(|s| println!("{s}"));
let mut mgr = SimpleEventManager::new(mon);
let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective);
let observers = tuple_list!(map_observer $(, $extra_observer)*);
let mut executor =
InProcessExecutor::new($harness, observers, &mut fuzzer, &mut state, &mut mgr)
.expect("Failed to create the Executor");
fn is_corpus_entry(name: &str) -> bool {
name != "instruction_log.txt" && name != ".gitignore"
}
let corpus_entries: Vec<_> = fs::read_dir(&corpus_dir)
.unwrap()
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.file_name().and_then(|n| n.to_str()).is_some_and(is_corpus_entry))
.collect();
if corpus_entries.is_empty() {
use rand::RngCore;
let mut rng = rand::rng();
let len = (rng.next_u32() % 1024 + 1) as usize;
let mut buf = vec![0u8; len];
rng.fill_bytes(&mut buf);
println!("Corpus was empty — using a randomly generated seed ({len} bytes)");
fuzzer.evaluate_input(&mut state, &mut executor, &mut mgr, &BytesInput::new(buf)).unwrap();
}
for p in &corpus_entries {
let mut f = File::open(p).unwrap();
let mut buffer = Vec::new();
f.read_to_end(&mut buffer).unwrap();
fuzzer.evaluate_input(&mut state, &mut executor, &mut mgr, &BytesInput::new(buffer)).unwrap();
}
let havoc_mutator = HavocScheduledMutator::new(havoc_mutations());
let mut stages = tuple_list!(
calibration_stage,
$($extra_stage,)*
StdPowerMutationalStage::new(havoc_mutator),
stats_stage
);
fuzzer
.fuzz_loop(&mut stages, &mut executor, &mut state, &mut mgr)
.expect("Error in the fuzzing loop");
}};
}
use run_fuzzing_loop;