Rowan Hart 0b25d723c0
Windows Support for LibAFL-LibFuzzer (#3130)
* Add windows build script and additional changes to support windows for libafl-libfuzzer

* Update build scripts and harness wrapping directives

* Resolve issue with corpus edge count calculation

* Add help message and make fork do nothing on Windows

* Format harness_wrap.cpp

* Clippy happiness pass

* Clippy happiness pass

* Clippy happiness pass

* Correct logic

* Correct logic

* Update help output and make runs argument work

* Add test for libafl_libfuzzer on windows

* Add workflow for libafl_libfuzzer test

* Fix copy without dependent task

* Add libafl_libfuzzer_windows to preflight list

* Format harness

* Explicitly ignore windows fuzzer

* Remove windows-specific copy from unix instructions

* Ensure using nightly

* Fix job name

* Update build to use libFuzzer.lib on Windows to keep consistent with Linux

* Remove nightly requirement

---------

Co-authored-by: Rowan Hart <rowanhart@microsoft.com>
2025-05-21 01:35:48 +02:00

267 lines
8.9 KiB
Rust

use std::{env::temp_dir, ffi::c_int, fs::rename};
#[cfg(unix)]
use std::{
fs::File,
io::Write,
os::fd::{AsRawFd, FromRawFd},
};
use libafl::{
Error, HasScheduler, StdFuzzer,
corpus::Corpus,
events::{SendExiting, SimpleRestartingEventManager},
executors::{ExitKind, InProcessExecutor},
feedback_and_fast, feedback_or_fast,
feedbacks::{CrashFeedback, MinMapFeedback, TimeoutFeedback},
inputs::{BytesInput, HasTargetBytes},
monitors::MultiMonitor,
observers::{MultiMapObserver, TimeObserver},
schedulers::RemovableScheduler,
state::{HasCorpus, HasRand, StdState},
};
use libafl_bolts::{
AsSlice,
rands::{Rand, StdRand},
shmem::{ShMemProvider, StdShMemProvider},
tuples::tuple_list,
};
use libafl_targets::{OomFeedback, OomObserver, counters_maps_ptr_mut};
use crate::{
corpus::{ArtifactCorpus, LibfuzzerCorpus},
feedbacks::{LibfuzzerCrashCauseFeedback, LibfuzzerKeepFeedback},
observers::{MappedEdgeMapObserver, SizeTimeValueObserver},
options::LibfuzzerOptions,
schedulers::MergeScheduler,
};
#[expect(clippy::too_many_lines)]
pub fn merge(
options: &LibfuzzerOptions,
harness: &extern "C" fn(*const u8, usize) -> c_int,
) -> Result<(), Error> {
if options.dirs().is_empty() {
return Err(Error::illegal_argument(
"Missing corpora to minimize; you should provide one directory to minimize into and one-to-many from which the inputs are loaded.",
));
}
let crash_corpus = ArtifactCorpus::new();
let keep_observer = LibfuzzerKeepFeedback::new();
let keep = keep_observer.keep();
let mut shmem_provider = StdShMemProvider::new().unwrap();
#[cfg(unix)]
let mut stderr = unsafe {
let new_fd = libc::dup(std::io::stderr().as_raw_fd());
File::from_raw_fd(new_fd)
};
let monitor = MultiMonitor::new(move |s| {
#[cfg(unix)]
writeln!(stderr, "{s}").expect("Could not write to stderr???");
#[cfg(not(unix))]
eprintln!("{s}");
});
let (state, mut mgr): (
Option<StdState<_, _, _, _>>,
SimpleRestartingEventManager<_, _, StdState<_, _, _, _>, _, _>,
) = match SimpleRestartingEventManager::launch(monitor, &mut shmem_provider) {
// The restarting state will spawn the same process again as child, then restarted it each time it crashes.
Ok(res) => res,
Err(err) => match err {
Error::ShuttingDown => {
return Ok(());
}
_ => {
panic!("Failed to setup the restarter: {err}");
}
},
};
#[cfg(unix)]
{
if options.close_fd_mask() != 0 {
let file_null = File::open("/dev/null")?;
unsafe {
if options.close_fd_mask() & 1 != 0 {
libc::dup2(file_null.as_raw_fd(), 1);
}
if options.close_fd_mask() & 2 != 0 {
libc::dup2(file_null.as_raw_fd(), 2);
}
}
}
}
let edges = unsafe { core::mem::take(&mut *(counters_maps_ptr_mut())) };
let edges_observer = MultiMapObserver::new("edges", edges);
let time = TimeObserver::new("time");
let edges_observer =
MappedEdgeMapObserver::new(edges_observer, SizeTimeValueObserver::new(time));
let map_feedback = MinMapFeedback::new(&edges_observer);
// Create an OOM observer to monitor if an OOM has occurred
let oom_observer = OomObserver::new(options.rss_limit(), options.malloc_limit());
// Feedback to rate the interestingness of an input
// This one is composed by two Feedbacks in OR
let mut feedback = feedback_and_fast!(keep_observer, map_feedback);
// A feedback to choose if an input is a solution or not
let mut objective = feedback_or_fast!(
LibfuzzerCrashCauseFeedback::new(options.artifact_prefix().clone()),
OomFeedback,
CrashFeedback::new(),
TimeoutFeedback::new()
);
let observers = tuple_list!(edges_observer, oom_observer);
// scheduler doesn't really matter here
let scheduler = MergeScheduler::new();
let mut state = state.map_or_else(|| {
let mut rand = StdRand::new();
let corpus_dir = if options.dirs().first().unwrap().exists()
&& options
.dirs()
.first()
.unwrap()
.read_dir()?
.any(|entry| entry.map_or(true, |e| !(e.file_name() == "." || e.file_name() == "..")))
{
let temp = temp_dir().join(format!("libafl-merge-{}{}", rand.next(), rand.next()));
eprintln!("Warning: creating an intermediary directory for minimisation at {}. We will move your existing corpus dir to.", temp.to_str().unwrap());
temp
} else {
options.dirs().first().cloned().unwrap()
};
StdState::new(
// RNG
StdRand::new(),
// Corpus that will be evolved, we keep it in memory for performance
LibfuzzerCorpus::new(corpus_dir, 4096),
// Corpus in which we store solutions (crashes in this example),
// on disk so the user can get them after stopping the fuzzer
crash_corpus,
// A reference to the feedbacks, to create their feedback state
&mut feedback,
// A reference to the objectives, to create their objective state
&mut objective,
)
}, Ok)?;
let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective); // The wrapped harness function, calling out to the LLVM-style harness
let mut harness = |input: &BytesInput| {
let target = input.target_bytes();
let buf = target.as_slice();
let result = unsafe {
crate::libafl_libfuzzer_test_one_input(Some(*harness), buf.as_ptr(), buf.len())
};
if result == -2 {
ExitKind::Crash
} else {
*keep.borrow_mut() = result == 0;
ExitKind::Ok
}
};
// Create the executor for an in-process function with one observer for edge coverage and one for the execution time
let mut executor = InProcessExecutor::with_timeout(
&mut harness,
observers,
&mut fuzzer,
&mut state,
&mut mgr,
options.timeout(),
)?;
// In case the corpus is empty (on first run) or crashed while loading, reset
if state.must_load_initial_inputs() && !options.dirs().is_empty() {
let loaded_dirs = options
.dirs()
.iter()
.filter(|&dir| state.corpus().dir_path() != dir)
.cloned()
.collect::<Vec<_>>();
// Load from disk
state
.load_initial_inputs(&mut fuzzer, &mut executor, &mut mgr, &loaded_dirs)
.unwrap_or_else(|e| {
panic!(
"Failed to load initial corpus at {:?}: {}",
options.dirs(),
e
)
});
}
for id in fuzzer.scheduler().removable() {
let testcase = state.corpus_mut().remove(id)?;
fuzzer
.scheduler_mut()
.on_remove(&mut state, id, &Some(testcase))?;
}
for id in fuzzer.scheduler().current().clone() {
let mut testcase = state.corpus_mut().get(id)?.borrow_mut();
let file_path = testcase
.file_path_mut()
.as_mut()
.expect("No file backing for corpus entry");
if let Some((base, _)) = file_path
.file_name()
.unwrap()
.to_str()
.unwrap()
.rsplit_once('-')
{
let mut new_file_path = file_path.clone();
new_file_path.pop();
new_file_path.push(base);
if new_file_path.exists() {
drop(testcase);
let testcase = state.corpus_mut().remove(id)?;
fuzzer
.scheduler_mut()
.on_remove(&mut state, id, &Some(testcase))?;
} else {
// False-positive: file_path is used just below
rename(&file_path, &new_file_path)?;
*file_path = new_file_path;
}
}
}
println!(
"Minimization complete; reduced to {} inputs!",
state.corpus().count()
);
let corpus_dir = state.corpus().dir_path().clone();
if corpus_dir != options.dirs()[0] {
let temp = temp_dir().join(format!(
"libafl-merge-orig-{}{}",
state.rand_mut().next(),
state.rand_mut().next()
));
eprintln!(
"Moving original corpus directory {} to {} and replacing it with minimisation result ({}).",
options.dirs()[0].to_str().unwrap(),
temp.to_str().unwrap(),
corpus_dir.to_str().unwrap()
);
rename(&options.dirs()[0], temp)?;
rename(corpus_dir, &options.dirs()[0])?;
}
mgr.send_exiting()
}