diff --git a/libafl/src/executors/forkserver.rs b/libafl/src/executors/forkserver.rs index 606be4c635..0e690bc821 100644 --- a/libafl/src/executors/forkserver.rs +++ b/libafl/src/executors/forkserver.rs @@ -1,9 +1,9 @@ //! 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::{ fs::{File, OpenOptions}, - io::{self, prelude::*, SeekFrom}, + io::{self, prelude::*, ErrorKind, SeekFrom}, os::unix::{ io::{AsRawFd, RawFd}, process::CommandExt, @@ -11,13 +11,21 @@ use std::{ process::{Command, Stdio}, }; -use crate::bolts::os::{dup2, pipes::Pipe}; use crate::{ + bolts::os::{dup2, pipes::Pipe}, executors::{Executor, ExitKind, HasExecHooksTuple, HasObservers, HasObserversHooks}, inputs::{HasTargetBytes, Input}, observers::ObserversTuple, Error, }; +use nix::{ + sys::{ + select::{select, FdSet}, + signal::{kill, Signal}, + time::{TimeVal, TimeValLike}, + }, + unistd::Pid, +}; const FORKSRV_FD: i32 = 198; @@ -162,7 +170,7 @@ impl OutFile { pub struct Forkserver { st_pipe: Pipe, ctl_pipe: Pipe, - child_pid: u32, + child_pid: Pid, status: i32, last_run_timed_out: i32, } @@ -210,7 +218,7 @@ impl Forkserver { Ok(Self { st_pipe, ctl_pipe, - child_pid: 0, + child_pid: Pid::from_raw(0), status: 0, last_run_timed_out: 0, }) @@ -221,17 +229,29 @@ impl Forkserver { 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] pub fn status(&self) -> i32 { self.status } + pub fn set_status(&mut self, status: i32) { + self.status = status; + } + #[must_use] - pub fn child_pid(&self) -> u32 { + pub fn child_pid(&self) -> 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 rlen = self.st_pipe.read(&mut buf)?; @@ -240,11 +260,150 @@ impl Forkserver { Ok((rlen, val)) } - pub fn write_ctl(&mut self, val: i32) -> Result { + pub fn write_ctl(&mut self, val: i32) -> Result { let slen = self.ctl_pipe.write(&val.to_ne_bytes())?; Ok(slen) } + + pub fn read_st_timed(&mut self, timeout: &mut TimeVal) -> Result, 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 { + executor: E, + timeout: TimeVal, +} + +impl TimeoutForkserverExecutor { + pub fn new(executor: E, exec_tmout: Duration) -> Result { + let milli_sec = exec_tmout.as_millis() as i64; + let timeout = TimeVal::milliseconds(milli_sec); + Ok(Self { executor, timeout }) + } +} + +impl Executor for TimeoutForkserverExecutor +where + I: Input + HasTargetBytes, + E: Executor + HasForkserver, +{ + #[inline] + fn run_target( + &mut self, + _fuzzer: &mut Z, + _state: &mut S, + _mgr: &mut EM, + input: &I, + ) -> Result { + 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. @@ -324,6 +483,10 @@ where pub fn forkserver(&self) -> &Forkserver { &self.forkserver } + + pub fn out_file(&self) -> &OutFile { + &self.out_file + } } impl Executor for ForkserverExecutor @@ -353,8 +516,8 @@ where )); } - let (recv_len, pid) = self.forkserver.read_st()?; - if recv_len != 4 { + let (recv_pid_len, pid) = self.forkserver.read_st()?; + if recv_pid_len != 4 { return Err(Error::Forkserver( "Unable to request new process from fork server (OOM?)".to_string(), )); @@ -366,17 +529,23 @@ where )); } - let (_, status) = self.forkserver.read_st()?; - self.forkserver.status = status; + self.forkserver.set_child_pid(Pid::from_raw(pid)); - if !libc::WIFSTOPPED(self.forkserver.status()) { - self.forkserver.child_pid = 0; + let (recv_status_len, status) = self.forkserver.read_st()?; + 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()) { exit_kind = ExitKind::Crash; } + self.forkserver.set_child_pid(Pid::from_raw(0)); + Ok(exit_kind) } } @@ -404,9 +573,58 @@ where { } +impl HasForkserver for ForkserverExecutor +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 HasObservers for TimeoutForkserverExecutor +where + E: HasObservers, + OT: ObserversTuple, +{ + #[inline] + fn observers(&self) -> &OT { + self.executor.observers() + } + + #[inline] + fn observers_mut(&mut self) -> &mut OT { + self.executor.observers_mut() + } +} + +impl HasObserversHooks for TimeoutForkserverExecutor +where + E: HasObservers, + I: Input, + OT: ObserversTuple + HasExecHooksTuple, +{ +} + #[cfg(test)] mod tests { - use crate::{ bolts::{ shmem::{ShMem, ShMemProvider, StdShMemProvider}, @@ -441,6 +659,7 @@ mod tests { 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 { diff --git a/libafl/src/executors/mod.rs b/libafl/src/executors/mod.rs index 1d62dab5b9..25bcaf3c89 100644 --- a/libafl/src/executors/mod.rs +++ b/libafl/src/executors/mod.rs @@ -8,7 +8,7 @@ pub use timeout::TimeoutExecutor; #[cfg(all(feature = "std", unix))] pub mod forkserver; #[cfg(all(feature = "std", unix))] -pub use forkserver::{Forkserver, ForkserverExecutor, OutFile}; +pub use forkserver::{Forkserver, ForkserverExecutor, OutFile, TimeoutForkserverExecutor}; pub mod combined; pub use combined::CombinedExecutor; diff --git a/libafl/src/executors/timeout.rs b/libafl/src/executors/timeout.rs index cfea299c4f..9986e12773 100644 --- a/libafl/src/executors/timeout.rs +++ b/libafl/src/executors/timeout.rs @@ -36,7 +36,7 @@ extern "C" { #[cfg(unix)] 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 { executor: E, #[cfg(unix)]