Timeout forkserver (#136)

* barebones for TimeoutForkserverExecutor

* TimeoutForkserverExecutor

* update pid in forkserverexecutor

* clppy and other small fixes

* doc

* fix

* no unwrap

* read_exact and error handling

* fix

* semicolon
This commit is contained in:
Toka 2021-05-31 06:11:10 +09:00 committed by GitHub
parent 3e51981cf6
commit e68eaf8244
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 236 additions and 17 deletions

View File

@ -1,9 +1,9 @@
//! Expose an `Executor` based on a `Forkserver` in order to execute AFL/AFL++ binaries //! Expose an `Executor` based on a `Forkserver` in order to execute AFL/AFL++ binaries
use core::marker::PhantomData; use core::{marker::PhantomData, time::Duration};
use std::{ use std::{
fs::{File, OpenOptions}, fs::{File, OpenOptions},
io::{self, prelude::*, SeekFrom}, io::{self, prelude::*, ErrorKind, SeekFrom},
os::unix::{ os::unix::{
io::{AsRawFd, RawFd}, io::{AsRawFd, RawFd},
process::CommandExt, process::CommandExt,
@ -11,13 +11,21 @@ use std::{
process::{Command, Stdio}, process::{Command, Stdio},
}; };
use crate::bolts::os::{dup2, pipes::Pipe};
use crate::{ use crate::{
bolts::os::{dup2, pipes::Pipe},
executors::{Executor, ExitKind, HasExecHooksTuple, HasObservers, HasObserversHooks}, executors::{Executor, ExitKind, HasExecHooksTuple, HasObservers, HasObserversHooks},
inputs::{HasTargetBytes, Input}, inputs::{HasTargetBytes, Input},
observers::ObserversTuple, observers::ObserversTuple,
Error, Error,
}; };
use nix::{
sys::{
select::{select, FdSet},
signal::{kill, Signal},
time::{TimeVal, TimeValLike},
},
unistd::Pid,
};
const FORKSRV_FD: i32 = 198; const FORKSRV_FD: i32 = 198;
@ -162,7 +170,7 @@ impl OutFile {
pub struct Forkserver { pub struct Forkserver {
st_pipe: Pipe, st_pipe: Pipe,
ctl_pipe: Pipe, ctl_pipe: Pipe,
child_pid: u32, child_pid: Pid,
status: i32, status: i32,
last_run_timed_out: i32, last_run_timed_out: i32,
} }
@ -210,7 +218,7 @@ impl Forkserver {
Ok(Self { Ok(Self {
st_pipe, st_pipe,
ctl_pipe, ctl_pipe,
child_pid: 0, child_pid: Pid::from_raw(0),
status: 0, status: 0,
last_run_timed_out: 0, last_run_timed_out: 0,
}) })
@ -221,17 +229,29 @@ impl Forkserver {
self.last_run_timed_out self.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;
}
#[must_use] #[must_use]
pub fn status(&self) -> i32 { pub fn status(&self) -> i32 {
self.status self.status
} }
pub fn set_status(&mut self, status: i32) {
self.status = status;
}
#[must_use] #[must_use]
pub fn child_pid(&self) -> u32 { pub fn child_pid(&self) -> Pid {
self.child_pid self.child_pid
} }
pub fn read_st(&mut self) -> Result<(usize, i32), io::Error> { pub fn set_child_pid(&mut self, child_pid: Pid) {
self.child_pid = child_pid;
}
pub fn read_st(&mut self) -> Result<(usize, i32), Error> {
let mut buf: [u8; 4] = [0u8; 4]; let mut buf: [u8; 4] = [0u8; 4];
let rlen = self.st_pipe.read(&mut buf)?; let rlen = self.st_pipe.read(&mut buf)?;
@ -240,11 +260,150 @@ impl Forkserver {
Ok((rlen, val)) Ok((rlen, val))
} }
pub fn write_ctl(&mut self, val: i32) -> Result<usize, io::Error> { pub fn write_ctl(&mut self, val: i32) -> Result<usize, Error> {
let slen = self.ctl_pipe.write(&val.to_ne_bytes())?; let slen = self.ctl_pipe.write(&val.to_ne_bytes())?;
Ok(slen) Ok(slen)
} }
pub fn read_st_timed(&mut self, timeout: &mut TimeVal) -> Result<Option<i32>, Error> {
let mut buf: [u8; 4] = [0u8; 4];
let st_read = match self.st_pipe.read_end() {
Some(fd) => fd,
None => {
return Err(Error::File(io::Error::new(
ErrorKind::BrokenPipe,
"Read pipe end was already closed",
)));
}
};
let mut readfds = FdSet::new();
let mut copy = *timeout;
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 = select(
Some(readfds.highest().unwrap() + 1),
&mut readfds,
None,
None,
&mut copy,
)?;
if sret > 0 {
if let Err(_) = self.st_pipe.read_exact(&mut buf) {
return Err(Error::Forkserver(
"Unable to communicate with fork server (OOM?)".to_string(),
));
}
let val: i32 = i32::from_ne_bytes(buf);
Ok(Some(val))
} else {
Ok(None)
}
}
}
pub trait HasForkserver {
fn forkserver(&self) -> &Forkserver;
fn forkserver_mut(&mut self) -> &mut Forkserver;
fn out_file(&self) -> &OutFile;
fn out_file_mut(&mut self) -> &mut OutFile;
}
/// The timeout forkserver executor that wraps around the standard forkserver executor and sets a timeout before each run.
pub struct TimeoutForkserverExecutor<E> {
executor: E,
timeout: TimeVal,
}
impl<E> TimeoutForkserverExecutor<E> {
pub fn new(executor: E, exec_tmout: Duration) -> Result<Self, Error> {
let milli_sec = exec_tmout.as_millis() as i64;
let timeout = TimeVal::milliseconds(milli_sec);
Ok(Self { executor, timeout })
}
}
impl<E, EM, I, S, Z> Executor<EM, I, S, Z> for TimeoutForkserverExecutor<E>
where
I: Input + HasTargetBytes,
E: Executor<EM, I, S, Z> + HasForkserver,
{
#[inline]
fn run_target(
&mut self,
_fuzzer: &mut Z,
_state: &mut S,
_mgr: &mut EM,
input: &I,
) -> Result<ExitKind, Error> {
let mut exit_kind = ExitKind::Ok;
let last_run_timed_out = self.executor.forkserver().last_run_timed_out();
self.executor
.out_file_mut()
.write_buf(&input.target_bytes().as_slice());
let send_len = self
.executor
.forkserver_mut()
.write_ctl(last_run_timed_out)?;
if send_len != 4 {
return Err(Error::Forkserver(
"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::Forkserver(
"Unable to request new process from fork server (OOM?)".to_string(),
));
}
if pid <= 0 {
return Err(Error::Forkserver(
"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(&mut 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()?
kill(self.executor.forkserver().child_pid(), Signal::SIGKILL)?;
let (recv_status_len, exit_code) = self.executor.forkserver_mut().read_st()?;
if recv_status_len != 4 || exit_code != 9 {
return Err(Error::Forkserver(
"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. /// This [`Executor`] can run binaries compiled for AFL/AFL++ that make use of a forkserver.
@ -324,6 +483,10 @@ where
pub fn forkserver(&self) -> &Forkserver { pub fn forkserver(&self) -> &Forkserver {
&self.forkserver &self.forkserver
} }
pub fn out_file(&self) -> &OutFile {
&self.out_file
}
} }
impl<EM, I, OT, S, Z> Executor<EM, I, S, Z> for ForkserverExecutor<I, OT> impl<EM, I, OT, S, Z> Executor<EM, I, S, Z> for ForkserverExecutor<I, OT>
@ -353,8 +516,8 @@ where
)); ));
} }
let (recv_len, pid) = self.forkserver.read_st()?; let (recv_pid_len, pid) = self.forkserver.read_st()?;
if recv_len != 4 { if recv_pid_len != 4 {
return Err(Error::Forkserver( return Err(Error::Forkserver(
"Unable to request new process from fork server (OOM?)".to_string(), "Unable to request new process from fork server (OOM?)".to_string(),
)); ));
@ -366,17 +529,23 @@ where
)); ));
} }
let (_, status) = self.forkserver.read_st()?; self.forkserver.set_child_pid(Pid::from_raw(pid));
self.forkserver.status = status;
if !libc::WIFSTOPPED(self.forkserver.status()) { let (recv_status_len, status) = self.forkserver.read_st()?;
self.forkserver.child_pid = 0; if recv_status_len != 4 {
return Err(Error::Forkserver(
"Unable to communicate with fork server (OOM?)".to_string(),
));
} }
self.forkserver.set_status(status);
if libc::WIFSIGNALED(self.forkserver.status()) { if libc::WIFSIGNALED(self.forkserver.status()) {
exit_kind = ExitKind::Crash; exit_kind = ExitKind::Crash;
} }
self.forkserver.set_child_pid(Pid::from_raw(0));
Ok(exit_kind) Ok(exit_kind)
} }
} }
@ -404,9 +573,58 @@ where
{ {
} }
impl<I, OT> HasForkserver for ForkserverExecutor<I, OT>
where
I: Input + HasTargetBytes,
OT: ObserversTuple,
{
#[inline]
fn forkserver(&self) -> &Forkserver {
&self.forkserver
}
#[inline]
fn forkserver_mut(&mut self) -> &mut Forkserver {
&mut self.forkserver
}
#[inline]
fn out_file(&self) -> &OutFile {
&self.out_file
}
#[inline]
fn out_file_mut(&mut self) -> &mut OutFile {
&mut self.out_file
}
}
impl<E, OT> HasObservers<OT> for TimeoutForkserverExecutor<E>
where
E: HasObservers<OT>,
OT: ObserversTuple,
{
#[inline]
fn observers(&self) -> &OT {
self.executor.observers()
}
#[inline]
fn observers_mut(&mut self) -> &mut OT {
self.executor.observers_mut()
}
}
impl<E, EM, I, OT, S, Z> HasObserversHooks<EM, I, OT, S, Z> for TimeoutForkserverExecutor<E>
where
E: HasObservers<OT>,
I: Input,
OT: ObserversTuple + HasExecHooksTuple<EM, I, S, Z>,
{
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{ use crate::{
bolts::{ bolts::{
shmem::{ShMem, ShMemProvider, StdShMemProvider}, shmem::{ShMem, ShMemProvider, StdShMemProvider},
@ -441,6 +659,7 @@ mod tests {
tuple_list!(edges_observer), 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 // 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 { let result = match executor {
Ok(_) => true, Ok(_) => true,
Err(e) => match e { Err(e) => match e {

View File

@ -8,7 +8,7 @@ pub use timeout::TimeoutExecutor;
#[cfg(all(feature = "std", unix))] #[cfg(all(feature = "std", unix))]
pub mod forkserver; pub mod forkserver;
#[cfg(all(feature = "std", unix))] #[cfg(all(feature = "std", unix))]
pub use forkserver::{Forkserver, ForkserverExecutor, OutFile}; pub use forkserver::{Forkserver, ForkserverExecutor, OutFile, TimeoutForkserverExecutor};
pub mod combined; pub mod combined;
pub use combined::CombinedExecutor; pub use combined::CombinedExecutor;

View File

@ -36,7 +36,7 @@ extern "C" {
#[cfg(unix)] #[cfg(unix)]
const ITIMER_REAL: c_int = 0; const ITIMER_REAL: c_int = 0;
/// The timeout excutor is a wrapper that set a timeout before each run /// The timeout excutor is a wrapper that sets a timeout before each run
pub struct TimeoutExecutor<E> { pub struct TimeoutExecutor<E> {
executor: E, executor: E,
#[cfg(unix)] #[cfg(unix)]