2022-12-11 00:59:59 +01:00

703 lines
22 KiB
Rust

//! The command executor executes a sub program for each run
use alloc::vec::Vec;
use core::{
fmt::{self, Debug, Formatter},
marker::PhantomData,
};
#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
#[cfg(feature = "std")]
use std::process::Child;
#[cfg(all(feature = "std", unix))]
use std::time::Duration;
use std::{
ffi::{OsStr, OsString},
io::{Read, Write},
path::{Path, PathBuf},
process::{Command, Stdio},
};
use super::HasObservers;
#[cfg(all(feature = "std", unix))]
use crate::executors::{Executor, ExitKind};
use crate::{
bolts::{
fs::{InputFile, INPUTFILE_STD},
tuples::MatchName,
AsSlice,
},
inputs::{HasTargetBytes, UsesInput},
observers::{ObserversTuple, UsesObservers},
state::UsesState,
std::borrow::ToOwned,
};
#[cfg(feature = "std")]
use crate::{inputs::Input, Error};
/// How to deliver input to an external program
/// `StdIn`: The target reads from stdin
/// `File`: The target reads from the specified [`InputFile`]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InputLocation {
/// Mutate a commandline argument to deliver an input
Arg {
/// The offset of the argument to mutate
argnum: usize,
},
/// Deliver input via `StdIn`
StdIn,
/// Deliver the input via the specified [`InputFile`]
/// You can use specify [`InputFile::create(INPUTFILE_STD)`] to use a default filename.
File {
/// The file to write input to. The target should read input from this location.
out_file: InputFile,
},
}
/// Clones a [`Command`] (without stdio and stdout/stderr - they are not accesible)
fn clone_command(cmd: &Command) -> Command {
let mut new_cmd = Command::new(cmd.get_program());
new_cmd.args(cmd.get_args());
new_cmd.env_clear();
new_cmd.envs(
cmd.get_envs()
.filter_map(|(key, value)| value.map(|value| (key, value))),
);
if let Some(cwd) = cmd.get_current_dir() {
new_cmd.current_dir(cwd);
}
new_cmd
}
/// A simple Configurator that takes the most common parameters
/// Writes the input either to stdio or to a file
/// Use [`CommandExecutor::builder()`] to use this configurator.
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug)]
pub struct StdCommandConfigurator {
/// If set to true, the child output will remain visible
/// By default, the child output is hidden to increase execution speed
debug_child: bool,
has_stdout_observer: bool,
has_stderr_observer: bool,
/// true: input gets delivered via stdink
input_location: InputLocation,
/// The Command to execute
command: Command,
}
impl CommandConfigurator for StdCommandConfigurator {
fn spawn_child<I>(&mut self, input: &I) -> Result<Child, Error>
where
I: Input + HasTargetBytes,
{
match &mut self.input_location {
InputLocation::Arg { argnum } => {
let args = self.command.get_args();
let mut cmd = Command::new(self.command.get_program());
if !self.debug_child {
cmd.stdout(Stdio::null());
cmd.stderr(Stdio::null());
}
if self.has_stdout_observer {
cmd.stdout(Stdio::piped());
}
if self.has_stderr_observer {
cmd.stderr(Stdio::piped());
}
for (i, arg) in args.enumerate() {
if i == *argnum {
debug_assert_eq!(arg, "DUMMY");
#[cfg(unix)]
cmd.arg(OsStr::from_bytes(input.target_bytes().as_slice()));
// There is an issue here that the chars on windows are 16 bit wide.
// I can't really test it. Please open a PR if this goes wrong.
#[cfg(not(unix))]
cmd.arg(OsString::from_vec(input.target_bytes().as_vec()));
} else {
cmd.arg(arg);
}
}
cmd.envs(
self.command
.get_envs()
.filter_map(|(key, value)| value.map(|value| (key, value))),
);
if let Some(cwd) = self.command.get_current_dir() {
cmd.current_dir(cwd);
}
Ok(cmd.spawn()?)
}
InputLocation::StdIn => {
self.command.stdin(Stdio::piped()).spawn()?;
let mut handle = self.command.spawn()?;
let mut stdin = handle.stdin.take().unwrap();
stdin.write_all(input.target_bytes().as_slice())?;
stdin.flush()?;
drop(stdin);
Ok(handle)
}
InputLocation::File { out_file } => {
out_file.write_buf(input.target_bytes().as_slice())?;
Ok(self.command.spawn()?)
}
}
}
}
/// A `CommandExecutor` is a wrapper around [`std::process::Command`] to execute a target as a child process.
/// Construct a `CommandExecutor` by implementing [`CommandConfigurator`] for a type of your choice and calling [`CommandConfigurator::into_executor`] on it.
/// Instead, you can use [`CommandExecutor::builder()`] to construct a [`CommandExecutor`] backed by a [`StdCommandConfigurator`].
pub struct CommandExecutor<EM, OT, S, T, Z> {
/// The wrapped command configurer
configurer: T,
/// The observers used by this executor
observers: OT,
phantom: PhantomData<(EM, S, Z)>,
}
impl CommandExecutor<(), (), (), (), ()> {
/// Creates a builder for a new [`CommandExecutor`],
/// backed by a [`StdCommandConfigurator`]
/// This is usually the easiest way to construct a [`CommandExecutor`].
///
/// It mimics the api of [`Command`], specifically, you will use
/// `arg`, `args`, `env`, and so on.
///
/// By default, input is read from stdin, unless you specify a different location using
/// * `arg_input_arg` for input delivered _as_ an command line argument
/// * `arg_input_file` for input via a file of a specific name
/// * `arg_input_file_std` for a file with default name
/// (at the right location in the arguments)
#[must_use]
pub fn builder() -> CommandExecutorBuilder {
CommandExecutorBuilder::new()
}
}
impl<EM, OT, S, T, Z> Debug for CommandExecutor<EM, OT, S, T, Z>
where
T: Debug,
OT: Debug,
{
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("CommandExecutor")
.field("inner", &self.configurer)
.field("observers", &self.observers)
.finish()
}
}
impl<EM, OT, S, T, Z> CommandExecutor<EM, OT, S, T, Z>
where
T: Debug,
OT: Debug,
{
/// Accesses the inner value
pub fn inner(&mut self) -> &mut T {
&mut self.configurer
}
}
impl<EM, OT, S, Z> CommandExecutor<EM, OT, S, StdCommandConfigurator, Z>
where
OT: MatchName + Debug + ObserversTuple<S>,
S: UsesInput,
{
/// Creates a new `CommandExecutor`.
/// Instead of parsing the Command for `@@`, it will
pub fn from_cmd_with_file<P>(
cmd: &Command,
debug_child: bool,
observers: OT,
path: P,
) -> Result<Self, Error>
where
P: AsRef<Path>,
{
let mut command = clone_command(cmd);
if !debug_child {
command.stdout(Stdio::null());
command.stderr(Stdio::null());
}
command.stdin(Stdio::null());
let has_stdout_observer = observers.observes_stdout();
if has_stdout_observer {
command.stdout(Stdio::piped());
}
let has_stderr_observer = observers.observes_stderr();
if has_stderr_observer {
command.stderr(Stdio::piped());
}
Ok(Self {
observers,
configurer: StdCommandConfigurator {
input_location: InputLocation::File {
out_file: InputFile::create(path)?,
},
command,
debug_child,
has_stdout_observer,
has_stderr_observer,
},
phantom: PhantomData,
})
}
/// Parses an AFL-like commandline, replacing `@@` with the input file.
/// If no `@@` was found, will use stdin for input.
/// The arg 0 is the program.
pub fn parse_afl_cmdline<IT, O>(
args: IT,
observers: OT,
debug_child: bool,
) -> Result<Self, Error>
where
IT: IntoIterator<Item = O>,
O: AsRef<OsStr>,
{
let mut atat_at = None;
let mut builder = CommandExecutorBuilder::new();
builder.debug_child(debug_child);
let afl_delim = OsStr::new("@@");
for (pos, arg) in args.into_iter().enumerate() {
if pos == 0 {
if arg.as_ref() == afl_delim {
return Err(Error::illegal_argument(
"The first argument must not be @@ but the program to execute",
));
}
builder.program(arg);
} else if arg.as_ref() == afl_delim {
if atat_at.is_some() {
return Err(Error::illegal_argument(
"Multiple @@ in afl commandline are not permitted",
));
}
atat_at = Some(pos);
builder.arg_input_file_std();
} else {
builder.arg(arg);
}
}
builder.build(observers)
}
}
// this only works on unix because of the reliance on checking the process signal for detecting OOM
#[cfg(all(feature = "std", unix))]
impl<EM, OT, S, T, Z> Executor<EM, Z> for CommandExecutor<EM, OT, S, T, Z>
where
EM: UsesState<State = S>,
S: UsesInput,
S::Input: HasTargetBytes,
T: CommandConfigurator + Debug,
OT: Debug + MatchName + ObserversTuple<S>,
Z: UsesState<State = S>,
{
fn run_target(
&mut self,
_fuzzer: &mut Z,
_state: &mut Self::State,
_mgr: &mut EM,
input: &Self::Input,
) -> Result<ExitKind, Error> {
use std::os::unix::prelude::ExitStatusExt;
use wait_timeout::ChildExt;
let mut child = self.configurer.spawn_child(input)?;
let res = match child
.wait_timeout(Duration::from_secs(5))
.expect("waiting on child failed")
.map(|status| status.signal())
{
// for reference: https://www.man7.org/linux/man-pages/man7/signal.7.html
Some(Some(9)) => Ok(ExitKind::Oom),
Some(Some(_)) => Ok(ExitKind::Crash),
Some(None) => Ok(ExitKind::Ok),
None => {
// if this fails, there is not much we can do. let's hope it failed because the process finished
// in the meantime.
drop(child.kill());
// finally, try to wait to properly clean up system resources.
drop(child.wait());
Ok(ExitKind::Timeout)
}
};
if self.observers.observes_stderr() {
let mut stderr = Vec::new();
child.stderr.as_mut().ok_or_else(|| {
Error::illegal_state(
"Observer tries to read stderr, but stderr was not `Stdio::pipe` in CommandExecutor",
)
})?.read_to_end(&mut stderr)?;
self.observers.observe_stderr(&stderr);
}
if self.observers.observes_stdout() {
let mut stdout = Vec::new();
child.stdout.as_mut().ok_or_else(|| {
Error::illegal_state(
"Observer tries to read stdout, but stdout was not `Stdio::pipe` in CommandExecutor",
)
})?.read_to_end(&mut stdout)?;
self.observers.observe_stdout(&stdout);
}
res
}
}
impl<EM, OT, S, T, Z> UsesState for CommandExecutor<EM, OT, S, T, Z>
where
S: UsesInput,
{
type State = S;
}
impl<EM, OT, S, T, Z> UsesObservers for CommandExecutor<EM, OT, S, T, Z>
where
OT: ObserversTuple<S>,
S: UsesInput,
{
type Observers = OT;
}
impl<EM, OT, S, T, Z> HasObservers for CommandExecutor<EM, OT, S, T, Z>
where
S: UsesInput,
T: Debug,
OT: ObserversTuple<S>,
{
fn observers(&self) -> &OT {
&self.observers
}
fn observers_mut(&mut self) -> &mut OT {
&mut self.observers
}
}
/// The builder for a default [`CommandExecutor`] that should fit most use-cases.
#[derive(Debug, Clone)]
pub struct CommandExecutorBuilder {
debug_child: bool,
program: Option<OsString>,
args: Vec<OsString>,
input_location: InputLocation,
cwd: Option<PathBuf>,
envs: Vec<(OsString, OsString)>,
}
impl Default for CommandExecutorBuilder {
fn default() -> Self {
Self::new()
}
}
impl CommandExecutorBuilder {
/// Create a new [`CommandExecutorBuilder`]
#[must_use]
fn new() -> CommandExecutorBuilder {
CommandExecutorBuilder {
program: None,
args: vec![],
input_location: InputLocation::StdIn,
cwd: None,
envs: vec![],
debug_child: false,
}
}
/// Set the binary to execute
/// This option is required.
pub fn program<O>(&mut self, program: O) -> &mut Self
where
O: AsRef<OsStr>,
{
self.program = Some(program.as_ref().to_owned());
self
}
/// Set the input mode and location.
/// This option is mandatory, if not set, the `build` method will error.
fn input(&mut self, input: InputLocation) -> &mut Self {
// This is a fatal error in the user code, no point in returning Err.
assert_eq!(
self.input_location,
InputLocation::StdIn,
"input location already set to non-stdin, cannot set it again"
);
self.input_location = input;
self
}
/// Sets the input mode to [`InputLocation::Arg`] and uses the current arg offset as `argnum`.
/// During execution, at input will be provided _as argument_ at this position.
/// Use [`Self::arg_input_file_std`] if you want to provide the input as a file instead.
pub fn arg_input_arg(&mut self) -> &mut Self {
let argnum = self.args.len();
self.input(InputLocation::Arg { argnum });
self.arg("DUMMY");
self
}
/// Sets the input mode to [`InputLocation::File`]
/// and adds the filename as arg to at the current position.
/// Uses a default filename.
/// Use [`Self::arg_input_file`] to specify a custom filename.
pub fn arg_input_file_std(&mut self) -> &mut Self {
self.arg_input_file(INPUTFILE_STD);
self
}
/// Sets the input mode to [`InputLocation::File`]
/// and adds the filename as arg to at the current position.
pub fn arg_input_file<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
self.arg(path.as_ref());
let out_file_std = InputFile::create(path.as_ref()).unwrap();
self.input(InputLocation::File {
out_file: out_file_std,
});
self
}
/// Adds an argument to the program's commandline.
pub fn arg<O: AsRef<OsStr>>(&mut self, arg: O) -> &mut CommandExecutorBuilder {
self.args.push(arg.as_ref().to_owned());
self
}
/// Adds a range of arguments to the program's commandline.
pub fn args<IT, O>(&mut self, args: IT) -> &mut CommandExecutorBuilder
where
IT: IntoIterator<Item = O>,
O: AsRef<OsStr>,
{
for arg in args {
self.arg(arg.as_ref());
}
self
}
/// Adds a range of environment variables to the executed command.
pub fn envs<IT, K, V>(&mut self, vars: IT) -> &mut CommandExecutorBuilder
where
IT: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
for (ref key, ref val) in vars {
self.env(key.as_ref(), val.as_ref());
}
self
}
/// Adds an environment variable to the executed command.
pub fn env<K, V>(&mut self, key: K, val: V) -> &mut CommandExecutorBuilder
where
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.envs
.push((key.as_ref().to_owned(), val.as_ref().to_owned()));
self
}
/// Sets the working directory for the child process.
pub fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut CommandExecutorBuilder {
self.cwd = Some(dir.as_ref().to_owned());
self
}
/// If set to true, the child's output won't be redirecited to `/dev/null`.
/// Defaults to `false`.
pub fn debug_child(&mut self, debug_child: bool) -> &mut CommandExecutorBuilder {
self.debug_child = debug_child;
self
}
/// Builds the `CommandExecutor`
pub fn build<EM, OT, S, Z>(
&self,
observers: OT,
) -> Result<CommandExecutor<EM, OT, S, StdCommandConfigurator, Z>, Error>
where
OT: Debug + MatchName + ObserversTuple<S>,
S: UsesInput,
{
let Some(program) = &self.program else {
return Err(Error::illegal_argument(
"CommandExecutor::builder: no program set!",
));
};
let mut command = Command::new(program);
match &self.input_location {
InputLocation::StdIn => {
command.stdin(Stdio::piped());
}
InputLocation::File { .. } | InputLocation::Arg { .. } => {
command.stdin(Stdio::null());
}
}
command.args(&self.args);
command.envs(
self.envs
.iter()
.map(|(k, v)| (k.as_os_str(), v.as_os_str())),
);
if let Some(cwd) = &self.cwd {
command.current_dir(cwd);
}
if !self.debug_child {
command.stdout(Stdio::null());
command.stderr(Stdio::null());
}
if observers.observes_stdout() {
command.stdout(Stdio::piped());
}
if observers.observes_stderr() {
// we need stderr for `AsanBacktaceObserver`, and others
command.stderr(Stdio::piped());
}
let configurator = StdCommandConfigurator {
debug_child: self.debug_child,
has_stdout_observer: observers.observes_stdout(),
has_stderr_observer: observers.observes_stderr(),
input_location: self.input_location.clone(),
command,
};
Ok(configurator.into_executor::<EM, OT, S, Z>(observers))
}
}
/// A `CommandConfigurator` takes care of creating and spawning a [`std::process::Command`] for the [`CommandExecutor`].
/// # Example
#[cfg_attr(all(feature = "std", unix), doc = " ```")]
#[cfg_attr(not(all(feature = "std", unix)), doc = " ```ignore")]
/// use std::{io::Write, process::{Stdio, Command, Child}};
/// use libafl::{Error, bolts::AsSlice, inputs::{HasTargetBytes, Input, UsesInput}, executors::{Executor, command::CommandConfigurator}, state::UsesState};
/// #[derive(Debug)]
/// struct MyExecutor;
///
/// impl CommandConfigurator for MyExecutor {
/// fn spawn_child<I: HasTargetBytes>(
/// &mut self,
/// input: &I,
/// ) -> Result<Child, Error> {
/// let mut command = Command::new("../if");
/// command
/// .stdin(Stdio::piped())
/// .stdout(Stdio::null())
/// .stderr(Stdio::null());
///
/// let child = command.spawn().expect("failed to start process");
/// let mut stdin = child.stdin.as_ref().unwrap();
/// stdin.write_all(input.target_bytes().as_slice())?;
/// Ok(child)
/// }
/// }
///
/// fn make_executor<EM, Z>() -> impl Executor<EM, Z>
/// where
/// EM: UsesState,
/// Z: UsesState<State = EM::State>,
/// EM::State: UsesInput,
/// EM::Input: HasTargetBytes
/// {
/// MyExecutor.into_executor(())
/// }
/// ```
#[cfg(all(feature = "std", any(unix, doc)))]
pub trait CommandConfigurator: Sized + Debug {
/// Spawns a new process with the given configuration.
fn spawn_child<I>(&mut self, input: &I) -> Result<Child, Error>
where
I: Input + HasTargetBytes;
/// Create an `Executor` from this `CommandConfigurator`.
fn into_executor<EM, OT, S, Z>(self, observers: OT) -> CommandExecutor<EM, OT, S, Self, Z>
where
OT: Debug + MatchName,
{
CommandExecutor {
observers,
configurer: self,
phantom: PhantomData,
}
}
}
#[cfg(test)]
mod tests {
use crate::{
events::SimpleEventManager,
executors::{
command::{CommandExecutor, InputLocation},
Executor,
},
inputs::BytesInput,
monitors::SimpleMonitor,
state::NopState,
NopFuzzer,
};
#[test]
#[cfg(unix)]
fn test_builder() {
let mut mgr = SimpleEventManager::new(SimpleMonitor::new(|status| {
println!("{status}");
}));
let mut executor = CommandExecutor::builder();
executor
.program("ls")
.input(InputLocation::Arg { argnum: 0 });
let executor = executor.build(());
let mut executor = executor.unwrap();
executor
.run_target(
&mut NopFuzzer::new(),
&mut NopState::new(),
&mut mgr,
&BytesInput::new(b"test".to_vec()),
)
.unwrap();
}
#[test]
#[cfg(unix)]
fn test_parse_afl_cmdline() {
use alloc::string::ToString;
let mut mgr = SimpleEventManager::new(SimpleMonitor::new(|status| {
println!("{status}");
}));
let mut executor =
CommandExecutor::parse_afl_cmdline(["file".to_string(), "@@".to_string()], (), true)
.unwrap();
executor
.run_target(
&mut NopFuzzer::new(),
&mut NopState::new(),
&mut mgr,
&BytesInput::new(b"test".to_vec()),
)
.unwrap();
}
}