Support capturing stdout/stderr for ForkserverExecutor and code clean (#3216)

* Support capture stdout/stderr for ForkserverExecutor

Reduce code duplication for ForkserverExecutor and CommandExecutor

* use memfd_create from nix for macos and remove debug print

* resolve macos issue

* clippy

* fix macos again

* fix docs

* fix imports

* format code

* fix docs again

* fix sample

* fix another wrong import

* restore cargo.lock

* add an inner for target args

* fix and docs

* fix

* rename to ChildArgs and ChildArgsInner

* revert forkserver_simple

* allow debug child with observers

* fmt

* std marker

* fix

* move implementation to observers

* implement serde

* Add a forkserver_capture_stdout

* renaming

* fix

* fmt

* fix CommandExecutor

* add a test to check capture

* fix imports

* clippy

* fix sample

* update sample to make it closer to real usecase

* also CommandExecutor for sample

* format

* add forkserver_capture_stdout to CI

* fix doc

* accidentally remove

* fix non_std

* fix for windows

* remove useless lint

* remove spurious fuzzer

* fix for windows again

* fix imports

* fix doc sample

* fix docs

* fix sample

* fmt

* clippy

* clippy again

* fix msrv

* have cargo.lock for sample fuzzer

* avoid double read

* fix fsrv and cmd

* fix sample

* fix docs for windows

* fix typo

* clippy again

* fix exec

* typo

* clippy

* update

* fix nyx executor

* cliipy

* fmt again

* last clippy

* clippy
This commit is contained in:
lazymio 2025-05-13 22:08:27 +08:00 committed by GitHub
parent 2dbf636201
commit f901c2085d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 2331 additions and 386 deletions

View File

@ -280,6 +280,7 @@ jobs:
# Forkserver # Forkserver
- ./fuzzers/forkserver/forkserver_simple - ./fuzzers/forkserver/forkserver_simple
- ./fuzzers/forkserver/forkserver_capture_stdout
- ./fuzzers/forkserver/forkserver_libafl_cc - ./fuzzers/forkserver/forkserver_libafl_cc
- ./fuzzers/forkserver/fuzzbench_forkserver - ./fuzzers/forkserver/fuzzbench_forkserver
- ./fuzzers/forkserver/fuzzbench_forkserver_cmplog - ./fuzzers/forkserver/fuzzbench_forkserver_cmplog

View File

@ -117,8 +117,11 @@ pub fn main() {
} }
let timeout = Duration::from_secs(5); let timeout = Duration::from_secs(5);
let mut executor = let mut executor = MyExecutor { shmem_id, timeout }.into_executor(
MyExecutor { shmem_id, timeout }.into_executor(tuple_list!(observer, bt_observer)); tuple_list!(observer, bt_observer),
None,
None,
);
// Generator of printable bytearrays of max size 32 // Generator of printable bytearrays of max size 32
let mut generator = RandPrintablesGenerator::new(nonzero!(32)); let mut generator = RandPrintablesGenerator::new(nonzero!(32));

View File

@ -25,7 +25,7 @@ use libafl_bolts::{
rands::StdRand, rands::StdRand,
shmem::{ShMem, ShMemProvider}, shmem::{ShMem, ShMemProvider},
tuples::tuple_list, tuples::tuple_list,
AsSliceMut, TargetArgs, AsSliceMut, StdTargetArgs,
}; };
pub fn main() { pub fn main() {

View File

@ -136,6 +136,8 @@ pub fn main() {
command_configurator, command_configurator,
tuple_list!(observer), tuple_list!(observer),
tuple_list!(hook), tuple_list!(hook),
None,
None,
); );
// Generator of printable bytearrays of max size 32 // Generator of printable bytearrays of max size 32

View File

@ -0,0 +1 @@
forkserver_capture_stdout

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
[package]
name = "forkserver_capture_stdout"
version = "0.14.1"
authors = ["tokatoka <tokazerkje@outlook.com>", "Ziqiao Kong <mio@lazym.io>"]
edition = "2024"
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
lto = true
codegen-units = 1
opt-level = 3
[dependencies]
clap = { version = "4.5.18", features = ["derive"] }
env_logger = "0.11.5"
libafl = { path = "../../../libafl", features = ["std", "derive"] }
libafl_bolts = { path = "../../../libafl_bolts" }
log = { version = "0.4.22", features = ["release_max_level_info"] }
nix = { version = "0.29.0", features = ["signal"] }
serde = "1.0.219"
serde_json = "1.0.140"

View File

@ -0,0 +1,14 @@
# Simple Forkserver Fuzzer to Capture Stdout
This is a simple example fuzzer to showcase how to capture stdout(stderr) from `ForkserverExecutor`.
## Usage
You can build this example by `cargo build --release`.
This downloads AFLplusplus/AFLplusplus and compiles the example harness program in src/program.c with afl-cc
## Run
After you build it you can run
`cp ./target/release/forkserver_capture_stdout .` to copy the fuzzer into this directory,
and you can run
`taskset -c 1 ./forkserver_capture_stdout ./target/release/program ./corpus/ -t 1000` to run the fuzzer.
`taskset` binds this process to a specific core to improve the throughput.

View File

@ -0,0 +1,61 @@
use std::{
env,
path::Path,
process::{Command, exit},
};
const AFL_URL: &str = "https://github.com/AFLplusplus/AFLplusplus";
fn main() {
if cfg!(windows) {
println!("cargo:warning=No support for windows yet.");
exit(0);
}
unsafe {
env::remove_var("DEBUG");
}
let cwd = env::current_dir().unwrap().to_string_lossy().to_string();
let afl = format!("{}/AFLplusplus", &cwd);
let afl_cc = format!("{}/AFLplusplus/afl-cc", &cwd);
let afl_path = Path::new(&afl);
let afl_cc_path = Path::new(&afl_cc);
if !afl_path.is_dir() {
println!("cargo:warning=AFL++ not found, downloading...");
Command::new("git")
.arg("clone")
.arg(AFL_URL)
.status()
.unwrap();
}
if !afl_cc_path.is_file() {
let mut afl_cc_make = Command::new("make");
afl_cc_make.arg("all").current_dir(afl_path);
if let Ok(llvm_config) = env::var("LLVM_CONFIG") {
if !llvm_config.is_empty() {
afl_cc_make.env("LLVM_CONFIG", llvm_config);
}
}
afl_cc_make.status().unwrap();
}
let mut compile_command = Command::new(afl_cc_path);
compile_command
.args(["src/program.c", "-o"])
.arg(format!("{cwd}/target/release/program"));
if let Ok(llvm_config) = env::var("LLVM_CONFIG") {
if !llvm_config.is_empty() {
compile_command.env("LLVM_CONFIG", llvm_config);
}
}
compile_command.status().unwrap();
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=src/");
}

View File

@ -0,0 +1,261 @@
use core::time::Duration;
use std::{ops::Index, path::PathBuf};
use clap::Parser;
use libafl::{
HasMetadata,
corpus::{Corpus, InMemoryCorpus, OnDiskCorpus},
events::SimpleEventManager,
executors::{
CommandExecutor, DiffExecutor, HasObservers, StdChildArgs, forkserver::ForkserverExecutor,
},
feedback_and_fast, feedback_or,
feedbacks::{CrashFeedback, MaxMapFeedback, TimeFeedback},
fuzzer::{Fuzzer, StdFuzzer},
inputs::BytesInput,
monitors::SimpleMonitor,
mutators::{HavocScheduledMutator, Tokens, havoc_mutations, tokens_mutations},
observers::{CanTrack, HitcountsMapObserver, StdMapObserver, StdOutObserver, TimeObserver},
schedulers::{IndexesLenTimeMinimizerScheduler, QueueScheduler},
stages::mutational::StdMutationalStage,
state::{HasCorpus, StdState},
};
use libafl_bolts::{
AsSliceMut, StdTargetArgs, Truncate, current_nanos,
rands::StdRand,
shmem::{ShMem, ShMemProvider, UnixShMemProvider},
tuples::{Handled, Merge, tuple_list},
};
use nix::sys::signal::Signal;
use serde::Deserialize;
/// The commandline args this fuzzer accepts
#[derive(Debug, Parser)]
#[command(
name = "forkserver_simple",
about = "This is a simple example fuzzer to fuzz a executable instrumented by afl-cc.",
author = "tokatoka <tokazerkje@outlook.com>"
)]
struct Opt {
#[arg(
help = "The instrumented binary we want to fuzz",
name = "EXEC",
required = true
)]
executable: String,
#[arg(
help = "The directory to read initial inputs from ('seeds')",
name = "INPUT_DIR",
required = true
)]
in_dir: PathBuf,
#[arg(
help = "Timeout for each individual execution, in milliseconds",
short = 't',
long = "timeout",
default_value = "1200"
)]
timeout: u64,
#[arg(
help = "If not set, the child's stdout and stderror will be redirected to /dev/null",
short = 'd',
long = "debug-child",
default_value = "false"
)]
debug_child: bool,
#[arg(
help = "Arguments passed to the target",
name = "arguments",
num_args(1..),
allow_hyphen_values = true,
)]
arguments: Vec<String>,
#[arg(
help = "Signal used to stop child",
short = 's',
long = "signal",
value_parser = str::parse::<Signal>,
default_value = "SIGKILL"
)]
signal: Signal,
}
#[derive(Debug, Clone, Deserialize)]
struct ProgramOutput {
len: i32,
}
pub fn main() {
env_logger::init();
const MAP_SIZE: usize = 65536;
let opt = Opt::parse();
let corpus_dirs: Vec<PathBuf> = [opt.in_dir].to_vec();
// The unix shmem provider supported by AFL++ for shared memory
let mut shmem_provider = UnixShMemProvider::new().unwrap();
// The coverage map shared between observer and executor
let mut shmem = shmem_provider.new_shmem(MAP_SIZE).unwrap();
// let the forkserver know the shmid
unsafe {
shmem.write_to_env("__AFL_SHM_ID").unwrap();
}
let shmem_buf = shmem.as_slice_mut();
// Create an observation channel using the signals map
let edges_observer = unsafe {
HitcountsMapObserver::new(StdMapObserver::new("shared_mem", shmem_buf)).track_indices()
};
// Create an observation channel to keep track of the execution time
let time_observer = TimeObserver::new("time");
// Feedback to rate the interestingness of an input
// This one is composed by two Feedbacks in OR
let mut feedback = feedback_or!(
// New maximization map feedback linked to the edges observer and the feedback state
MaxMapFeedback::new(&edges_observer),
// Time feedback, this one does not need a feedback state
TimeFeedback::new(&time_observer)
);
// A feedback to choose if an input is a solution or not
// We want to do the same crash deduplication that AFL does
let mut objective = feedback_and_fast!(
// Must be a crash
CrashFeedback::new(),
// Take it only if trigger new coverage over crashes
// Uses `with_name` to create a different history from the `MaxMapFeedback` in `feedback` above
MaxMapFeedback::with_name("mapfeedback_metadata_objective", &edges_observer)
);
// create a State from scratch
let mut state = StdState::new(
// RNG
StdRand::with_seed(current_nanos()),
// Corpus that will be evolved, we keep it in memory for performance
InMemoryCorpus::<BytesInput>::new(),
// Corpus in which we store solutions (crashes in this example),
// on disk so the user can get them after stopping the fuzzer
OnDiskCorpus::new(PathBuf::from("./crashes")).unwrap(),
// States of the feedbacks.
// The feedbacks can report the data that should persist in the State.
&mut feedback,
// Same for objective feedbacks
&mut objective,
)
.unwrap();
// The Monitor trait define how the fuzzer stats are reported to the user
let monitor = SimpleMonitor::new(|s| println!("{s}"));
// The event manager handle the various events generated during the fuzzing loop
// such as the notification of the addition of a new item to the corpus
let mut mgr = SimpleEventManager::new(monitor);
// A minimization+queue policy to get testcasess from the corpus
let scheduler = IndexesLenTimeMinimizerScheduler::new(&edges_observer, QueueScheduler::new());
// A fuzzer with feedbacks and a corpus scheduler
let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective);
// If we should debug the child
let debug_child = opt.debug_child;
// Create the executor for the forkserver
let args = opt.arguments;
let observer_ref = edges_observer.handle();
let stdout = StdOutObserver::new("stdout".into()).expect("observer");
let stdout_handle = stdout.handle();
let cmd_stdout = StdOutObserver::new("cmd_stdout".into()).expect("observer");
let cmd_stdout_handle = cmd_stdout.handle();
let mut tokens = Tokens::new();
let mut executor = ForkserverExecutor::builder()
.program(opt.executable.clone())
.debug_child(debug_child)
.shmem_provider(&mut shmem_provider)
.autotokens(&mut tokens)
.parse_afl_cmdline(args.clone())
.coverage_map_size(MAP_SIZE)
.timeout(Duration::from_millis(opt.timeout))
.kill_signal(opt.signal)
.stdout_observer(stdout_handle.clone())
.build(tuple_list!(time_observer, edges_observer, stdout))
.unwrap();
let cmd_executor = CommandExecutor::builder()
.program(opt.executable)
.debug_child(debug_child)
.parse_afl_cmdline(args)
.timeout(Duration::from_millis(opt.timeout))
.stdout_observer(cmd_stdout_handle.clone())
.build(tuple_list!(cmd_stdout))
.unwrap();
if let Some(dynamic_map_size) = executor.coverage_map_size() {
executor.observers_mut()[&observer_ref]
.as_mut()
.truncate(dynamic_map_size);
}
let mut executor = DiffExecutor::new(executor, cmd_executor, ());
// In case the corpus is empty (on first run), reset
if state.must_load_initial_inputs() {
state
.load_initial_inputs(&mut fuzzer, &mut executor, &mut mgr, &corpus_dirs)
.unwrap_or_else(|err| {
panic!(
"Failed to load initial corpus at {:?}: {:?}",
&corpus_dirs, err
)
});
println!("We imported {} inputs from disk.", state.corpus().count());
}
state.add_metadata(tokens);
// Setup a mutational stage with a basic bytes mutator
let mutator =
HavocScheduledMutator::with_max_stack_pow(havoc_mutations().merge(tokens_mutations()), 6);
let mut stages = tuple_list!(StdMutationalStage::new(mutator));
for _ in 0..5 {
fuzzer
.fuzz_one(&mut stages, &mut executor, &mut state, &mut mgr)
.unwrap();
let stdout = executor
.observers()
.index(&stdout_handle)
.output
.clone()
.expect("no stdout");
let out: ProgramOutput = serde_json::from_slice(&stdout).unwrap();
println!(
"Program output from Forkserver after serde_json::from_slice is {:?}",
&out
);
let cmd_stdout = executor
.observers()
.index(&cmd_stdout_handle)
.output
.clone()
.expect("no stdout");
let out: ProgramOutput = serde_json::from_slice(&stdout).unwrap();
println!(
"Program output from CommandExecutor after serde_json::from_slice is {:?}",
&out
);
}
}

View File

@ -0,0 +1,24 @@
#include <stdio.h>
// The following line is needed for shared memeory testcase fuzzing
__AFL_FUZZ_INIT();
int main(int argc, char **argv) {
FILE *file = stdin;
if (argc > 1) { file = fopen(argv[1], "rb"); }
// The following three lines are for normal fuzzing.
/*
char buf[16];
char* p = fgets(buf, 16, file);
buf[15] = 0;
*/
// The following line is also needed for shared memory testcase fuzzing
unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;
int len = __AFL_FUZZ_TESTCASE_LEN;
printf("{\"len\": %d}", len);
return 0;
}

View File

@ -5,7 +5,7 @@ use clap::Parser;
use libafl::{ use libafl::{
corpus::{Corpus, InMemoryCorpus, OnDiskCorpus}, corpus::{Corpus, InMemoryCorpus, OnDiskCorpus},
events::SimpleEventManager, events::SimpleEventManager,
executors::{forkserver::ForkserverExecutor, HasObservers}, executors::{forkserver::ForkserverExecutor, HasObservers, StdChildArgs},
feedback_and_fast, feedback_or, feedback_and_fast, feedback_or,
feedbacks::{CrashFeedback, MaxMapFeedback, TimeFeedback}, feedbacks::{CrashFeedback, MaxMapFeedback, TimeFeedback},
fuzzer::{Fuzzer, StdFuzzer}, fuzzer::{Fuzzer, StdFuzzer},
@ -22,7 +22,7 @@ use libafl_bolts::{
rands::StdRand, rands::StdRand,
shmem::{ShMem, ShMemProvider, UnixShMemProvider}, shmem::{ShMem, ShMemProvider, UnixShMemProvider},
tuples::{tuple_list, Handled, Merge}, tuples::{tuple_list, Handled, Merge},
AsSliceMut, TargetArgs, Truncate, AsSliceMut, StdTargetArgs, Truncate,
}; };
use libafl_targets::EDGES_MAP_DEFAULT_SIZE; use libafl_targets::EDGES_MAP_DEFAULT_SIZE;
use nix::sys::signal::Signal; use nix::sys::signal::Signal;

View File

@ -6,7 +6,7 @@ use libafl::{
HasMetadata, HasMetadata,
corpus::{Corpus, InMemoryCorpus, OnDiskCorpus}, corpus::{Corpus, InMemoryCorpus, OnDiskCorpus},
events::SimpleEventManager, events::SimpleEventManager,
executors::{HasObservers, forkserver::ForkserverExecutor}, executors::{HasObservers, StdChildArgs, forkserver::ForkserverExecutor},
feedback_and_fast, feedback_or, feedback_and_fast, feedback_or,
feedbacks::{CrashFeedback, MaxMapFeedback, TimeFeedback}, feedbacks::{CrashFeedback, MaxMapFeedback, TimeFeedback},
fuzzer::{Fuzzer, StdFuzzer}, fuzzer::{Fuzzer, StdFuzzer},
@ -19,7 +19,7 @@ use libafl::{
state::{HasCorpus, StdState}, state::{HasCorpus, StdState},
}; };
use libafl_bolts::{ use libafl_bolts::{
AsSliceMut, TargetArgs, Truncate, current_nanos, AsSliceMut, StdTargetArgs, Truncate, current_nanos,
rands::StdRand, rands::StdRand,
shmem::{ShMem, ShMemProvider, UnixShMemProvider}, shmem::{ShMem, ShMemProvider, UnixShMemProvider},
tuples::{Handled, Merge, tuple_list}, tuples::{Handled, Merge, tuple_list},

View File

@ -11,7 +11,10 @@ use clap::{Arg, ArgAction, Command};
use libafl::{ use libafl::{
corpus::{Corpus, InMemoryOnDiskCorpus, OnDiskCorpus}, corpus::{Corpus, InMemoryOnDiskCorpus, OnDiskCorpus},
events::SimpleEventManager, events::SimpleEventManager,
executors::forkserver::{ForkserverExecutor, SHM_CMPLOG_ENV_VAR}, executors::{
forkserver::{ForkserverExecutor, SHM_CMPLOG_ENV_VAR},
StdChildArgs,
},
feedback_or, feedback_or,
feedbacks::{CrashFeedback, MaxMapFeedback, TimeFeedback}, feedbacks::{CrashFeedback, MaxMapFeedback, TimeFeedback},
fuzzer::{Fuzzer, StdFuzzer}, fuzzer::{Fuzzer, StdFuzzer},
@ -38,7 +41,7 @@ use libafl_bolts::{
rands::StdRand, rands::StdRand,
shmem::{ShMem, ShMemProvider, UnixShMemProvider}, shmem::{ShMem, ShMemProvider, UnixShMemProvider},
tuples::{tuple_list, Merge}, tuples::{tuple_list, Merge},
AsSliceMut, TargetArgs, AsSliceMut, StdTargetArgs,
}; };
use libafl_targets::cmps::AFLppCmpLogMap; use libafl_targets::cmps::AFLppCmpLogMap;
use nix::sys::signal::Signal; use nix::sys::signal::Signal;

View File

@ -11,7 +11,10 @@ use clap::{Arg, ArgAction, Command};
use libafl::{ use libafl::{
corpus::{Corpus, InMemoryOnDiskCorpus, OnDiskCorpus}, corpus::{Corpus, InMemoryOnDiskCorpus, OnDiskCorpus},
events::SimpleEventManager, events::SimpleEventManager,
executors::forkserver::{ForkserverExecutor, SHM_CMPLOG_ENV_VAR}, executors::{
forkserver::{ForkserverExecutor, SHM_CMPLOG_ENV_VAR},
StdChildArgs,
},
feedback_or, feedback_or,
feedbacks::{CrashFeedback, MaxMapFeedback, TimeFeedback}, feedbacks::{CrashFeedback, MaxMapFeedback, TimeFeedback},
fuzzer::{Fuzzer, StdFuzzer}, fuzzer::{Fuzzer, StdFuzzer},
@ -37,7 +40,7 @@ use libafl_bolts::{
rands::StdRand, rands::StdRand,
shmem::{ShMem, ShMemProvider, UnixShMemProvider}, shmem::{ShMem, ShMemProvider, UnixShMemProvider},
tuples::{tuple_list, Handled, Merge}, tuples::{tuple_list, Handled, Merge},
AsSliceMut, TargetArgs, AsSliceMut, StdTargetArgs,
}; };
use libafl_targets::{ use libafl_targets::{
cmps::{observers::AFLppCmpLogObserver, stages::AFLppCmplogTracingStage}, cmps::{observers::AFLppCmpLogObserver, stages::AFLppCmplogTracingStage},

View File

@ -14,6 +14,7 @@ use libafl::{
executors::{ executors::{
forkserver::{ForkserverExecutor, SHM_CMPLOG_ENV_VAR}, forkserver::{ForkserverExecutor, SHM_CMPLOG_ENV_VAR},
sand::SANDExecutor, sand::SANDExecutor,
StdChildArgs,
}, },
feedback_or, feedback_or,
feedbacks::{CrashFeedback, MaxMapFeedback, TimeFeedback}, feedbacks::{CrashFeedback, MaxMapFeedback, TimeFeedback},
@ -41,7 +42,7 @@ use libafl_bolts::{
rands::StdRand, rands::StdRand,
shmem::{ShMem, ShMemProvider, UnixShMemProvider}, shmem::{ShMem, ShMemProvider, UnixShMemProvider},
tuples::{tuple_list, Handled, Merge}, tuples::{tuple_list, Handled, Merge},
AsSliceMut, TargetArgs, AsSliceMut, StdTargetArgs,
}; };
use libafl_targets::cmps::AFLppCmpLogMap; use libafl_targets::cmps::AFLppCmpLogMap;
use nix::sys::signal::Signal; use nix::sys::signal::Signal;

View File

@ -16,7 +16,10 @@ use libafl::monitors::SimpleMonitor;
use libafl::{ use libafl::{
corpus::{CachedOnDiskCorpus, Corpus, OnDiskCorpus}, corpus::{CachedOnDiskCorpus, Corpus, OnDiskCorpus},
events::ProgressReporter, events::ProgressReporter,
executors::forkserver::{ForkserverExecutor, ForkserverExecutorBuilder, SHM_CMPLOG_ENV_VAR}, executors::{
forkserver::{ForkserverExecutor, ForkserverExecutorBuilder, SHM_CMPLOG_ENV_VAR},
StdChildArgs,
},
feedback_and, feedback_or, feedback_or_fast, feedback_and, feedback_or, feedback_or_fast,
feedbacks::{ feedbacks::{
CaptureTimeoutFeedback, ConstFeedback, CrashFeedback, MaxMapFeedback, TimeFeedback, CaptureTimeoutFeedback, ConstFeedback, CrashFeedback, MaxMapFeedback, TimeFeedback,
@ -51,7 +54,7 @@ use libafl_bolts::{
rands::StdRand, rands::StdRand,
shmem::{ShMem, ShMemProvider, UnixShMemProvider}, shmem::{ShMem, ShMemProvider, UnixShMemProvider},
tuples::{tuple_list, Handled, Merge}, tuples::{tuple_list, Handled, Merge},
AsSliceMut, TargetArgs, AsSliceMut, StdTargetArgs,
}; };
#[cfg(feature = "nyx")] #[cfg(feature = "nyx")]
use libafl_nyx::{executor::NyxExecutor, helper::NyxHelper, settings::NyxSettings}; use libafl_nyx::{executor::NyxExecutor, helper::NyxHelper, settings::NyxSettings};

View File

@ -5,7 +5,7 @@ use clap::Parser;
use libafl::{ use libafl::{
corpus::{InMemoryCorpus, OnDiskCorpus}, corpus::{InMemoryCorpus, OnDiskCorpus},
events::SimpleEventManager, events::SimpleEventManager,
executors::{forkserver::ForkserverExecutor, HasObservers}, executors::{forkserver::ForkserverExecutor, HasObservers, StdChildArgs},
feedback_and_fast, feedback_or, feedback_and_fast, feedback_or,
feedbacks::{ feedbacks::{
CrashFeedback, MaxMapFeedback, NautilusChunksMetadata, NautilusFeedback, TimeFeedback, CrashFeedback, MaxMapFeedback, NautilusChunksMetadata, NautilusFeedback, TimeFeedback,
@ -29,7 +29,7 @@ use libafl_bolts::{
rands::StdRand, rands::StdRand,
shmem::{ShMem, ShMemProvider, UnixShMemProvider}, shmem::{ShMem, ShMemProvider, UnixShMemProvider},
tuples::{tuple_list, Handled}, tuples::{tuple_list, Handled},
AsSliceMut, TargetArgs, Truncate, AsSliceMut, StdTargetArgs, Truncate,
}; };
use nix::sys::signal::Signal; use nix::sys::signal::Signal;

View File

@ -217,9 +217,11 @@ fn fuzz(
let mut stages = tuple_list!( let mut stages = tuple_list!(
// Create a concolic trace // Create a concolic trace
ConcolicTracingStage::new( ConcolicTracingStage::new(
TracingStage::new( TracingStage::new(MyCommandConfigurator.into_executor(
MyCommandConfigurator.into_executor(tuple_list!(concolic_observer)), tuple_list!(concolic_observer),
), None,
None
),),
concolic_ref, concolic_ref,
), ),
// Use the concolic trace for z3-based solving // Use the concolic trace for z3-based solving

View File

@ -533,7 +533,10 @@ mod tests {
match try_create_new(&path) { match try_create_new(&path) {
Ok(None) => (), Ok(None) => (),
Ok(_) => panic!("File {path:?} did not exist even though it should have?"), Ok(_) => panic!(
"File {} did not exist even though it should have?",
&path.display()
),
Err(e) => panic!("An unexpected error occurred: {e}"), Err(e) => panic!("An unexpected error occurred: {e}"),
} }
drop(f); drop(f);

View File

@ -1,6 +1,7 @@
//! The command executor executes a sub program for each run //! The command executor executes a sub program for each run
#[cfg(all(feature = "intel_pt", target_os = "linux"))] #[cfg(all(feature = "intel_pt", target_os = "linux"))]
use alloc::ffi::CString; use alloc::ffi::CString;
#[cfg(all(feature = "intel_pt", target_os = "linux"))]
use alloc::vec::Vec; use alloc::vec::Vec;
#[cfg(all(feature = "intel_pt", target_os = "linux"))] #[cfg(all(feature = "intel_pt", target_os = "linux"))]
use core::ffi::CStr; use core::ffi::CStr;
@ -13,18 +14,17 @@ use core::{
#[cfg(all(feature = "intel_pt", target_os = "linux"))] #[cfg(all(feature = "intel_pt", target_os = "linux"))]
use std::os::fd::AsRawFd; use std::os::fd::AsRawFd;
use std::{ use std::{
ffi::{OsStr, OsString}, ffi::OsStr,
io::{Read, Write}, io::{Read, Write},
os::unix::ffi::OsStrExt, os::{fd::RawFd, unix::ffi::OsStrExt},
path::{Path, PathBuf},
process::{Child, Command, Stdio}, process::{Child, Command, Stdio},
}; };
#[cfg(all(feature = "intel_pt", target_os = "linux"))] #[cfg(all(feature = "intel_pt", target_os = "linux"))]
use libafl_bolts::core_affinity::CoreId; use libafl_bolts::core_affinity::CoreId;
use libafl_bolts::{ use libafl_bolts::{
AsSlice, InputLocation, TargetArgs, AsSlice, InputLocation, StdTargetArgs, StdTargetArgsInner,
tuples::{Handle, MatchName, RefIndexable}, tuples::{Handle, MatchName, MatchNameRef, RefIndexable},
}; };
#[cfg(all(feature = "intel_pt", target_os = "linux"))] #[cfg(all(feature = "intel_pt", target_os = "linux"))]
use libc::STDIN_FILENO; use libc::STDIN_FILENO;
@ -46,7 +46,9 @@ use nix::{
#[cfg(all(feature = "intel_pt", target_os = "linux"))] #[cfg(all(feature = "intel_pt", target_os = "linux"))]
use typed_builder::TypedBuilder; use typed_builder::TypedBuilder;
use super::HasTimeout; #[cfg(all(target_family = "unix", feature = "fork"))]
use super::forkserver::ConfigTarget;
use super::{HasTimeout, StdChildArgs, StdChildArgsInner};
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use crate::executors::hooks::ExecutorHooksTuple; use crate::executors::hooks::ExecutorHooksTuple;
use crate::{ use crate::{
@ -55,9 +57,47 @@ use crate::{
inputs::HasTargetBytes, inputs::HasTargetBytes,
observers::{ObserversTuple, StdErrObserver, StdOutObserver}, observers::{ObserversTuple, StdErrObserver, StdOutObserver},
state::HasExecutions, state::HasExecutions,
std::borrow::ToOwned,
}; };
/// How do we capture stdout/stderr. Not intended for public use.
#[derive(Debug, Default)]
#[allow(dead_code)]
enum StdCommandCaptureMethod {
Fd(RawFd),
#[default]
Pipe,
}
impl StdCommandCaptureMethod {
fn pipe_capture(cmd: &mut Command, stdout: bool) {
if stdout {
cmd.stdout(Stdio::piped());
} else {
cmd.stderr(Stdio::piped());
}
}
fn pre_capture(&self, cmd: &mut Command, stdout: bool) {
#[cfg(feature = "fork")]
{
if let Self::Fd(old) = self {
if stdout {
cmd.setdup2(*old, libc::STDOUT_FILENO);
cmd.stdout(Stdio::null());
} else {
cmd.setdup2(*old, libc::STDERR_FILENO);
cmd.stderr(Stdio::null());
}
} else {
Self::pipe_capture(cmd, stdout);
}
}
#[cfg(not(feature = "fork"))]
Self::pipe_capture(cmd, stdout);
}
}
/// A simple Configurator that takes the most common parameters /// A simple Configurator that takes the most common parameters
/// Writes the input either to stdio or to a file /// Writes the input either to stdio or to a file
/// Use [`CommandExecutor::builder()`] to use this configurator. /// Use [`CommandExecutor::builder()`] to use this configurator.
@ -66,8 +106,8 @@ pub struct StdCommandConfigurator {
/// If set to true, the child output will remain visible /// If set to true, the child output will remain visible
/// By default, the child output is hidden to increase execution speed /// By default, the child output is hidden to increase execution speed
debug_child: bool, debug_child: bool,
stdout_observer: Option<Handle<StdOutObserver>>, stdout_cap: Option<StdCommandCaptureMethod>,
stderr_observer: Option<Handle<StdErrObserver>>, stderr_cap: Option<StdCommandCaptureMethod>,
timeout: Duration, timeout: Duration,
/// true: input gets delivered via stdink /// true: input gets delivered via stdink
input_location: InputLocation, input_location: InputLocation,
@ -79,30 +119,26 @@ impl<I> CommandConfigurator<I> for StdCommandConfigurator
where where
I: HasTargetBytes, I: HasTargetBytes,
{ {
fn stdout_observer(&self) -> Option<Handle<StdOutObserver>> {
self.stdout_observer.clone()
}
fn stderr_observer(&self) -> Option<Handle<StdErrObserver>> {
self.stderr_observer.clone()
}
fn spawn_child(&mut self, input: &I) -> Result<Child, Error> { fn spawn_child(&mut self, input: &I) -> Result<Child, Error> {
let mut cmd = Command::new(self.command.get_program());
match &mut self.input_location { match &mut self.input_location {
InputLocation::Arg { argnum } => { InputLocation::Arg { argnum } => {
let args = self.command.get_args(); let args = self.command.get_args();
let mut cmd = Command::new(self.command.get_program());
if !self.debug_child { if self.debug_child {
cmd.stdout(Stdio::inherit());
} else if let Some(cap) = &self.stdout_cap {
cap.pre_capture(&mut cmd, true);
} else {
cmd.stdout(Stdio::null()); cmd.stdout(Stdio::null());
cmd.stderr(Stdio::null());
} }
if self.stdout_observer.is_some() { if self.debug_child {
cmd.stdout(Stdio::piped()); cmd.stderr(Stdio::inherit());
} } else if let Some(cap) = &self.stderr_cap {
if self.stderr_observer.is_some() { cap.pre_capture(&mut cmd, false);
cmd.stderr(Stdio::piped()); } else {
cmd.stderr(Stdio::null());
} }
for (i, arg) in args.enumerate() { for (i, arg) in args.enumerate() {
@ -269,6 +305,8 @@ pub struct CommandExecutor<I, OT, S, T, HT = (), C = Child> {
configurer: T, configurer: T,
/// The observers used by this executor /// The observers used by this executor
observers: OT, observers: OT,
stdout_observer: Option<Handle<StdOutObserver>>,
stderr_observer: Option<Handle<StdErrObserver>>,
hooks: HT, hooks: HT,
phantom: PhantomData<(C, I, S)>, phantom: PhantomData<(C, I, S)>,
} }
@ -302,6 +340,8 @@ where
.field("inner", &self.configurer) .field("inner", &self.configurer)
.field("observers", &self.observers) .field("observers", &self.observers)
.field("hooks", &self.hooks) .field("hooks", &self.hooks)
.field("stdout_observer", &self.stdout_observer)
.field("stderr_observer", &self.stderr_observer)
.finish() .finish()
} }
} }
@ -323,9 +363,8 @@ where
fn execute_input_with_command(&mut self, state: &mut S, input: &I) -> Result<ExitKind, Error> { fn execute_input_with_command(&mut self, state: &mut S, input: &I) -> Result<ExitKind, Error> {
use wait_timeout::ChildExt; use wait_timeout::ChildExt;
self.observers_mut().pre_exec_all(state, input)?;
*state.executions_mut() += 1; *state.executions_mut() += 1;
self.observers.pre_exec_child_all(state, input)?;
let mut child = self.configurer.spawn_child(input)?; let mut child = self.configurer.spawn_child(input)?;
let exit_kind = child let exit_kind = child
@ -341,31 +380,28 @@ where
ExitKind::Timeout ExitKind::Timeout
}); });
self.observers // Manualy update stdout/stderr here if we use piped implementation.
.post_exec_child_all(state, input, &exit_kind)?; // Reason of not putting into state and pass by post_exec_all is that
// - Save extra at least two hashmap lookups since we already know the handle
// - Doesn't pose HasNamedMetadata bound on S (note we might have many stdout/stderr observers)
if let Some(mut stderr) = child.stderr {
if let Some(stderr_handle) = self.stderr_observer.clone() {
let mut buf = vec![];
stderr.read_to_end(&mut buf)?;
self.observers_mut().index_mut(&stderr_handle).observe(buf);
}
}
if let Some(h) = &mut self.configurer.stdout_observer() { if let Some(mut stdout) = child.stdout {
let mut stdout = Vec::new(); if let Some(stdout_handle) = self.stdout_observer.clone() {
child.stdout.as_mut().ok_or_else(|| { let mut buf = vec![];
Error::illegal_state( stdout.read_to_end(&mut buf)?;
"Observer tries to read stderr, but stderr was not `Stdio::pipe` in CommandExecutor", self.observers_mut().index_mut(&stdout_handle).observe(buf);
) }
})?.read_to_end(&mut stdout)?;
let mut observers = self.observers_mut();
let obs = observers.index_mut(h);
obs.observe(&stdout);
}
if let Some(h) = &mut self.configurer.stderr_observer() {
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)?;
let mut observers = self.observers_mut();
let obs = observers.index_mut(h);
obs.observe(&stderr);
} }
self.observers_mut()
.post_exec_child_all(state, input, &exit_kind)?;
Ok(exit_kind) Ok(exit_kind)
} }
} }
@ -497,48 +533,27 @@ where
/// The builder for a default [`CommandExecutor`] that should fit most use-cases. /// The builder for a default [`CommandExecutor`] that should fit most use-cases.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CommandExecutorBuilder { pub struct CommandExecutorBuilder {
stdout: Option<Handle<StdOutObserver>>, target_inner: StdTargetArgsInner,
stderr: Option<Handle<StdErrObserver>>, child_env_inner: StdChildArgsInner,
debug_child: bool,
program: Option<OsString>,
args: Vec<OsString>,
input_location: InputLocation,
cwd: Option<PathBuf>,
envs: Vec<(OsString, OsString)>,
timeout: Duration,
} }
impl TargetArgs for CommandExecutorBuilder { impl StdTargetArgs for CommandExecutorBuilder {
fn arguments_ref(&self) -> &Vec<OsString> { fn inner(&self) -> &StdTargetArgsInner {
&self.args &self.target_inner
} }
fn arguments_mut(&mut self) -> &mut Vec<OsString> { fn inner_mut(&mut self) -> &mut StdTargetArgsInner {
&mut self.args &mut self.target_inner
}
}
impl StdChildArgs for CommandExecutorBuilder {
fn inner(&self) -> &StdChildArgsInner {
&self.child_env_inner
} }
fn envs_ref(&self) -> &Vec<(OsString, OsString)> { fn inner_mut(&mut self) -> &mut StdChildArgsInner {
&self.envs &mut self.child_env_inner
}
fn envs_mut(&mut self) -> &mut Vec<(OsString, OsString)> {
&mut self.envs
}
fn program_ref(&self) -> &Option<OsString> {
&self.program
}
fn program_mut(&mut self) -> &mut Option<OsString> {
&mut self.program
}
fn input_location_ref(&self) -> &InputLocation {
&self.input_location
}
fn input_location_mut(&mut self) -> &mut InputLocation {
&mut self.input_location
} }
} }
@ -553,49 +568,11 @@ impl CommandExecutorBuilder {
#[must_use] #[must_use]
fn new() -> CommandExecutorBuilder { fn new() -> CommandExecutorBuilder {
CommandExecutorBuilder { CommandExecutorBuilder {
stdout: None, target_inner: StdTargetArgsInner::default(),
stderr: None, child_env_inner: StdChildArgsInner::default(),
program: None,
args: vec![],
input_location: InputLocation::StdIn,
cwd: None,
envs: vec![],
timeout: Duration::from_secs(5),
debug_child: false,
} }
} }
/// Sets the stdout observer
pub fn stdout_observer(&mut self, stdout: Handle<StdOutObserver>) -> &mut Self {
self.stdout = Some(stdout);
self
}
/// Sets the stderr observer
pub fn stderr_observer(&mut self, stderr: Handle<StdErrObserver>) -> &mut Self {
self.stderr = Some(stderr);
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
}
/// Sets the execution timeout duration.
pub fn timeout(&mut self, timeout: Duration) -> &mut CommandExecutorBuilder {
self.timeout = timeout;
self
}
/// Builds the `CommandExecutor` /// Builds the `CommandExecutor`
pub fn build<I, OT, S>( pub fn build<I, OT, S>(
&self, &self,
@ -605,14 +582,14 @@ impl CommandExecutorBuilder {
I: HasTargetBytes, I: HasTargetBytes,
OT: MatchName + ObserversTuple<I, S>, OT: MatchName + ObserversTuple<I, S>,
{ {
let Some(program) = &self.program else { let Some(program) = &self.target_inner.program else {
return Err(Error::illegal_argument( return Err(Error::illegal_argument(
"CommandExecutor::builder: no program set!", "CommandExecutor::builder: no program set!",
)); ));
}; };
let mut command = Command::new(program); let mut command = Command::new(program);
match &self.input_location { match &self.target_inner.input_location {
InputLocation::StdIn => { InputLocation::StdIn => {
command.stdin(Stdio::piped()); command.stdin(Stdio::piped());
} }
@ -620,40 +597,77 @@ impl CommandExecutorBuilder {
command.stdin(Stdio::null()); command.stdin(Stdio::null());
} }
} }
command.args(&self.args); command.args(&self.target_inner.arguments);
command.envs( command.envs(
self.envs self.target_inner
.envs
.iter() .iter()
.map(|(k, v)| (k.as_os_str(), v.as_os_str())), .map(|(k, v)| (k.as_os_str(), v.as_os_str())),
); );
if let Some(cwd) = &self.cwd { if let Some(cwd) = &self.child_env_inner.current_directory {
command.current_dir(cwd); command.current_dir(cwd);
} }
if !self.debug_child {
command.stdout(Stdio::null()); let stdout_cap = self.child_env_inner.stdout_observer.as_ref().map(|hdl| {
command.stderr(Stdio::null()); observers
.get(hdl)
.as_ref()
.expect("stdout observer not in observers tuple")
.as_raw_fd()
.map(StdCommandCaptureMethod::Fd)
.unwrap_or_default()
});
let stderr_cap = self.child_env_inner.stderr_observer.as_ref().map(|hdl| {
observers
.get(hdl)
.as_ref()
.expect("stderr observer not in observers tuple")
.as_raw_fd()
.map(StdCommandCaptureMethod::Fd)
.unwrap_or_default()
});
if self.child_env_inner.debug_child {
command.stdout(Stdio::piped());
command.stderr(Stdio::piped());
} else {
if let Some(cap) = &stdout_cap {
cap.pre_capture(&mut command, true);
} else {
command.stdout(Stdio::null());
}
if let Some(cap) = &stderr_cap {
cap.pre_capture(&mut command, false);
} else {
command.stderr(Stdio::null());
}
} }
if self.stdout.is_some() { if self.child_env_inner.stdout_observer.is_some() {
command.stdout(Stdio::piped()); command.stdout(Stdio::piped());
} }
if self.stderr.is_some() { if self.child_env_inner.stderr_observer.is_some() {
command.stderr(Stdio::piped()); command.stderr(Stdio::piped());
} }
let configurator = StdCommandConfigurator { let configurator = StdCommandConfigurator {
debug_child: self.debug_child, debug_child: self.child_env_inner.debug_child,
stdout_observer: self.stdout.clone(), stdout_cap,
stderr_observer: self.stderr.clone(), stderr_cap,
input_location: self.input_location.clone(), input_location: self.target_inner.input_location.clone(),
timeout: self.timeout, timeout: self.child_env_inner.timeout,
command, command,
}; };
Ok( Ok(
<StdCommandConfigurator as CommandConfigurator<I>>::into_executor::<OT, S>( <StdCommandConfigurator as CommandConfigurator<I>>::into_executor::<OT, S>(
configurator, configurator,
observers, observers,
self.child_env_inner.stdout_observer.clone(),
self.child_env_inner.stderr_observer.clone(),
), ),
) )
} }
@ -705,7 +719,7 @@ impl CommandExecutorBuilder {
/// where /// where
/// S: HasExecutions, /// S: HasExecutions,
/// { /// {
/// MyExecutor.into_executor(()) /// MyExecutor.into_executor((), None, None)
/// } /// }
/// ``` /// ```
pub trait CommandConfigurator<I, C = Child>: Sized { pub trait CommandConfigurator<I, C = Child>: Sized {
@ -739,11 +753,18 @@ pub trait CommandConfigurator<I, C = Child>: Sized {
} }
/// Create an `Executor` from this `CommandConfigurator`. /// Create an `Executor` from this `CommandConfigurator`.
fn into_executor<OT, S>(self, observers: OT) -> CommandExecutor<I, OT, S, Self, (), C> { fn into_executor<OT, S>(
self,
observers: OT,
stdout_observer: Option<Handle<StdOutObserver>>,
stderr_observer: Option<Handle<StdErrObserver>>,
) -> CommandExecutor<I, OT, S, Self, (), C> {
CommandExecutor { CommandExecutor {
configurer: self, configurer: self,
observers, observers,
hooks: (), hooks: (),
stderr_observer,
stdout_observer,
phantom: PhantomData, phantom: PhantomData,
} }
} }
@ -753,11 +774,15 @@ pub trait CommandConfigurator<I, C = Child>: Sized {
self, self,
observers: OT, observers: OT,
hooks: HT, hooks: HT,
stdout_observer: Option<Handle<StdOutObserver>>,
stderr_observer: Option<Handle<StdErrObserver>>,
) -> CommandExecutor<I, OT, S, Self, HT, C> { ) -> CommandExecutor<I, OT, S, Self, HT, C> {
CommandExecutor { CommandExecutor {
configurer: self, configurer: self,
observers, observers,
hooks, hooks,
stderr_observer,
stdout_observer,
phantom: PhantomData, phantom: PhantomData,
} }
} }
@ -781,17 +806,19 @@ fn waitpid_filtered(pid: Pid, options: Option<WaitPidFlag>) -> Result<WaitStatus
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use libafl_bolts::TargetArgs; use libafl_bolts::{StdTargetArgs, tuples::Handled};
use tuple_list::tuple_list;
use crate::{ use crate::{
events::SimpleEventManager, events::SimpleEventManager,
executors::{ executors::{
Executor, Executor, StdChildArgs,
command::{CommandExecutor, InputLocation}, command::{CommandExecutor, InputLocation},
}, },
fuzzer::NopFuzzer, fuzzer::NopFuzzer,
inputs::{BytesInput, NopInput}, inputs::{BytesInput, NopInput},
monitors::SimpleMonitor, monitors::SimpleMonitor,
observers::StdOutObserver,
state::NopState, state::NopState,
}; };
@ -818,4 +845,33 @@ mod tests {
) )
.unwrap(); .unwrap();
} }
#[test]
#[cfg_attr(miri, ignore)]
fn test_capture() {
let mut mgr: SimpleEventManager<NopInput, _, NopState<NopInput>> =
SimpleEventManager::new(SimpleMonitor::new(|status| {
log::info!("{status}");
}));
let stdout = StdOutObserver::new("stdout".into()).unwrap();
let handle = stdout.handle();
let executor = CommandExecutor::builder()
.program("ls")
.stdout_observer(handle.clone())
.input(InputLocation::Arg { argnum: 0 });
let executor = executor.build(tuple_list!(stdout));
let mut executor = executor.unwrap();
executor
.run_target(
&mut NopFuzzer::new(),
&mut NopState::<NopInput>::new(),
&mut mgr,
&BytesInput::new(b".".to_vec()),
)
.unwrap();
assert!(executor.observers.0.output.is_some());
}
} }

View File

@ -14,17 +14,18 @@ use std::{
fd::{AsRawFd, BorrowedFd}, fd::{AsRawFd, BorrowedFd},
unix::{io::RawFd, process::CommandExt}, unix::{io::RawFd, process::CommandExt},
}, },
path::PathBuf,
process::{Child, Command, Stdio}, process::{Child, Command, Stdio},
}; };
#[cfg(feature = "regex")] #[cfg(feature = "regex")]
use libafl_bolts::tuples::{Handle, Handled, MatchNameRef}; use libafl_bolts::tuples::{Handle, Handled};
use libafl_bolts::{ use libafl_bolts::{
AsSlice, AsSliceMut, InputLocation, TargetArgs, Truncate, AsSlice, AsSliceMut, InputLocation, StdTargetArgs, StdTargetArgsInner, Truncate,
fs::{InputFile, get_unique_std_input_file}, fs::{InputFile, get_unique_std_input_file},
os::{dup2, pipes::Pipe}, os::{dup2, pipes::Pipe},
shmem::{ShMem, ShMemProvider, UnixShMem, UnixShMemProvider}, shmem::{ShMem, ShMemProvider, UnixShMem, UnixShMemProvider},
tuples::{Prepend, RefIndexable}, tuples::{MatchNameRef, Prepend, RefIndexable},
}; };
use libc::RLIM_INFINITY; use libc::RLIM_INFINITY;
use nix::{ use nix::{
@ -37,7 +38,7 @@ use nix::{
unistd::Pid, unistd::Pid,
}; };
use super::HasTimeout; use super::{HasTimeout, StdChildArgs, StdChildArgsInner};
#[cfg(feature = "regex")] #[cfg(feature = "regex")]
use crate::observers::{ use crate::observers::{
AsanBacktraceObserver, get_asan_runtime_flags, get_asan_runtime_flags_with_log_path, AsanBacktraceObserver, get_asan_runtime_flags, get_asan_runtime_flags_with_log_path,
@ -161,8 +162,6 @@ pub trait ConfigTarget {
fn setlimit(&mut self, memlimit: u64) -> &mut Self; fn setlimit(&mut self, memlimit: u64) -> &mut Self;
/// enables core dumps (rlimit = infinity) /// enables core dumps (rlimit = infinity)
fn set_coredump(&mut self, enable: bool) -> &mut Self; fn set_coredump(&mut self, enable: bool) -> &mut Self;
/// Sets the stdin
fn setstdin(&mut self, fd: RawFd, use_stdin: bool) -> &mut Self;
/// Sets the AFL forkserver pipes /// Sets the AFL forkserver pipes
fn setpipe( fn setpipe(
&mut self, &mut self,
@ -171,6 +170,8 @@ pub trait ConfigTarget {
ctl_read: RawFd, ctl_read: RawFd,
ctl_write: RawFd, ctl_write: RawFd,
) -> &mut Self; ) -> &mut Self;
/// dup2 the specific fd, used for stdio
fn setdup2(&mut self, old_fd: RawFd, new_fd: RawFd) -> &mut Self;
} }
impl ConfigTarget for Command { impl ConfigTarget for Command {
@ -216,23 +217,6 @@ impl ConfigTarget for Command {
unsafe { self.pre_exec(func) } 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
}
}
#[expect(trivial_numeric_casts)] #[expect(trivial_numeric_casts)]
fn setlimit(&mut self, memlimit: u64) -> &mut Self { fn setlimit(&mut self, memlimit: u64) -> &mut Self {
if memlimit == 0 { if memlimit == 0 {
@ -277,6 +261,19 @@ impl ConfigTarget for Command {
// This calls our non-shady function from above. // This calls our non-shady function from above.
unsafe { self.pre_exec(func) } unsafe { self.pre_exec(func) }
} }
fn setdup2(&mut self, old_fd: RawFd, new_fd: RawFd) -> &mut Self {
let func = move || {
let ret = unsafe { libc::dup2(old_fd, new_fd) };
if ret < 0 {
return Err(io::Error::last_os_error());
}
Ok(())
};
// # Safety
// This calls our non-shady function from above.
unsafe { self.pre_exec(func) }
}
} }
/// The [`Forkserver`] is communication channel with a child process that forks on request of the fuzzer. /// The [`Forkserver`] is communication channel with a child process that forks on request of the fuzzer.
@ -358,6 +355,9 @@ impl Forkserver {
coverage_map_size: Option<usize>, coverage_map_size: Option<usize>,
debug_output: bool, debug_output: bool,
kill_signal: Signal, kill_signal: Signal,
stdout_memfd: Option<RawFd>,
stderr_memfd: Option<RawFd>,
cwd: Option<PathBuf>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let Some(coverage_map_size) = coverage_map_size else { let Some(coverage_map_size) = coverage_map_size else {
return Err(Error::unknown( return Err(Error::unknown(
@ -385,19 +385,32 @@ impl Forkserver {
let mut st_pipe = Pipe::new().unwrap(); let mut st_pipe = Pipe::new().unwrap();
let mut ctl_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); let mut command = Command::new(target);
// Setup args, stdio // Setup args, stdio
command command.args(args);
.args(args) if use_stdin {
.stdin(Stdio::null()) command.setdup2(input_filefd, libc::STDIN_FILENO);
.stdout(stdout) } else {
.stderr(stderr); command.stdin(Stdio::null());
}
if debug_output {
command.stdout(Stdio::inherit());
} else if let Some(fd) = &stdout_memfd {
command.setdup2(*fd, libc::STDOUT_FILENO);
command.stdout(Stdio::null());
} else {
command.stdout(Stdio::null());
}
if debug_output {
command.stderr(Stdio::inherit());
} else if let Some(fd) = &stderr_memfd {
command.setdup2(*fd, libc::STDERR_FILENO);
command.stderr(Stdio::null());
} else {
command.stderr(Stdio::null());
}
command.env("AFL_MAP_SIZE", format!("{coverage_map_size}")); command.env("AFL_MAP_SIZE", format!("{coverage_map_size}"));
@ -422,13 +435,16 @@ impl Forkserver {
#[cfg(not(feature = "regex"))] #[cfg(not(feature = "regex"))]
let _ = dump_asan_logs; let _ = dump_asan_logs;
if let Some(cwd) = cwd {
command.current_dir(cwd);
}
let fsrv_handle = match command let fsrv_handle = match command
.env("LD_BIND_NOW", "1") .env("LD_BIND_NOW", "1")
.envs(envs) .envs(envs)
.setlimit(memlimit) .setlimit(memlimit)
.set_coredump(afl_debug) .set_coredump(afl_debug)
.setsid() .setsid()
.setstdin(input_filefd, use_stdin)
.setpipe( .setpipe(
st_pipe.read_end().unwrap(), st_pipe.read_end().unwrap(),
st_pipe.write_end().unwrap(), st_pipe.write_end().unwrap(),
@ -798,58 +814,40 @@ where
/// The builder for `ForkserverExecutor` /// The builder for `ForkserverExecutor`
#[derive(Debug)] #[derive(Debug)]
#[expect(clippy::struct_excessive_bools)]
pub struct ForkserverExecutorBuilder<'a, SP> { pub struct ForkserverExecutorBuilder<'a, SP> {
program: Option<OsString>, target_inner: StdTargetArgsInner,
arguments: Vec<OsString>, child_env_inner: StdChildArgsInner,
envs: Vec<(OsString, OsString)>,
debug_child: bool,
uses_shmem_testcase: bool, uses_shmem_testcase: bool,
is_persistent: bool, is_persistent: bool,
is_deferred_frksrv: bool, is_deferred_frksrv: bool,
autotokens: Option<&'a mut Tokens>, autotokens: Option<&'a mut Tokens>,
input_location: InputLocation,
shmem_provider: Option<&'a mut SP>, shmem_provider: Option<&'a mut SP>,
max_input_size: usize, max_input_size: usize,
min_input_size: usize, min_input_size: usize,
map_size: Option<usize>, map_size: Option<usize>,
kill_signal: Option<Signal>, kill_signal: Option<Signal>,
timeout: Option<Duration>,
#[cfg(feature = "regex")] #[cfg(feature = "regex")]
asan_obs: Option<Handle<AsanBacktraceObserver>>, asan_obs: Option<Handle<AsanBacktraceObserver>>,
crash_exitcode: Option<i8>, crash_exitcode: Option<i8>,
} }
impl<SP> TargetArgs for ForkserverExecutorBuilder<'_, SP> { impl<SP> StdChildArgs for ForkserverExecutorBuilder<'_, SP> {
fn arguments_ref(&self) -> &Vec<OsString> { fn inner(&self) -> &StdChildArgsInner {
&self.arguments &self.child_env_inner
}
fn arguments_mut(&mut self) -> &mut Vec<OsString> {
&mut self.arguments
} }
fn envs_ref(&self) -> &Vec<(OsString, OsString)> { fn inner_mut(&mut self) -> &mut StdChildArgsInner {
&self.envs &mut self.child_env_inner
}
}
impl<SP> StdTargetArgs for ForkserverExecutorBuilder<'_, SP> {
fn inner(&self) -> &StdTargetArgsInner {
&self.target_inner
} }
fn envs_mut(&mut self) -> &mut Vec<(OsString, OsString)> { fn inner_mut(&mut self) -> &mut StdTargetArgsInner {
&mut self.envs &mut self.target_inner
}
fn program_ref(&self) -> &Option<OsString> {
&self.program
}
fn program_mut(&mut self) -> &mut Option<OsString> {
&mut self.program
}
fn input_location_ref(&self) -> &InputLocation {
&self.input_location
}
fn input_location_mut(&mut self) -> &mut InputLocation {
&mut self.input_location
} }
fn arg_input_arg(self) -> Self { fn arg_input_arg(self) -> Self {
@ -875,13 +873,13 @@ where
where where
OT: ObserversTuple<I, S>, OT: ObserversTuple<I, S>,
{ {
let (forkserver, input_file, map) = self.build_helper()?; let (forkserver, input_file, map) = self.build_helper(&observers)?;
let target = self.program.take().unwrap(); let target = self.target_inner.program.take().unwrap();
log::info!( log::info!(
"ForkserverExecutor: program: {:?}, arguments: {:?}, use_stdin: {:?}", "ForkserverExecutor: program: {:?}, arguments: {:?}, use_stdin: {:?}",
target, target,
self.arguments.clone(), self.target_inner.arguments.clone(),
self.use_stdin() self.use_stdin()
); );
@ -891,10 +889,7 @@ where
)); ));
} }
let timeout: TimeSpec = match self.timeout { let timeout: TimeSpec = self.child_env_inner.timeout.into();
Some(t) => t.into(),
None => Duration::from_millis(5000).into(),
};
if self.min_input_size > self.max_input_size { if self.min_input_size > self.max_input_size {
return Err(Error::illegal_argument( return Err(Error::illegal_argument(
format!( format!(
@ -907,7 +902,7 @@ where
Ok(ForkserverExecutor { Ok(ForkserverExecutor {
target, target,
args: self.arguments.clone(), args: self.target_inner.arguments.clone(),
input_file, input_file,
uses_shmem_testcase: self.uses_shmem_testcase, uses_shmem_testcase: self.uses_shmem_testcase,
forkserver, forkserver,
@ -940,13 +935,13 @@ where
MO: MapObserver + Truncate, // TODO maybe enforce Entry = u8 for the cov map MO: MapObserver + Truncate, // TODO maybe enforce Entry = u8 for the cov map
OT: ObserversTuple<I, S> + Prepend<MO>, OT: ObserversTuple<I, S> + Prepend<MO>,
{ {
let (forkserver, input_file, map) = self.build_helper()?; let (forkserver, input_file, map) = self.build_helper(&other_observers)?;
let target = self.program.take().unwrap(); let target = self.target_inner.program.take().unwrap();
log::info!( log::info!(
"ForkserverExecutor: program: {:?}, arguments: {:?}, use_stdin: {:?}, map_size: {:?}", "ForkserverExecutor: program: {:?}, arguments: {:?}, use_stdin: {:?}, map_size: {:?}",
target, target,
self.arguments.clone(), self.target_inner.arguments.clone(),
self.use_stdin(), self.use_stdin(),
self.map_size self.map_size
); );
@ -963,14 +958,11 @@ where
)); ));
} }
let timeout: TimeSpec = match self.timeout { let timeout: TimeSpec = self.child_env_inner.timeout.into();
Some(t) => t.into(),
None => Duration::from_millis(5000).into(),
};
Ok(ForkserverExecutor { Ok(ForkserverExecutor {
target, target,
args: self.arguments.clone(), args: self.target_inner.arguments.clone(),
input_file, input_file,
uses_shmem_testcase: self.uses_shmem_testcase, uses_shmem_testcase: self.uses_shmem_testcase,
forkserver, forkserver,
@ -991,8 +983,14 @@ where
} }
#[expect(clippy::pedantic)] #[expect(clippy::pedantic)]
fn build_helper(&mut self) -> Result<(Forkserver, InputFile, Option<SHM>), Error> { fn build_helper<I, OT, S>(
let input_file = match &self.input_location { &mut self,
obs: &OT,
) -> Result<(Forkserver, InputFile, Option<SHM>), Error>
where
OT: ObserversTuple<I, S>,
{
let input_file = match &self.target_inner.input_location {
InputLocation::StdIn => InputFile::create(OsString::from(get_unique_std_input_file()))?, InputLocation::StdIn => InputFile::create(OsString::from(get_unique_std_input_file()))?,
InputLocation::Arg { argnum: _ } => { InputLocation::Arg { argnum: _ } => {
return Err(Error::illegal_argument( return Err(Error::illegal_argument(
@ -1019,11 +1017,11 @@ where
} }
}; };
let mut forkserver = match &self.program { let mut forkserver = match &self.target_inner.program {
Some(t) => Forkserver::new( Some(t) => Forkserver::new(
t.clone(), t.clone(),
self.arguments.clone(), self.target_inner.arguments.clone(),
self.envs.clone(), self.target_inner.envs.clone(),
input_file.as_raw_fd(), input_file.as_raw_fd(),
self.use_stdin(), self.use_stdin(),
0, 0,
@ -1031,8 +1029,23 @@ where
self.is_deferred_frksrv, self.is_deferred_frksrv,
self.has_asan_obs(), self.has_asan_obs(),
self.map_size, self.map_size,
self.debug_child, self.child_env_inner.debug_child,
self.kill_signal.unwrap_or(KILL_SIGNAL_DEFAULT), self.kill_signal.unwrap_or(KILL_SIGNAL_DEFAULT),
self.child_env_inner.stdout_observer.as_ref().map(|t| {
obs.get(t)
.as_ref()
.expect("stdout observer not passed in the builder")
.as_raw_fd()
.expect("only memory fd backend is allowed for forkserver executor")
}),
self.child_env_inner.stderr_observer.as_ref().map(|t| {
obs.get(t)
.as_ref()
.expect("stderr observer not passed in the builder")
.as_raw_fd()
.expect("only memory fd backend is allowed for forkserver executor")
}),
self.child_env_inner.current_directory.clone(),
)?, )?,
None => { None => {
return Err(Error::illegal_argument( return Err(Error::illegal_argument(
@ -1278,13 +1291,6 @@ where
self self
} }
#[must_use]
/// set the timeout for the executor
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
/// Set the max input size /// Set the max input size
#[must_use] #[must_use]
pub fn max_input_size(mut self, size: usize) -> Self { pub fn max_input_size(mut self, size: usize) -> Self {
@ -1299,13 +1305,6 @@ where
self self
} }
/// If `debug_child` is set, the child will print to `stdout`/`stderr`.
#[must_use]
pub fn debug_child(mut self, debug_child: bool) -> Self {
self.debug_child = debug_child;
self
}
/// Call this if you want to run it under persistent mode; default is false /// Call this if you want to run it under persistent mode; default is false
#[must_use] #[must_use]
pub fn is_persistent(mut self, is_persistent: bool) -> Self { pub fn is_persistent(mut self, is_persistent: bool) -> Self {
@ -1366,21 +1365,17 @@ impl<'a> ForkserverExecutorBuilder<'a, UnixShMemProvider> {
#[must_use] #[must_use]
pub fn new() -> ForkserverExecutorBuilder<'a, UnixShMemProvider> { pub fn new() -> ForkserverExecutorBuilder<'a, UnixShMemProvider> {
ForkserverExecutorBuilder { ForkserverExecutorBuilder {
program: None, target_inner: StdTargetArgsInner::default(),
arguments: vec![], child_env_inner: StdChildArgsInner::default(),
envs: vec![],
debug_child: false,
uses_shmem_testcase: false, uses_shmem_testcase: false,
is_persistent: false, is_persistent: false,
is_deferred_frksrv: false, is_deferred_frksrv: false,
autotokens: None, autotokens: None,
input_location: InputLocation::StdIn,
shmem_provider: None, shmem_provider: None,
map_size: None, map_size: None,
max_input_size: MAX_INPUT_SIZE_DEFAULT, max_input_size: MAX_INPUT_SIZE_DEFAULT,
min_input_size: MIN_INPUT_SIZE_DEFAULT, min_input_size: MIN_INPUT_SIZE_DEFAULT,
kill_signal: None, kill_signal: None,
timeout: None,
#[cfg(feature = "regex")] #[cfg(feature = "regex")]
asan_obs: None, asan_obs: None,
crash_exitcode: None, crash_exitcode: None,
@ -1398,20 +1393,16 @@ impl<'a> ForkserverExecutorBuilder<'a, UnixShMemProvider> {
// Set the new provider // Set the new provider
shmem_provider: Some(shmem_provider), shmem_provider: Some(shmem_provider),
// Copy all other values from the old Builder // Copy all other values from the old Builder
program: self.program, target_inner: self.target_inner,
arguments: self.arguments, child_env_inner: self.child_env_inner,
envs: self.envs,
debug_child: self.debug_child,
uses_shmem_testcase: self.uses_shmem_testcase, uses_shmem_testcase: self.uses_shmem_testcase,
is_persistent: self.is_persistent, is_persistent: self.is_persistent,
is_deferred_frksrv: self.is_deferred_frksrv, is_deferred_frksrv: self.is_deferred_frksrv,
autotokens: self.autotokens, autotokens: self.autotokens,
input_location: InputLocation::StdIn,
map_size: self.map_size, map_size: self.map_size,
max_input_size: self.max_input_size, max_input_size: self.max_input_size,
min_input_size: self.min_input_size, min_input_size: self.min_input_size,
kill_signal: self.kill_signal, kill_signal: self.kill_signal,
timeout: self.timeout,
#[cfg(feature = "regex")] #[cfg(feature = "regex")]
asan_obs: self.asan_obs, asan_obs: self.asan_obs,
crash_exitcode: self.crash_exitcode, crash_exitcode: self.crash_exitcode,
@ -1443,7 +1434,11 @@ where
) -> Result<ExitKind, Error> { ) -> Result<ExitKind, Error> {
let converter = fuzzer.converter_mut(); let converter = fuzzer.converter_mut();
let bytes = converter.to_bytes(input); let bytes = converter.to_bytes(input);
self.execute_input(state, bytes.as_slice()) self.observers_mut().pre_exec_child_all(state, input)?;
let exit = self.execute_input(state, bytes.as_slice())?;
self.observers_mut()
.post_exec_child_all(state, input, &exit)?;
Ok(exit)
} }
} }
@ -1481,7 +1476,7 @@ mod tests {
use std::ffi::OsString; use std::ffi::OsString;
use libafl_bolts::{ use libafl_bolts::{
AsSliceMut, TargetArgs, AsSliceMut, StdTargetArgs,
shmem::{ShMem, ShMemProvider, UnixShMemProvider}, shmem::{ShMem, ShMemProvider, UnixShMemProvider},
tuples::tuple_list, tuples::tuple_list,
}; };
@ -1490,7 +1485,10 @@ mod tests {
use crate::{ use crate::{
Error, Error,
corpus::NopCorpus, corpus::NopCorpus,
executors::forkserver::{FAILED_TO_START_FORKSERVER_MSG, ForkserverExecutor}, executors::{
StdChildArgs,
forkserver::{FAILED_TO_START_FORKSERVER_MSG, ForkserverExecutor},
},
inputs::BytesInput, inputs::BytesInput,
observers::{ConstMapObserver, HitcountsMapObserver}, observers::{ConstMapObserver, HitcountsMapObserver},
}; };

View File

@ -2,6 +2,8 @@
use alloc::vec::Vec; use alloc::vec::Vec;
use core::{fmt::Debug, time::Duration}; use core::{fmt::Debug, time::Duration};
#[cfg(feature = "std")]
use std::path::PathBuf;
pub use combined::CombinedExecutor; pub use combined::CombinedExecutor;
#[cfg(all(feature = "std", unix))] #[cfg(all(feature = "std", unix))]
@ -14,12 +16,16 @@ pub use inprocess::InProcessExecutor;
pub use inprocess_fork::InProcessForkExecutor; pub use inprocess_fork::InProcessForkExecutor;
#[cfg(unix)] #[cfg(unix)]
use libafl_bolts::os::unix_signals::Signal; use libafl_bolts::os::unix_signals::Signal;
#[cfg(feature = "std")]
use libafl_bolts::tuples::Handle;
use libafl_bolts::tuples::RefIndexable; use libafl_bolts::tuples::RefIndexable;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub use shadow::ShadowExecutor; pub use shadow::ShadowExecutor;
pub use with_observers::WithObservers; pub use with_observers::WithObservers;
use crate::Error; use crate::Error;
#[cfg(feature = "std")]
use crate::observers::{StdErrObserver, StdOutObserver};
pub mod combined; pub mod combined;
#[cfg(all(feature = "std", unix))] #[cfg(all(feature = "std", unix))]
@ -227,6 +233,87 @@ pub fn common_signals() -> Vec<Signal> {
] ]
} }
#[cfg(feature = "std")]
/// The inner shared members of [`StdChildArgs`]
#[derive(Debug, Clone)]
pub struct StdChildArgsInner {
/// The timeout of the children
pub timeout: Duration,
/// The stderr handle of the children
pub stderr_observer: Option<Handle<StdErrObserver>>,
/// The stdout handle of the children
pub stdout_observer: Option<Handle<StdOutObserver>>,
/// The current directory of the spawned children
pub current_directory: Option<PathBuf>,
/// Whether debug child by inheriting stdout/stderr
pub debug_child: bool,
}
#[cfg(feature = "std")]
impl Default for StdChildArgsInner {
fn default() -> Self {
Self {
timeout: Duration::from_millis(5000),
stderr_observer: None,
stdout_observer: None,
current_directory: None,
debug_child: false,
}
}
}
#[cfg(feature = "std")]
/// The shared implementation for children with stdout/stderr/timeouts.
pub trait StdChildArgs: Sized {
/// The inner struct of child environment.
fn inner(&self) -> &StdChildArgsInner;
/// The mutable inner struct of child environment.
fn inner_mut(&mut self) -> &mut StdChildArgsInner;
#[must_use]
/// Sets the execution timeout duration.
fn timeout(mut self, timeout: Duration) -> Self {
self.inner_mut().timeout = timeout;
self
}
#[must_use]
/// Sets the stdout observer
fn stdout_observer(mut self, stdout: Handle<StdOutObserver>) -> Self {
self.inner_mut().stdout_observer = Some(stdout);
self
}
#[must_use]
/// Sets the stderr observer
fn stderr_observer(mut self, stderr: Handle<StdErrObserver>) -> Self {
self.inner_mut().stderr_observer = Some(stderr);
self
}
#[must_use]
/// Sets the working directory for the child process.
fn current_dir(mut self, current_dir: PathBuf) -> Self {
self.inner_mut().current_directory = Some(current_dir);
self
}
#[must_use]
/// If set to true, the child's output won't be redirecited to `/dev/null` and will go to parent's stdout/stderr
/// Defaults to `false`.
fn debug_child(mut self, debug_child: bool) -> Self {
if debug_child {
assert!(
self.inner().stderr_observer.is_none() && self.inner().stdout_observer.is_none(),
"you can not set debug_child when you have stderr_observer or stdout_observer"
);
}
self.inner_mut().debug_child = debug_child;
self
}
}
#[cfg(test)] #[cfg(test)]
/// Tester for executor /// Tester for executor
pub mod test { pub mod test {

View File

@ -4,14 +4,18 @@
//! The executor must explicitly support these observers. //! The executor must explicitly support these observers.
#![cfg_attr( #![cfg_attr(
unix, unix,
doc = r"For example, they are supported on the [`crate::executors::CommandExecutor`]." doc = r"For example, they are supported on the [`crate::executors::CommandExecutor`] and [`crate::executors::ForkserverExecutor`]."
)] )]
use alloc::{borrow::Cow, vec::Vec}; use alloc::{borrow::Cow, string::ToString, vec::Vec};
use core::marker::PhantomData; use core::marker::PhantomData;
use std::{
fs::File,
io::{Read, Seek, SeekFrom},
};
use libafl_bolts::Named; use libafl_bolts::Named;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Deserializer, Serialize};
use crate::{Error, observers::Observer}; use crate::{Error, observers::Observer};
@ -26,7 +30,7 @@ use crate::{Error, observers::Observer};
/// Error, Fuzzer, StdFuzzer, /// Error, Fuzzer, StdFuzzer,
/// corpus::{Corpus, InMemoryCorpus, Testcase}, /// corpus::{Corpus, InMemoryCorpus, Testcase},
/// events::{EventFirer, NopEventManager}, /// events::{EventFirer, NopEventManager},
/// executors::{CommandExecutor, ExitKind}, /// executors::{StdChildArgs, CommandExecutor, ExitKind},
/// feedbacks::{Feedback, StateInitializer}, /// feedbacks::{Feedback, StateInitializer},
/// inputs::BytesInput, /// inputs::BytesInput,
/// mutators::{MutationResult, NopMutator}, /// mutators::{MutationResult, NopMutator},
@ -36,7 +40,7 @@ use crate::{Error, observers::Observer};
/// }; /// };
/// use libafl_bolts::{ /// use libafl_bolts::{
/// Named, current_nanos, /// Named, current_nanos,
/// TargetArgs, /// StdTargetArgs,
/// rands::StdRand, /// rands::StdRand,
/// tuples::{Handle, Handled, MatchNameRef, tuple_list}, /// tuples::{Handle, Handled, MatchNameRef, tuple_list},
/// }; /// };
@ -87,8 +91,8 @@ use crate::{Error, observers::Observer};
/// let input_text = "Hello, World!"; /// let input_text = "Hello, World!";
/// let encoded_input_text = "SGVsbG8sIFdvcmxkIQo="; /// let encoded_input_text = "SGVsbG8sIFdvcmxkIQo=";
/// ///
/// let stdout_observer = StdOutObserver::new("stdout-observer"); /// let stdout_observer = StdOutObserver::new("stdout-observer".into()).unwrap();
/// let stderr_observer = StdErrObserver::new("stderr-observer"); /// let stderr_observer = StdErrObserver::new("stderr-observer".into()).unwrap();
/// ///
/// let mut feedback = ExportStdXObserver { /// let mut feedback = ExportStdXObserver {
/// stdout_observer: stdout_observer.handle(), /// stdout_observer: stdout_observer.handle(),
@ -170,16 +174,30 @@ use crate::{Error, observers::Observer};
/// } /// }
/// ``` /// ```
/// ///
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct OutputObserver<T> { pub struct OutputObserver<T> {
/// The name of the observer. /// The name of the observer.
pub name: Cow<'static, str>, pub name: Cow<'static, str>,
/// The captured stdout/stderr data during last execution. /// The captured stdout/stderr data during last execution.
pub output: Option<Vec<u8>>, pub output: Option<Vec<u8>>,
#[serde(skip_serializing, deserialize_with = "new_file::<_, T>")]
/// File backend of the memory to capture output, if [`None`] we use portable piped output
pub file: Option<File>,
#[serde(skip)]
/// Phantom data to hold the stream type /// Phantom data to hold the stream type
phantom: PhantomData<T>, phantom: PhantomData<T>,
} }
/// Blanket implementation for a [`std::fs::File`]. Fortunately the contents of the file
/// is transient and thus we can safely create a new one on deserialization (and skip it)
/// when doing serialization
fn new_file<'de, D, T>(_d: D) -> Result<Option<File>, D::Error>
where
D: Deserializer<'de>,
{
OutputObserver::<T>::file().map_err(|e| serde::de::Error::custom(e.to_string()))
}
/// Marker traits to mark stdout for the `OutputObserver` /// Marker traits to mark stdout for the `OutputObserver`
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct StdOutMarker; pub struct StdOutMarker;
@ -189,19 +207,80 @@ pub struct StdOutMarker;
pub struct StdErrMarker; pub struct StdErrMarker;
impl<T> OutputObserver<T> { impl<T> OutputObserver<T> {
/// Create a new `OutputObserver` with the given name. // This is the best we can do on macOS because
#[must_use] // - macos doesn't have memfd_create
pub fn new(name: &'static str) -> Self { // - fd returned from shm_open can't be written (https://stackoverflow.com/questions/73752631/cant-write-to-fd-from-shm-open-on-macos)
Self { // - there is even no native tmpfs implementation!
name: Cow::from(name), // therefore we create a file and immediately remove it to get a writtable fd.
//
// In most cases, capturing stdout/stderr every loop is very slow and mostly for debugging purpose and thus this should be acceptable.
#[cfg(target_os = "macos")]
fn file() -> Result<Option<File>, Error> {
let fp = File::create_new("fsrvmemfd")?;
nix::unistd::unlink("fsrvmemfd")?;
Ok(Some(fp))
}
/// Cool, we can have [`MemfdShMemProvider`] to create a memfd.
#[cfg(target_os = "linux")]
fn file() -> Result<Option<File>, Error> {
Ok(Some(
libafl_bolts::shmem::unix_shmem::memfd::MemfdShMemProvider::new_file()?,
))
}
/// This will use standard but portable pipe mechanism to capture outputs
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
pub fn file() -> Result<Option<File>, Error> {
Ok(None)
}
/// Create a new [`OutputObserver`] with the given name. This will use the memory fd backend
/// on Linux and macOS, which is compatible with forkserver.
pub fn new(name: Cow<'static, str>) -> Result<Self, Error> {
Ok(Self {
name,
output: None, output: None,
file: Self::file()?,
phantom: PhantomData,
})
}
/// Create a new `OutputObserver` with the given name. This use portable piped backend, which
/// only works with [`std::process::Command`].
pub fn new_piped(name: Cow<'static, str>) -> Result<Self, Error> {
Ok(Self {
name,
output: None,
file: None,
phantom: PhantomData,
})
}
/// Create a new `OutputObserver` with given name and file.
/// Useful for targets like nyx which writes to the same file again and again.
#[must_use]
pub fn new_file(name: Cow<'static, str>, file: File) -> Self {
Self {
name,
output: None,
file: Some(file),
phantom: PhantomData, phantom: PhantomData,
} }
} }
/// React to new stream data /// React to new stream data
pub fn observe(&mut self, data: &[u8]) { pub fn observe(&mut self, data: Vec<u8>) {
self.output = Some(data.into()); self.output = Some(data);
}
#[must_use]
/// Return the raw fd, if any
pub fn as_raw_fd(&self) -> Option<i32> {
#[cfg(target_family = "unix")]
return self.file.as_ref().map(std::os::fd::AsRawFd::as_raw_fd);
#[cfg(not(target_family = "unix"))]
return None;
} }
} }
@ -211,16 +290,53 @@ impl<T> Named for OutputObserver<T> {
} }
} }
impl<I, S, T> Observer<I, S> for OutputObserver<T> { impl<I, S, T> Observer<I, S> for OutputObserver<T>
where
T: 'static,
{
fn pre_exec_child(&mut self, _state: &mut S, _input: &I) -> Result<(), Error> { fn pre_exec_child(&mut self, _state: &mut S, _input: &I) -> Result<(), Error> {
if let Some(file) = self.file.as_mut() {
file.seek(SeekFrom::Start(0))?;
}
self.output = None; self.output = None;
Ok(()) Ok(())
} }
fn pre_exec(&mut self, _state: &mut S, _input: &I) -> Result<(), Error> { fn pre_exec(&mut self, _state: &mut S, _input: &I) -> Result<(), Error> {
self.output = None; self.pre_exec_child(_state, _input)
}
fn post_exec_child(
&mut self,
_state: &mut S,
_input: &I,
_exit_kind: &crate::executors::ExitKind,
) -> Result<(), Error> {
if let Some(file) = self.file.as_mut() {
if self.output.is_none() {
let pos = file.stream_position()?;
if pos != 0 {
file.seek(SeekFrom::Start(0))?;
let mut buf = vec![0; pos as usize];
file.read_exact(&mut buf)?;
self.observe(buf);
}
}
}
Ok(()) Ok(())
} }
fn post_exec(
&mut self,
_state: &mut S,
_input: &I,
_exit_kind: &crate::executors::ExitKind,
) -> Result<(), Error> {
self.post_exec_child(_state, _input, _exit_kind)
}
} }
/// An observer that captures stdout of a target. /// An observer that captures stdout of a target.

View File

@ -4,50 +4,23 @@ use core::{
ffi::{c_char, c_int}, ffi::{c_char, c_int},
pin::Pin, pin::Pin,
}; };
use std::{ffi::OsString, os::unix::ffi::OsStrExt}; use std::os::unix::ffi::OsStrExt;
use crate::{Error, InputLocation, TargetArgs}; use crate::{Error, StdTargetArgs, StdTargetArgsInner};
/// For creating an C-compatible argument /// For creating an C-compatible argument
#[derive(Debug)] #[derive(Debug)]
pub struct CMainArgsBuilder { pub struct CMainArgsBuilder {
program: Option<OsString>, inner: StdTargetArgsInner,
input_location: InputLocation,
envs: Vec<(OsString, OsString)>,
args: Vec<OsString>,
} }
impl TargetArgs for CMainArgsBuilder { impl StdTargetArgs for CMainArgsBuilder {
fn arguments_ref(&self) -> &Vec<OsString> { fn inner(&self) -> &StdTargetArgsInner {
&self.args &self.inner
} }
fn arguments_mut(&mut self) -> &mut Vec<OsString> { fn inner_mut(&mut self) -> &mut StdTargetArgsInner {
&mut self.args &mut self.inner
}
fn input_location_ref(&self) -> &InputLocation {
&self.input_location
}
fn input_location_mut(&mut self) -> &mut InputLocation {
&mut self.input_location
}
fn envs_ref(&self) -> &Vec<(OsString, OsString)> {
&self.envs
}
fn envs_mut(&mut self) -> &mut Vec<(OsString, OsString)> {
&mut self.envs
}
fn program_ref(&self) -> &Option<OsString> {
&self.program
}
fn program_mut(&mut self) -> &mut Option<OsString> {
&mut self.program
} }
} }
@ -62,10 +35,7 @@ impl CMainArgsBuilder {
#[must_use] #[must_use]
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
program: None, inner: StdTargetArgsInner::default(),
input_location: InputLocation::StdIn,
envs: Vec::new(),
args: Vec::new(),
} }
} }
@ -73,13 +43,13 @@ impl CMainArgsBuilder {
pub fn build(&self) -> Result<CMainArgs, Error> { pub fn build(&self) -> Result<CMainArgs, Error> {
let mut argv: Vec<Pin<Box<CString>>> = Vec::new(); let mut argv: Vec<Pin<Box<CString>>> = Vec::new();
if let Some(program) = &self.program { if let Some(program) = &self.inner().program {
argv.push(Box::pin(CString::new(program.as_bytes()).unwrap())); argv.push(Box::pin(CString::new(program.as_bytes()).unwrap()));
} else { } else {
return Err(Error::illegal_argument("Program not specified")); return Err(Error::illegal_argument("Program not specified"));
} }
for args in &self.args { for args in &self.inner().arguments {
argv.push(Box::pin(CString::new(args.as_bytes()).unwrap())); argv.push(Box::pin(CString::new(args.as_bytes()).unwrap()));
} }

View File

@ -1390,7 +1390,7 @@ pub mod unix_shmem {
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
ptr, slice, ptr, slice,
}; };
use std::os::fd::IntoRawFd; use std::{fs::File, os::fd::IntoRawFd};
use libc::{MAP_SHARED, PROT_READ, PROT_WRITE, close, fstat, ftruncate, mmap, munmap}; use libc::{MAP_SHARED, PROT_READ, PROT_WRITE, close, fstat, ftruncate, mmap, munmap};
use nix::sys::memfd::{MemFdCreateFlag, memfd_create}; use nix::sys::memfd::{MemFdCreateFlag, memfd_create};
@ -1533,6 +1533,21 @@ pub mod unix_shmem {
} }
} }
/// Dedicated Implementation to yield a [`std::fs::File`]
#[cfg(unix)]
impl MemfdShMemProvider {
/// Unlike [`MemfdShMemProvider::new`], this returns a file instead, without any mmap and truncate.
/// By default, the file size is capped by the tmpfs installed by the operating system, which is big
/// enough to hold all output and avoid spurious read/write errors from children. However, you are free
/// to set the size via [`std::fs::File::set_len`]
pub fn new_file() -> Result<File, Error> {
Ok(File::from(memfd_create(
c"libafl_file",
MemFdCreateFlag::empty(),
)?))
}
}
/// Implement [`ShMemProvider`] for [`MemfdShMemProvider`] /// Implement [`ShMemProvider`] for [`MemfdShMemProvider`]
#[cfg(unix)] #[cfg(unix)]
impl ShMemProvider for MemfdShMemProvider { impl ShMemProvider for MemfdShMemProvider {

View File

@ -29,27 +29,26 @@ pub enum InputLocation {
}, },
} }
/// The shared inner structs of trait [`StdTargetArgs`]
#[derive(Debug, Clone, Default)]
pub struct StdTargetArgsInner {
/// Program arguments
pub arguments: Vec<OsString>,
/// Program main program
pub program: Option<OsString>,
/// Input location, might be stdin or file or cli arg
pub input_location: InputLocation,
/// Program environments
pub envs: Vec<(OsString, OsString)>,
}
/// The main implementation trait of afl style arguments handling /// The main implementation trait of afl style arguments handling
pub trait TargetArgs: Sized { pub trait StdTargetArgs: Sized {
/// Gets the arguments /// Get inner common arguments
fn arguments_ref(&self) -> &Vec<OsString>; fn inner(&self) -> &StdTargetArgsInner;
/// Gets the mutable arguments
fn arguments_mut(&mut self) -> &mut Vec<OsString>;
/// Gets the main program /// Get mutable inner common arguments
fn program_ref(&self) -> &Option<OsString>; fn inner_mut(&mut self) -> &mut StdTargetArgsInner;
/// Gets the mutable main program
fn program_mut(&mut self) -> &mut Option<OsString>;
/// Gets the input file
fn input_location_ref(&self) -> &InputLocation;
/// Gets the mutable input file
fn input_location_mut(&mut self) -> &mut InputLocation;
/// Get the environments
fn envs_ref(&self) -> &Vec<(OsString, OsString)>;
/// Get the mutable environments
fn envs_mut(&mut self) -> &mut Vec<(OsString, OsString)>;
/// Adds an environmental var to the harness's commandline /// Adds an environmental var to the harness's commandline
#[must_use] #[must_use]
@ -58,7 +57,8 @@ pub trait TargetArgs: Sized {
K: AsRef<OsStr>, K: AsRef<OsStr>,
V: AsRef<OsStr>, V: AsRef<OsStr>,
{ {
self.envs_mut() self.inner_mut()
.envs
.push((key.as_ref().to_owned(), val.as_ref().to_owned())); .push((key.as_ref().to_owned(), val.as_ref().to_owned()));
self self
} }
@ -75,20 +75,20 @@ pub trait TargetArgs: Sized {
for (ref key, ref val) in vars { for (ref key, ref val) in vars {
res.push((key.as_ref().to_owned(), val.as_ref().to_owned())); res.push((key.as_ref().to_owned(), val.as_ref().to_owned()));
} }
self.envs_mut().append(&mut res); self.inner_mut().envs.append(&mut res);
self self
} }
/// If use stdin /// If use stdin
#[must_use] #[must_use]
fn use_stdin(&self) -> bool { fn use_stdin(&self) -> bool {
matches!(self.input_location_ref(), InputLocation::StdIn) matches!(self.inner().input_location, InputLocation::StdIn)
} }
/// Set input /// Set input
#[must_use] #[must_use]
fn input(mut self, input: InputLocation) -> Self { fn input(mut self, input: InputLocation) -> Self {
*self.input_location_mut() = input; self.inner_mut().input_location = input;
self self
} }
@ -97,7 +97,7 @@ pub trait TargetArgs: Sized {
/// Use [`Self::arg_input_file_std`] if you want to provide the input as a file instead. /// Use [`Self::arg_input_file_std`] if you want to provide the input as a file instead.
#[must_use] #[must_use]
fn arg_input_arg(mut self) -> Self { fn arg_input_arg(mut self) -> Self {
let argnum = self.arguments_ref().len(); let argnum = self.inner().arguments.len();
self = self.input(InputLocation::Arg { argnum }); self = self.input(InputLocation::Arg { argnum });
// Placeholder arg that gets replaced with the input name later. // Placeholder arg that gets replaced with the input name later.
self = self.arg("PLACEHOLDER"); self = self.arg("PLACEHOLDER");
@ -112,7 +112,7 @@ pub trait TargetArgs: Sized {
fn arg_input_file<P: AsRef<Path>>(self, path: P) -> Self { fn arg_input_file<P: AsRef<Path>>(self, path: P) -> Self {
let mut moved = self.arg(path.as_ref()); let mut moved = self.arg(path.as_ref());
assert!( assert!(
match moved.input_location_ref() { match &moved.inner().input_location {
InputLocation::File { out_file } => out_file.path.as_path() == path.as_ref(), InputLocation::File { out_file } => out_file.path.as_path() == path.as_ref(),
InputLocation::StdIn => true, InputLocation::StdIn => true,
InputLocation::Arg { argnum: _ } => false, InputLocation::Arg { argnum: _ } => false,
@ -137,7 +137,7 @@ pub trait TargetArgs: Sized {
where where
O: AsRef<OsStr>, O: AsRef<OsStr>,
{ {
*self.program_mut() = Some(program.as_ref().to_owned()); self.inner_mut().program = Some(program.as_ref().to_owned());
self self
} }
@ -150,7 +150,7 @@ pub trait TargetArgs: Sized {
where where
O: AsRef<OsStr>, O: AsRef<OsStr>,
{ {
self.arguments_mut().push(arg.as_ref().to_owned()); self.inner_mut().arguments.push(arg.as_ref().to_owned());
self self
} }
@ -168,7 +168,7 @@ pub trait TargetArgs: Sized {
for arg in args { for arg in args {
res.push(arg.as_ref().to_owned()); res.push(arg.as_ref().to_owned());
} }
self.arguments_mut().append(&mut res); self.inner_mut().arguments.append(&mut res);
self self
} }
@ -189,7 +189,7 @@ pub trait TargetArgs: Sized {
let mut moved = self; let mut moved = self;
let mut use_arg_0_as_program = false; let mut use_arg_0_as_program = false;
if moved.program_ref().is_none() { if moved.inner().program.is_none() {
use_arg_0_as_program = true; use_arg_0_as_program = true;
} }
@ -200,7 +200,7 @@ pub trait TargetArgs: Sized {
// subsequent arguments as regular arguments // subsequent arguments as regular arguments
use_arg_0_as_program = false; use_arg_0_as_program = false;
} else if item.as_ref() == "@@" { } else if item.as_ref() == "@@" {
match moved.input_location_ref().clone() { match moved.inner().input_location.clone() {
InputLocation::File { out_file } => { InputLocation::File { out_file } => {
// If the input file name has been modified, use this one // If the input file name has been modified, use this one
moved = moved.arg_input_file(&out_file.path); moved = moved.arg_input_file(&out_file.path);

View File

@ -1,6 +1,7 @@
use core::marker::PhantomData; use core::marker::PhantomData;
use std::{ use std::{
io::{Read, Seek}, io::{Read, Seek},
ops::IndexMut,
os::fd::AsRawFd, os::fd::AsRawFd,
}; };
@ -11,7 +12,10 @@ use libafl::{
observers::{ObserversTuple, StdOutObserver}, observers::{ObserversTuple, StdOutObserver},
state::HasExecutions, state::HasExecutions,
}; };
use libafl_bolts::{AsSlice, tuples::RefIndexable}; use libafl_bolts::{
AsSlice,
tuples::{Handle, RefIndexable},
};
use libnyx::NyxReturnValue; use libnyx::NyxReturnValue;
use crate::{cmplog::CMPLOG_ENABLED, helper::NyxHelper}; use crate::{cmplog::CMPLOG_ENABLED, helper::NyxHelper};
@ -21,7 +25,7 @@ pub struct NyxExecutor<S, OT> {
/// implement nyx function /// implement nyx function
pub helper: NyxHelper, pub helper: NyxHelper,
/// stdout /// stdout
stdout: Option<StdOutObserver>, stdout: Option<Handle<StdOutObserver>>,
/// stderr /// stderr
// stderr: Option<StdErrObserver>, // stderr: Option<StdErrObserver>,
/// observers /// observers
@ -112,7 +116,7 @@ where
} }
}; };
if let Some(ob) = self.stdout.as_mut() { if let Some(ob) = self.stdout.clone() {
let mut stdout = Vec::new(); let mut stdout = Vec::new();
self.helper.nyx_stdout.rewind()?; self.helper.nyx_stdout.rewind()?;
self.helper self.helper
@ -120,7 +124,7 @@ where
.read_to_end(&mut stdout) .read_to_end(&mut stdout)
.map_err(|e| Error::illegal_state(format!("Failed to read Nyx stdout: {e}")))?; .map_err(|e| Error::illegal_state(format!("Failed to read Nyx stdout: {e}")))?;
ob.observe(&stdout); self.observers_mut().index_mut(&ob).observe(stdout);
} }
unsafe { unsafe {
@ -169,7 +173,7 @@ impl<S, OT> NyxExecutor<S, OT> {
} }
pub struct NyxExecutorBuilder { pub struct NyxExecutorBuilder {
stdout: Option<StdOutObserver>, stdout: Option<Handle<StdOutObserver>>,
// stderr: Option<StdErrObserver>, // stderr: Option<StdErrObserver>,
} }
@ -188,7 +192,7 @@ impl NyxExecutorBuilder {
} }
} }
pub fn stdout(&mut self, stdout: StdOutObserver) -> &mut Self { pub fn stdout(&mut self, stdout: Handle<StdOutObserver>) -> &mut Self {
self.stdout = Some(stdout); self.stdout = Some(stdout);
self self
} }

View File

@ -7,7 +7,10 @@ use libafl::{
Error, HasMetadata, Error, HasMetadata,
corpus::{CachedOnDiskCorpus, Corpus, OnDiskCorpus}, corpus::{CachedOnDiskCorpus, Corpus, OnDiskCorpus},
events::{EventConfig, LlmpRestartingEventManager, launcher::Launcher}, events::{EventConfig, LlmpRestartingEventManager, launcher::Launcher},
executors::forkserver::{ForkserverExecutor, SHM_CMPLOG_ENV_VAR}, executors::{
StdChildArgs,
forkserver::{ForkserverExecutor, SHM_CMPLOG_ENV_VAR},
},
feedback_and_fast, feedback_or, feedback_or_fast, feedback_and_fast, feedback_or, feedback_or_fast,
feedbacks::{CrashFeedback, MaxMapFeedback, TimeFeedback, TimeoutFeedback}, feedbacks::{CrashFeedback, MaxMapFeedback, TimeFeedback, TimeoutFeedback},
fuzzer::{Fuzzer, StdFuzzer}, fuzzer::{Fuzzer, StdFuzzer},
@ -28,7 +31,7 @@ use libafl::{
state::{HasCorpus, StdState}, state::{HasCorpus, StdState},
}; };
use libafl_bolts::{ use libafl_bolts::{
AsSliceMut, TargetArgs, AsSliceMut, StdTargetArgs,
core_affinity::Cores, core_affinity::Cores,
nonzero, nonzero,
ownedref::OwnedRefMut, ownedref::OwnedRefMut,

View File

@ -131,11 +131,11 @@ where
let mut out_dir = self.output_dir.clone(); let mut out_dir = self.output_dir.clone();
if fs::create_dir(&out_dir).is_err() { if fs::create_dir(&out_dir).is_err() {
log::info!("Out dir at {:?} already exists.", &out_dir); log::info!("Out dir at {} already exists.", &out_dir.display());
assert!( assert!(
out_dir.is_dir(), out_dir.is_dir(),
"Out dir at {:?} is not a valid directory!", "Out dir at {} is not a valid directory!",
&out_dir &out_dir.display()
); );
} }
let mut crashes = out_dir.clone(); let mut crashes = out_dir.clone();

View File

@ -6,7 +6,7 @@ use std::{
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use nix::unistd::{dup2, execvp}; use nix::unistd::{dup2, execvp};
use crate::{args::ChildArgs, exit::Exit}; use crate::{args::StdChildArgs, exit::Exit};
pub struct Child { pub struct Child {
argv: Vec<String>, argv: Vec<String>,
@ -47,7 +47,7 @@ impl Child {
Ok(()) Ok(())
} }
pub fn new(args: &impl ChildArgs, fd1: RawFd, fd2: RawFd) -> Child { pub fn new(args: &impl StdChildArgs, fd1: RawFd, fd2: RawFd) -> Child {
Child { Child {
argv: args.argv().to_vec(), argv: args.argv().to_vec(),
fd1, fd1,