Dominik Maier 2ed6583041
CI: Run miri tests (#1130)
* Fixes/ignores for miri support

* linux

* fix doctest for miri

* fix docs

* fix UB in baby_fuzzer

* no custom allocator in miri
2023-03-08 19:21:17 +01:00

1321 lines
41 KiB
Rust

//! Expose an `Executor` based on a `Forkserver` in order to execute AFL/AFL++ binaries
use alloc::{borrow::ToOwned, string::ToString, vec::Vec};
use core::{
fmt::{self, Debug, Formatter},
marker::PhantomData,
sync::atomic::{compiler_fence, Ordering},
time::Duration,
};
use std::{
ffi::{OsStr, OsString},
io::{self, prelude::*, ErrorKind},
os::unix::{io::RawFd, process::CommandExt},
path::Path,
process::{Command, Stdio},
};
use nix::{
sys::{
select::{pselect, FdSet},
signal::{kill, SigSet, Signal},
time::{TimeSpec, TimeValLike},
},
unistd::Pid,
};
#[cfg(feature = "regex")]
use crate::observers::{get_asan_runtime_flags_with_log_path, AsanBacktraceObserver};
use crate::{
bolts::{
fs::{get_unique_std_input_file, InputFile},
os::{dup2, pipes::Pipe},
shmem::{ShMem, ShMemProvider, UnixShMemProvider},
tuples::Prepend,
AsMutSlice, AsSlice,
},
executors::{Executor, ExitKind, HasObservers},
inputs::{HasTargetBytes, Input, UsesInput},
mutators::Tokens,
observers::{MapObserver, Observer, ObserversTuple, UsesObservers},
state::UsesState,
Error,
};
const FORKSRV_FD: i32 = 198;
#[allow(clippy::cast_possible_wrap)]
const FS_OPT_ENABLED: i32 = 0x80000001_u32 as i32;
#[allow(clippy::cast_possible_wrap)]
const FS_OPT_MAPSIZE: i32 = 0x40000000_u32 as i32;
#[allow(clippy::cast_possible_wrap)]
const FS_OPT_SHDMEM_FUZZ: i32 = 0x01000000_u32 as i32;
#[allow(clippy::cast_possible_wrap)]
const FS_OPT_AUTODICT: i32 = 0x10000000_u32 as i32;
// #[allow(clippy::cast_possible_wrap)]
// const FS_OPT_MAX_MAPSIZE: i32 = ((0x00fffffe_u32 >> 1) + 1) as i32; // 8388608
const fn fs_opt_get_mapsize(x: i32) -> i32 {
((x & 0x00fffffe) >> 1) + 1
}
/* const fn fs_opt_set_mapsize(x: usize) -> usize {
if x <= 1 {
if x > FS_OPT_MAX_MAPSIZE { 0 } else { (x - 1) << 1 }
} else { 0 }
} */
/// The length of header bytes which tells shmem size
const SHMEM_FUZZ_HDR_SIZE: usize = 4;
const MAX_FILE: usize = 1024 * 1024;
/// Configure the target, `limit`, `setsid`, `pipe_stdin`, the code was borrowed from the [`Angora`](https://github.com/AngoraFuzzer/Angora) fuzzer
pub trait ConfigTarget {
/// Sets the sid
fn setsid(&mut self) -> &mut Self;
/// Sets a mem limit
fn setlimit(&mut self, memlimit: u64) -> &mut Self;
/// Sets the stdin
fn setstdin(&mut self, fd: RawFd, use_stdin: bool) -> &mut Self;
/// Sets the AFL forkserver pipes
fn setpipe(
&mut self,
st_read: RawFd,
st_write: RawFd,
ctl_read: RawFd,
ctl_write: RawFd,
) -> &mut Self;
}
impl ConfigTarget for Command {
fn setsid(&mut self) -> &mut Self {
let func = move || {
unsafe {
libc::setsid();
};
Ok(())
};
unsafe { self.pre_exec(func) }
}
fn setpipe(
&mut self,
st_read: RawFd,
st_write: RawFd,
ctl_read: RawFd,
ctl_write: RawFd,
) -> &mut Self {
let func = move || {
match dup2(ctl_read, FORKSRV_FD) {
Ok(_) => (),
Err(_) => {
return Err(io::Error::last_os_error());
}
}
match dup2(st_write, FORKSRV_FD + 1) {
Ok(_) => (),
Err(_) => {
return Err(io::Error::last_os_error());
}
}
unsafe {
libc::close(st_read);
libc::close(st_write);
libc::close(ctl_read);
libc::close(ctl_write);
}
Ok(())
};
unsafe { self.pre_exec(func) }
}
fn setstdin(&mut self, fd: RawFd, use_stdin: bool) -> &mut Self {
if use_stdin {
let func = move || {
match dup2(fd, libc::STDIN_FILENO) {
Ok(_) => (),
Err(_) => {
return Err(io::Error::last_os_error());
}
}
Ok(())
};
unsafe { self.pre_exec(func) }
} else {
self
}
}
#[allow(trivial_numeric_casts, clippy::cast_possible_wrap)]
fn setlimit(&mut self, memlimit: u64) -> &mut Self {
if memlimit == 0 {
return self;
}
let func = move || {
let memlimit: libc::rlim_t = (memlimit as libc::rlim_t) << 20;
let r = libc::rlimit {
rlim_cur: memlimit,
rlim_max: memlimit,
};
let r0 = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
#[cfg(target_os = "openbsd")]
let mut ret = unsafe { libc::setrlimit(libc::RLIMIT_RSS, &r) };
#[cfg(not(target_os = "openbsd"))]
let mut ret = unsafe { libc::setrlimit(libc::RLIMIT_AS, &r) };
if ret < 0 {
return Err(io::Error::last_os_error());
}
ret = unsafe { libc::setrlimit(libc::RLIMIT_CORE, &r0) };
if ret < 0 {
return Err(io::Error::last_os_error());
}
Ok(())
};
unsafe { self.pre_exec(func) }
}
}
/// The [`Forkserver`] is communication channel with a child process that forks on request of the fuzzer.
/// The communication happens via pipe.
#[derive(Debug)]
pub struct Forkserver {
st_pipe: Pipe,
ctl_pipe: Pipe,
child_pid: Pid,
status: i32,
last_run_timed_out: i32,
}
#[allow(clippy::fn_params_excessive_bools)]
impl Forkserver {
/// Create a new [`Forkserver`]
#[allow(clippy::too_many_arguments)]
pub fn new(
target: OsString,
args: Vec<OsString>,
envs: Vec<(OsString, OsString)>,
input_filefd: RawFd,
use_stdin: bool,
memlimit: u64,
is_persistent: bool,
is_deferred_frksrv: bool,
debug_output: bool,
) -> Result<Self, Error> {
let mut st_pipe = Pipe::new().unwrap();
let mut ctl_pipe = Pipe::new().unwrap();
let (stdout, stderr) = if debug_output {
(Stdio::inherit(), Stdio::inherit())
} else {
(Stdio::null(), Stdio::null())
};
let mut command = Command::new(target);
// Setup args, stdio
command
.args(args)
.stdin(Stdio::null())
.stdout(stdout)
.stderr(stderr);
// Persistent, deferred forkserver
if is_persistent {
command.env("__AFL_PERSISTENT", "1");
}
if is_deferred_frksrv {
command.env("__AFL_DEFER_FORKSRV", "1");
}
#[cfg(feature = "regex")]
command.env("ASAN_OPTIONS", get_asan_runtime_flags_with_log_path());
match command
.env("LD_BIND_NOW", "1")
.envs(envs)
.setlimit(memlimit)
.setsid()
.setstdin(input_filefd, use_stdin)
.setpipe(
st_pipe.read_end().unwrap(),
st_pipe.write_end().unwrap(),
ctl_pipe.read_end().unwrap(),
ctl_pipe.write_end().unwrap(),
)
.spawn()
{
Ok(_) => (),
Err(err) => {
return Err(Error::illegal_state(format!(
"Could not spawn the forkserver: {err:#?}"
)))
}
};
// Ctl_pipe.read_end and st_pipe.write_end are unnecessary for the parent, so we'll close them
ctl_pipe.close_read_end();
st_pipe.close_write_end();
Ok(Self {
st_pipe,
ctl_pipe,
child_pid: Pid::from_raw(0),
status: 0,
last_run_timed_out: 0,
})
}
/// If the last run timed out
#[must_use]
pub fn last_run_timed_out(&self) -> i32 {
self.last_run_timed_out
}
/// Sets if the last run timed out
pub fn set_last_run_timed_out(&mut self, last_run_timed_out: i32) {
self.last_run_timed_out = last_run_timed_out;
}
/// The status
#[must_use]
pub fn status(&self) -> i32 {
self.status
}
/// Sets the status
pub fn set_status(&mut self, status: i32) {
self.status = status;
}
/// The child pid
#[must_use]
pub fn child_pid(&self) -> Pid {
self.child_pid
}
/// Set the child pid
pub fn set_child_pid(&mut self, child_pid: Pid) {
self.child_pid = child_pid;
}
/// Read from the st pipe
pub fn read_st(&mut self) -> Result<(usize, i32), Error> {
let mut buf: [u8; 4] = [0_u8; 4];
let rlen = self.st_pipe.read(&mut buf)?;
let val: i32 = i32::from_ne_bytes(buf);
Ok((rlen, val))
}
/// Read bytes of any length from the st pipe
pub fn read_st_size(&mut self, size: usize) -> Result<(usize, Vec<u8>), Error> {
let mut buf = vec![0; size];
let rlen = self.st_pipe.read(&mut buf)?;
Ok((rlen, buf))
}
/// Write to the ctl pipe
pub fn write_ctl(&mut self, val: i32) -> Result<usize, Error> {
let slen = self.ctl_pipe.write(&val.to_ne_bytes())?;
Ok(slen)
}
/// Read a message from the child process.
pub fn read_st_timed(&mut self, timeout: &TimeSpec) -> Result<Option<i32>, Error> {
let mut buf: [u8; 4] = [0_u8; 4];
let Some(st_read) = self.st_pipe.read_end() else {
return Err(Error::file(io::Error::new(
ErrorKind::BrokenPipe,
"Read pipe end was already closed",
)));
};
let mut readfds = FdSet::new();
readfds.insert(st_read);
// We'll pass a copied timeout to keep the original timeout intact, because select updates timeout to indicate how much time was left. See select(2)
let sret = pselect(
Some(readfds.highest().unwrap() + 1),
&mut readfds,
None,
None,
Some(timeout),
Some(&SigSet::empty()),
)?;
if sret > 0 {
if self.st_pipe.read_exact(&mut buf).is_ok() {
let val: i32 = i32::from_ne_bytes(buf);
Ok(Some(val))
} else {
Err(Error::unknown(
"Unable to communicate with fork server (OOM?)".to_string(),
))
}
} else {
Ok(None)
}
}
}
/// A struct that has a forkserver
pub trait HasForkserver {
/// The [`ShMemProvider`] used for this forkserver's map
type SP: ShMemProvider;
/// The forkserver
fn forkserver(&self) -> &Forkserver;
/// The forkserver, mutable
fn forkserver_mut(&mut self) -> &mut Forkserver;
/// The file the forkserver is reading from
fn input_file(&self) -> &InputFile;
/// The file the forkserver is reading from, mutable
fn input_file_mut(&mut self) -> &mut InputFile;
/// The map of the fuzzer
fn shmem(&self) -> &Option<<<Self as HasForkserver>::SP as ShMemProvider>::ShMem>;
/// The map of the fuzzer, mutable
fn shmem_mut(&mut self) -> &mut Option<<<Self as HasForkserver>::SP as ShMemProvider>::ShMem>;
/// Whether testcases are expected in shared memory
fn uses_shmem_testcase(&self) -> bool;
}
/// The timeout forkserver executor that wraps around the standard forkserver executor and sets a timeout before each run.
#[derive(Debug)]
pub struct TimeoutForkserverExecutor<E> {
executor: E,
timeout: TimeSpec,
signal: Signal,
}
impl<E> TimeoutForkserverExecutor<E> {
/// Create a new [`TimeoutForkserverExecutor`]
pub fn new(executor: E, exec_tmout: Duration) -> Result<Self, Error> {
let signal = Signal::SIGKILL;
Self::with_signal(executor, exec_tmout, signal)
}
/// Create a new [`TimeoutForkserverExecutor`] that sends a user-defined signal to the timed-out process
pub fn with_signal(executor: E, exec_tmout: Duration, signal: Signal) -> Result<Self, Error> {
let milli_sec = exec_tmout.as_millis() as i64;
let timeout = TimeSpec::milliseconds(milli_sec);
Ok(Self {
executor,
timeout,
signal,
})
}
}
impl<E, EM, Z> Executor<EM, Z> for TimeoutForkserverExecutor<E>
where
E: Executor<EM, Z> + HasForkserver + Debug,
E::Input: HasTargetBytes,
EM: UsesState<State = E::State>,
Z: UsesState<State = E::State>,
{
#[inline]
fn run_target(
&mut self,
_fuzzer: &mut Z,
_state: &mut Self::State,
_mgr: &mut EM,
input: &Self::Input,
) -> Result<ExitKind, Error> {
let mut exit_kind = ExitKind::Ok;
let last_run_timed_out = self.executor.forkserver().last_run_timed_out();
if self.executor.uses_shmem_testcase() {
let shmem = unsafe { self.executor.shmem_mut().as_mut().unwrap_unchecked() };
let target_bytes = input.target_bytes();
let size = target_bytes.as_slice().len();
let size_in_bytes = size.to_ne_bytes();
// The first four bytes tells the size of the shmem.
shmem.as_mut_slice()[..4].copy_from_slice(&size_in_bytes[..4]);
shmem.as_mut_slice()[SHMEM_FUZZ_HDR_SIZE..(SHMEM_FUZZ_HDR_SIZE + size)]
.copy_from_slice(target_bytes.as_slice());
} else {
self.executor
.input_file_mut()
.write_buf(input.target_bytes().as_slice())?;
}
let send_len = self
.executor
.forkserver_mut()
.write_ctl(last_run_timed_out)?;
self.executor.forkserver_mut().set_last_run_timed_out(0);
if send_len != 4 {
return Err(Error::unknown(
"Unable to request new process from fork server (OOM?)".to_string(),
));
}
let (recv_pid_len, pid) = self.executor.forkserver_mut().read_st()?;
if recv_pid_len != 4 {
return Err(Error::unknown(
"Unable to request new process from fork server (OOM?)".to_string(),
));
}
if pid <= 0 {
return Err(Error::unknown(
"Fork server is misbehaving (OOM?)".to_string(),
));
}
self.executor
.forkserver_mut()
.set_child_pid(Pid::from_raw(pid));
if let Some(status) = self
.executor
.forkserver_mut()
.read_st_timed(&self.timeout)?
{
self.executor.forkserver_mut().set_status(status);
if libc::WIFSIGNALED(self.executor.forkserver().status()) {
exit_kind = ExitKind::Crash;
}
} else {
self.executor.forkserver_mut().set_last_run_timed_out(1);
// We need to kill the child in case he has timed out, or we can't get the correct pid in the next call to self.executor.forkserver_mut().read_st()?
let _: Result<(), nix::errno::Errno> =
kill(self.executor.forkserver().child_pid(), self.signal);
let (recv_status_len, _) = self.executor.forkserver_mut().read_st()?;
if recv_status_len != 4 {
return Err(Error::unknown("Could not kill timed-out child".to_string()));
}
exit_kind = ExitKind::Timeout;
}
self.executor
.forkserver_mut()
.set_child_pid(Pid::from_raw(0));
Ok(exit_kind)
}
}
/// This [`Executor`] can run binaries compiled for AFL/AFL++ that make use of a forkserver.
/// Shared memory feature is also available, but you have to set things up in your code.
/// Please refer to AFL++'s docs. <https://github.com/AFLplusplus/AFLplusplus/blob/stable/instrumentation/README.persistent_mode.md>
pub struct ForkserverExecutor<OT, S, SP>
where
SP: ShMemProvider,
{
target: OsString,
args: Vec<OsString>,
input_file: InputFile,
uses_shmem_testcase: bool,
forkserver: Forkserver,
observers: OT,
map: Option<SP::ShMem>,
phantom: PhantomData<S>,
/// Cache that indicates if we have a `ASan` observer registered.
has_asan_observer: Option<bool>,
map_size: Option<usize>,
}
impl<OT, S, SP> Debug for ForkserverExecutor<OT, S, SP>
where
OT: Debug,
SP: ShMemProvider,
{
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("ForkserverExecutor")
.field("target", &self.target)
.field("args", &self.args)
.field("input_file", &self.input_file)
.field("uses_shmem_testcase", &self.uses_shmem_testcase)
.field("forkserver", &self.forkserver)
.field("observers", &self.observers)
.field("map", &self.map)
.finish()
}
}
impl ForkserverExecutor<(), (), UnixShMemProvider> {
/// Builder for `ForkserverExecutor`
#[must_use]
pub fn builder() -> ForkserverExecutorBuilder<'static, UnixShMemProvider> {
ForkserverExecutorBuilder::new()
}
}
impl<OT, S, SP> ForkserverExecutor<OT, S, SP>
where
OT: ObserversTuple<S>,
S: UsesInput,
SP: ShMemProvider,
{
/// The `target` binary that's going to run.
pub fn target(&self) -> &OsString {
&self.target
}
/// The `args` used for the binary.
pub fn args(&self) -> &[OsString] {
&self.args
}
/// The [`Forkserver`] instance.
pub fn forkserver(&self) -> &Forkserver {
&self.forkserver
}
/// The [`InputFile`] used by this [`Executor`].
pub fn input_file(&self) -> &InputFile {
&self.input_file
}
/// The coverage map size if specified by the target
pub fn coverage_map_size(&self) -> Option<usize> {
self.map_size
}
}
/// The builder for `ForkserverExecutor`
#[derive(Debug)]
#[allow(clippy::struct_excessive_bools)]
pub struct ForkserverExecutorBuilder<'a, SP> {
program: Option<OsString>,
arguments: Vec<OsString>,
envs: Vec<(OsString, OsString)>,
debug_child: bool,
use_stdin: bool,
uses_shmem_testcase: bool,
is_persistent: bool,
is_deferred_frksrv: bool,
autotokens: Option<&'a mut Tokens>,
input_filename: Option<OsString>,
shmem_provider: Option<&'a mut SP>,
map_size: Option<usize>,
real_map_size: i32,
}
impl<'a, SP> ForkserverExecutorBuilder<'a, SP> {
/// Builds `ForkserverExecutor`.
/// This Forkserver will attempt to provide inputs over shared mem when `shmem_provider` is given.
/// Else this forkserver will pass the input to the target via `stdin`
/// in case no input file is specified.
/// If `debug_child` is set, the child will print to `stdout`/`stderr`.
#[allow(clippy::pedantic)]
pub fn build<OT, S>(&mut self, observers: OT) -> Result<ForkserverExecutor<OT, S, SP>, Error>
where
OT: ObserversTuple<S>,
S: UsesInput,
S::Input: Input + HasTargetBytes,
SP: ShMemProvider,
{
let (forkserver, input_file, map) = self.build_helper()?;
let target = self.program.take().unwrap();
log::info!(
"ForkserverExecutor: program: {:?}, arguments: {:?}, use_stdin: {:?}",
target,
self.arguments.clone(),
self.use_stdin
);
Ok(ForkserverExecutor {
target,
args: self.arguments.clone(),
input_file,
uses_shmem_testcase: self.uses_shmem_testcase,
forkserver,
observers,
map,
phantom: PhantomData,
has_asan_observer: None, // initialized on first use
map_size: self.map_size,
})
}
/// Builds `ForkserverExecutor` downsizing the coverage map to fit exaclty the AFL++ map size.
#[allow(clippy::pedantic)]
pub fn build_dynamic_map<MO, OT, S>(
&mut self,
mut map_observer: MO,
other_observers: OT,
) -> Result<ForkserverExecutor<(MO, OT), S, SP>, Error>
where
MO: Observer<S> + MapObserver, // TODO maybe enforce Entry = u8 for the cov map
OT: ObserversTuple<S> + Prepend<MO, PreprendResult = OT>,
S: UsesInput,
S::Input: Input + HasTargetBytes,
SP: ShMemProvider,
{
let (forkserver, input_file, map) = self.build_helper()?;
let target = self.program.take().unwrap();
log::info!(
"ForkserverExecutor: program: {:?}, arguments: {:?}, use_stdin: {:?}, map_size: {:?}",
target,
self.arguments.clone(),
self.use_stdin,
self.map_size
);
if let Some(dynamic_map_size) = self.map_size {
map_observer.downsize_map(dynamic_map_size);
}
let observers: (MO, OT) = other_observers.prepend(map_observer);
Ok(ForkserverExecutor {
target,
args: self.arguments.clone(),
input_file,
uses_shmem_testcase: self.uses_shmem_testcase,
forkserver,
observers,
map,
phantom: PhantomData,
has_asan_observer: None, // initialized on first use
map_size: self.map_size,
})
}
#[allow(clippy::pedantic)]
fn build_helper(&mut self) -> Result<(Forkserver, InputFile, Option<SP::ShMem>), Error>
where
SP: ShMemProvider,
{
let input_filename = match &self.input_filename {
Some(name) => name.clone(),
None => {
self.use_stdin = true;
OsString::from(get_unique_std_input_file())
}
};
let input_file = InputFile::create(input_filename)?;
let map = match &mut self.shmem_provider {
None => None,
Some(provider) => {
// setup shared memory
let mut shmem = provider.new_shmem(MAX_FILE + SHMEM_FUZZ_HDR_SIZE)?;
shmem.write_to_env("__AFL_SHM_FUZZ_ID")?;
let size_in_bytes = (MAX_FILE + SHMEM_FUZZ_HDR_SIZE).to_ne_bytes();
shmem.as_mut_slice()[..4].clone_from_slice(&size_in_bytes[..4]);
Some(shmem)
}
};
let mut forkserver = match &self.program {
Some(t) => Forkserver::new(
t.clone(),
self.arguments.clone(),
self.envs.clone(),
input_file.as_raw_fd(),
self.use_stdin,
0,
self.is_persistent,
self.is_deferred_frksrv,
self.debug_child,
)?,
None => {
return Err(Error::illegal_argument(
"ForkserverExecutorBuilder::build: target file not found".to_string(),
))
}
};
let (rlen, status) = forkserver.read_st()?; // Initial handshake, read 4-bytes hello message from the forkserver.
if rlen != 4 {
return Err(Error::unknown("Failed to start a forkserver".to_string()));
}
log::info!("All right - fork server is up.");
if status & FS_OPT_ENABLED == FS_OPT_ENABLED && status & FS_OPT_MAPSIZE == FS_OPT_MAPSIZE {
let mut map_size = fs_opt_get_mapsize(status);
// When 0, we assume that map_size was filled by the user or const
/* TODO autofill map size from the observer
if map_size > 0 {
self.map_size = Some(map_size as usize);
}
*/
self.real_map_size = map_size;
if map_size % 64 != 0 {
map_size = ((map_size + 63) >> 6) << 6;
}
// TODO set AFL_MAP_SIZE
assert!(self.map_size.is_none() || map_size as usize <= self.map_size.unwrap());
self.map_size = Some(map_size as usize);
}
// Only with SHMEM or AUTODICT we can send send_status back or it breaks!
// If forkserver is responding, we then check if there's any option enabled.
// We'll send 4-bytes message back to the forkserver to tell which features to use
// The forkserver is listening to our response if either shmem fuzzing is enabled or auto dict is enabled
// <https://github.com/AFLplusplus/AFLplusplus/blob/147654f8715d237fe45c1657c87b2fe36c4db22a/instrumentation/afl-compiler-rt.o.c#L1026>
if status & FS_OPT_ENABLED == FS_OPT_ENABLED
&& (status & FS_OPT_SHDMEM_FUZZ == FS_OPT_SHDMEM_FUZZ
|| status & FS_OPT_AUTODICT == FS_OPT_AUTODICT)
{
let mut send_status = FS_OPT_ENABLED;
if (status & FS_OPT_SHDMEM_FUZZ == FS_OPT_SHDMEM_FUZZ) && map.is_some() {
log::info!("Using SHARED MEMORY FUZZING feature.");
send_status |= FS_OPT_SHDMEM_FUZZ;
self.uses_shmem_testcase = true;
}
if (status & FS_OPT_AUTODICT == FS_OPT_AUTODICT) && self.autotokens.is_some() {
log::info!("Using AUTODICT feature");
send_status |= FS_OPT_AUTODICT;
}
if send_status != FS_OPT_ENABLED {
// if send_status is not changed (Options are available but we didn't use any), then don't send the next write_ctl message.
// This is important
let send_len = forkserver.write_ctl(send_status)?;
if send_len != 4 {
return Err(Error::unknown("Writing to forkserver failed.".to_string()));
}
if (send_status & FS_OPT_AUTODICT) == FS_OPT_AUTODICT {
let (read_len, dict_size) = forkserver.read_st()?;
if read_len != 4 {
return Err(Error::unknown(
"Reading from forkserver failed.".to_string(),
));
}
if !(2..=0xffffff).contains(&dict_size) {
return Err(Error::illegal_state(
"Dictionary has an illegal size".to_string(),
));
}
log::info!("Autodict size {dict_size:x}");
let (rlen, buf) = forkserver.read_st_size(dict_size as usize)?;
if rlen != dict_size as usize {
return Err(Error::unknown("Failed to load autodictionary".to_string()));
}
if let Some(t) = &mut self.autotokens {
t.parse_autodict(&buf, dict_size as usize);
}
}
}
} else {
log::warn!("Forkserver Options are not available.");
}
Ok((forkserver, input_file, map))
}
/// Use autodict?
#[must_use]
pub fn autotokens(mut self, tokens: &'a mut Tokens) -> Self {
self.autotokens = Some(tokens);
self
}
#[must_use]
/// Parse afl style command line
///
/// Replaces `@@` with the path to the input file generated by the fuzzer. If `@@` is omitted,
/// `stdin` is used to pass the test case instead.
///
/// Interprets the first argument as the path to the program as long as it is not set yet.
/// You have to omit the program path in case you have set it already. Otherwise
/// it will be interpreted as a regular argument, leading to probably unintended results.
pub fn parse_afl_cmdline<IT, O>(self, args: IT) -> Self
where
IT: IntoIterator<Item = O>,
O: AsRef<OsStr>,
{
let mut moved = self;
let mut use_arg_0_as_program = false;
if moved.program.is_none() {
use_arg_0_as_program = true;
}
for item in args {
if use_arg_0_as_program {
moved = moved.program(item);
// After the program has been set, unset `use_arg_0_as_program` to treat all
// subsequent arguments as regular arguments
use_arg_0_as_program = false;
} else if item.as_ref() == "@@" {
if let Some(name) = &moved.input_filename.clone() {
// If the input file name has been modified, use this one
moved = moved.arg_input_file(name);
} else {
moved = moved.arg_input_file_std();
}
} else {
moved = moved.arg(item);
}
}
// If we have not set an input file, use stdin as it is AFLs default
moved.use_stdin = moved.input_filename.is_none();
moved
}
/// The harness
#[must_use]
pub fn program<O>(mut self, program: O) -> Self
where
O: AsRef<OsStr>,
{
self.program = Some(program.as_ref().to_owned());
self
}
/// Adds an argument to the harness's commandline
#[must_use]
pub fn arg<O>(mut self, arg: O) -> Self
where
O: AsRef<OsStr>,
{
self.arguments.push(arg.as_ref().to_owned());
self
}
/// Adds arguments to the harness's commandline
#[must_use]
pub fn args<IT, O>(mut self, args: IT) -> Self
where
IT: IntoIterator<Item = O>,
O: AsRef<OsStr>,
{
let mut res = vec![];
for arg in args {
res.push(arg.as_ref().to_owned());
}
self.arguments.append(&mut res);
self
}
/// Adds an environmental var to the harness's commandline
#[must_use]
pub fn env<K, V>(mut self, key: K, val: V) -> Self
where
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.envs
.push((key.as_ref().to_owned(), val.as_ref().to_owned()));
self
}
/// Adds environmental vars to the harness's commandline
#[must_use]
pub fn envs<IT, K, V>(mut self, vars: IT) -> Self
where
IT: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
let mut res = vec![];
for (ref key, ref val) in vars {
res.push((key.as_ref().to_owned(), val.as_ref().to_owned()));
}
self.envs.append(&mut res);
self
}
#[must_use]
/// Place the input at this position and set the filename for the input.
///
/// Note: If you use this, you should ensure that there is only one instance using this
/// file at any given time.
pub fn arg_input_file<P: AsRef<Path>>(self, path: P) -> Self {
let mut moved = self.arg(path.as_ref());
let path_as_string = path.as_ref().as_os_str().to_os_string();
assert!(
// It's only save to set the input_filename, if it does not overwrite an existing one.
(moved.input_filename.is_none() || moved.input_filename.unwrap() == path_as_string),
"Already specified an input file under a different name. This is not supported"
);
moved.input_filename = Some(path_as_string);
moved
}
#[must_use]
/// Place the input at this position and set the default filename for the input.
/// The filename includes the PID of the fuzzer to ensure that no two fuzzers write to the same file
pub fn arg_input_file_std(self) -> Self {
self.arg_input_file(get_unique_std_input_file())
}
#[must_use]
/// If `debug_child` is set, the child will print to `stdout`/`stderr`.
pub fn debug_child(mut self, debug_child: bool) -> Self {
self.debug_child = debug_child;
self
}
#[must_use]
/// Call this if you want to run it under persistent mode; default is false
pub fn is_persistent(mut self, is_persistent: bool) -> Self {
self.is_persistent = is_persistent;
self
}
#[must_use]
/// Call this if the harness uses deferred forkserver mode; default is false
pub fn is_deferred_frksrv(mut self, is_deferred_frksrv: bool) -> Self {
self.is_deferred_frksrv = is_deferred_frksrv;
self
}
#[must_use]
/// Call this to set a defauult const coverage map size
pub fn coverage_map_size(mut self, size: usize) -> Self {
self.map_size = Some(size);
self
}
}
impl<'a> ForkserverExecutorBuilder<'a, UnixShMemProvider> {
/// Creates a new `AFL`-style [`ForkserverExecutor`] with the given target, arguments and observers.
/// This is the builder for `ForkserverExecutor`
/// This Forkserver will attempt to provide inputs over shared mem when `shmem_provider` is given.
/// Else this forkserver will pass the input to the target via `stdin`
/// in case no input file is specified.
/// If `debug_child` is set, the child will print to `stdout`/`stderr`.
#[must_use]
pub fn new() -> ForkserverExecutorBuilder<'a, UnixShMemProvider> {
ForkserverExecutorBuilder {
program: None,
arguments: vec![],
envs: vec![],
debug_child: false,
use_stdin: false,
uses_shmem_testcase: false,
is_persistent: false,
is_deferred_frksrv: false,
autotokens: None,
input_filename: None,
shmem_provider: None,
map_size: None,
real_map_size: 0,
}
}
/// Shmem provider for forkserver's shared memory testcase feature.
pub fn shmem_provider<SP: ShMemProvider>(
self,
shmem_provider: &'a mut SP,
) -> ForkserverExecutorBuilder<'a, SP> {
ForkserverExecutorBuilder {
program: self.program,
arguments: self.arguments,
envs: self.envs,
debug_child: self.debug_child,
use_stdin: self.use_stdin,
uses_shmem_testcase: self.uses_shmem_testcase,
is_persistent: self.is_persistent,
is_deferred_frksrv: self.is_deferred_frksrv,
autotokens: self.autotokens,
input_filename: self.input_filename,
shmem_provider: Some(shmem_provider),
map_size: self.map_size,
real_map_size: self.real_map_size,
}
}
}
impl<'a> Default for ForkserverExecutorBuilder<'a, UnixShMemProvider> {
fn default() -> Self {
Self::new()
}
}
impl<EM, OT, S, SP, Z> Executor<EM, Z> for ForkserverExecutor<OT, S, SP>
where
OT: ObserversTuple<S>,
SP: ShMemProvider,
S: UsesInput,
S::Input: HasTargetBytes,
EM: UsesState<State = S>,
Z: UsesState<State = S>,
{
#[inline]
fn run_target(
&mut self,
_fuzzer: &mut Z,
_state: &mut Self::State,
_mgr: &mut EM,
input: &Self::Input,
) -> Result<ExitKind, Error> {
let mut exit_kind = ExitKind::Ok;
// Write to testcase
if self.uses_shmem_testcase {
let map = unsafe { self.map.as_mut().unwrap_unchecked() };
let target_bytes = input.target_bytes();
let mut size = target_bytes.as_slice().len();
if size > MAX_FILE {
// Truncate like AFL++ does
size = MAX_FILE;
}
let size_in_bytes = size.to_ne_bytes();
// The first four bytes tells the size of the shmem.
map.as_mut_slice()[..SHMEM_FUZZ_HDR_SIZE]
.copy_from_slice(&size_in_bytes[..SHMEM_FUZZ_HDR_SIZE]);
map.as_mut_slice()[SHMEM_FUZZ_HDR_SIZE..(SHMEM_FUZZ_HDR_SIZE + size)]
.copy_from_slice(target_bytes.as_slice());
} else {
self.input_file.write_buf(input.target_bytes().as_slice())?;
}
// Don't tell the forkserver to spawn a new process before clearing the cov map
compiler_fence(Ordering::SeqCst);
let send_len = self
.forkserver
.write_ctl(self.forkserver().last_run_timed_out())?;
if send_len != 4 {
return Err(Error::illegal_state(
"Unable to request new process from fork server (OOM?)".to_string(),
));
}
let (recv_pid_len, pid) = self.forkserver.read_st()?;
if recv_pid_len != 4 {
return Err(Error::illegal_state(
"Unable to request new process from fork server (OOM?)".to_string(),
));
}
if pid <= 0 {
return Err(Error::unknown(
"Fork server is misbehaving (OOM?)".to_string(),
));
}
self.forkserver.set_child_pid(Pid::from_raw(pid));
let (recv_status_len, status) = self.forkserver.read_st()?;
if recv_status_len != 4 {
return Err(Error::unknown(
"Unable to communicate with fork server (OOM?)".to_string(),
));
}
self.forkserver.set_status(status);
if libc::WIFSIGNALED(self.forkserver.status()) {
exit_kind = ExitKind::Crash;
#[cfg(feature = "regex")]
if self.has_asan_observer.is_none() {
self.has_asan_observer = Some(
self.observers()
.match_name::<AsanBacktraceObserver>("AsanBacktraceObserver")
.is_some(),
);
}
#[cfg(feature = "regex")]
if self.has_asan_observer.unwrap() {
self.observers_mut()
.match_name_mut::<AsanBacktraceObserver>("AsanBacktraceObserver")
.unwrap()
.parse_asan_output_from_asan_log_file(pid)?;
}
}
self.forkserver.set_child_pid(Pid::from_raw(0));
// Clear the observer map after the execution is finished
compiler_fence(Ordering::SeqCst);
Ok(exit_kind)
}
}
impl<OT, S, SP> UsesState for ForkserverExecutor<OT, S, SP>
where
S: UsesInput,
SP: ShMemProvider,
{
type State = S;
}
impl<OT, S, SP> UsesObservers for ForkserverExecutor<OT, S, SP>
where
OT: ObserversTuple<S>,
S: UsesInput,
SP: ShMemProvider,
{
type Observers = OT;
}
impl<OT, S, SP> HasObservers for ForkserverExecutor<OT, S, SP>
where
OT: ObserversTuple<S>,
S: UsesInput,
SP: ShMemProvider,
{
#[inline]
fn observers(&self) -> &OT {
&self.observers
}
#[inline]
fn observers_mut(&mut self) -> &mut OT {
&mut self.observers
}
}
impl<OT, S, SP> HasForkserver for ForkserverExecutor<OT, S, SP>
where
OT: ObserversTuple<S>,
S: UsesInput,
S::Input: Input + HasTargetBytes,
SP: ShMemProvider,
{
type SP = SP;
#[inline]
fn forkserver(&self) -> &Forkserver {
&self.forkserver
}
#[inline]
fn forkserver_mut(&mut self) -> &mut Forkserver {
&mut self.forkserver
}
#[inline]
fn input_file(&self) -> &InputFile {
&self.input_file
}
#[inline]
fn input_file_mut(&mut self) -> &mut InputFile {
&mut self.input_file
}
#[inline]
fn shmem(&self) -> &Option<SP::ShMem> {
&self.map
}
#[inline]
fn shmem_mut(&mut self) -> &mut Option<SP::ShMem> {
&mut self.map
}
#[inline]
fn uses_shmem_testcase(&self) -> bool {
self.uses_shmem_testcase
}
}
impl<E> UsesState for TimeoutForkserverExecutor<E>
where
E: UsesState,
{
type State = E::State;
}
impl<E> UsesObservers for TimeoutForkserverExecutor<E>
where
E: UsesObservers,
{
type Observers = E::Observers;
}
impl<E> HasObservers for TimeoutForkserverExecutor<E>
where
E: HasObservers,
{
#[inline]
fn observers(&self) -> &Self::Observers {
self.executor.observers()
}
#[inline]
fn observers_mut(&mut self) -> &mut Self::Observers {
self.executor.observers_mut()
}
}
#[cfg(test)]
mod tests {
use std::ffi::OsString;
use serial_test::serial;
use crate::{
bolts::{
shmem::{ShMem, ShMemProvider, UnixShMemProvider},
tuples::tuple_list,
AsMutSlice,
},
executors::forkserver::ForkserverExecutorBuilder,
observers::{ConstMapObserver, HitcountsMapObserver},
Error,
};
#[test]
#[serial]
#[cfg_attr(miri, ignore)]
fn test_forkserver() {
const MAP_SIZE: usize = 65536;
let bin = OsString::from("echo");
let args = vec![OsString::from("@@")];
let mut shmem_provider = UnixShMemProvider::new().unwrap();
let mut shmem = shmem_provider.new_shmem(MAP_SIZE).unwrap();
shmem.write_to_env("__AFL_SHM_ID").unwrap();
let shmem_buf = shmem.as_mut_slice();
let edges_observer = HitcountsMapObserver::new(ConstMapObserver::<_, MAP_SIZE>::new(
"shared_mem",
shmem_buf,
));
let executor = ForkserverExecutorBuilder::new()
.program(bin)
.args(args)
.debug_child(false)
.shmem_provider(&mut shmem_provider)
.build::<_, ()>(tuple_list!(edges_observer));
// Since /usr/bin/echo is not a instrumented binary file, the test will just check if the forkserver has failed at the initial handshake
let result = match executor {
Ok(_) => true,
Err(e) => match e {
Error::Unknown(s, _) => s == "Failed to start a forkserver",
_ => false,
},
};
assert!(result);
}
}