libafl_nyx: Add support for StdOutObserver (#2033)

* libafl_nyx: Add support for `StdOutObserver`

* reset file offset

* update example fuzzers

* fix
This commit is contained in:
Maurice 2024-04-11 18:24:32 +02:00 committed by GitHub
parent 94a2a2363a
commit 631b1746e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 71 additions and 51 deletions

View File

@ -34,8 +34,8 @@ fn main() {
let mut run_client = |state: Option<_>, mut restarting_mgr, core_id: CoreId| { let mut run_client = |state: Option<_>, mut restarting_mgr, core_id: CoreId| {
// nyx stuff // nyx stuff
let settings = NyxSettings::builder() let settings = NyxSettings::builder()
.cpu_id(0) .cpu_id(core_id.0)
.parent_cpu_id(Some(parent_cpu_id.0 as u32)) .parent_cpu_id(Some(parent_cpu_id.0))
.build(); .build();
let helper = NyxHelper::new("/tmp/nyx_libxml2/", settings).unwrap(); let helper = NyxHelper::new("/tmp/nyx_libxml2/", settings).unwrap();
let observer = unsafe { let observer = unsafe {

View File

@ -14,9 +14,10 @@ categories = ["development-tools::testing", "emulators", "embedded", "os", "no-s
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
libnyx = { git = "https://github.com/nyx-fuzz/libnyx.git", rev = "acaf7f6" } libnyx = { git = "https://github.com/nyx-fuzz/libnyx.git", rev = "6833d23" }
libafl = { path = "../libafl", version = "0.11.2", features = ["std", "libafl_derive", "frida_cli" ]} libafl = { path = "../libafl", version = "0.11.2", features = ["std", "libafl_derive", "frida_cli" ]}
libafl_bolts = { path = "../libafl_bolts", version = "0.11.2", features = ["std", "libafl_derive", "frida_cli" ]} libafl_bolts = { path = "../libafl_bolts", version = "0.11.2", features = ["std", "libafl_derive", "frida_cli" ]}
libafl_targets = { path = "../libafl_targets", version = "0.11.2", features = ["std", "sancov_cmplog"] } libafl_targets = { path = "../libafl_targets", version = "0.11.2", features = ["std", "sancov_cmplog"] }
nix = { version = "0.28.0", features = ["fs"] }
typed-builder = "0.18.1" typed-builder = "0.18.1"

View File

@ -1,4 +1,8 @@
use std::marker::PhantomData; use std::{
io::{Read, Seek},
marker::PhantomData,
os::fd::AsRawFd,
};
use libafl::{ use libafl::{
executors::{Executor, ExitKind, HasObservers}, executors::{Executor, ExitKind, HasObservers},
@ -43,6 +47,7 @@ where
S: State + HasExecutions, S: State + HasExecutions,
S::Input: HasTargetBytes, S::Input: HasTargetBytes,
Z: UsesState<State = S>, Z: UsesState<State = S>,
OT: ObserversTuple<S>,
{ {
fn run_target( fn run_target(
&mut self, &mut self,
@ -65,35 +70,57 @@ where
))); )));
} }
self.helper
.nyx_stdout
.set_len(0)
.map_err(|e| Error::illegal_state(format!("Failed to clear Nyx stdout: {e}")))?;
let size = u32::try_from(buffer.len()) let size = u32::try_from(buffer.len())
.map_err(|_| Error::unsupported("Inputs larger than 4GB are not supported"))?; .map_err(|_| Error::unsupported("Inputs larger than 4GB are not supported"))?;
// Duplicate the file descriptor since QEMU(?) closes it and we
// want to keep |self.helper.nyx_stdout| open.
let hprintf_fd = nix::unistd::dup(self.helper.nyx_stdout.as_raw_fd())
.map_err(|e| Error::illegal_state(format!("Failed to duplicate Nyx stdout fd: {e}")))?;
self.helper.nyx_process.set_input(buffer, size); self.helper.nyx_process.set_input(buffer, size);
self.helper.nyx_process.set_hprintf_fd(hprintf_fd);
// exec will take care of trace_bits, so no need to reset // exec will take care of trace_bits, so no need to reset
match self.helper.nyx_process.exec() { let exit_kind = match self.helper.nyx_process.exec() {
NyxReturnValue::Normal => Ok(ExitKind::Ok), NyxReturnValue::Normal => ExitKind::Ok,
NyxReturnValue::Crash | NyxReturnValue::Asan => Ok(ExitKind::Crash), NyxReturnValue::Crash | NyxReturnValue::Asan => ExitKind::Crash,
NyxReturnValue::Timeout => Ok(ExitKind::Timeout), NyxReturnValue::Timeout => ExitKind::Timeout,
NyxReturnValue::InvalidWriteToPayload => { NyxReturnValue::InvalidWriteToPayload => {
self.helper.nyx_process.shutdown(); self.helper.nyx_process.shutdown();
Err(Error::illegal_state( return Err(Error::illegal_state(
"FixMe: Nyx InvalidWriteToPayload handler is missing", "FixMe: Nyx InvalidWriteToPayload handler is missing",
)) ));
} }
NyxReturnValue::Error => { NyxReturnValue::Error => {
self.helper.nyx_process.shutdown(); self.helper.nyx_process.shutdown();
Err(Error::illegal_state("Nyx runtime error has occurred")) return Err(Error::illegal_state("Nyx runtime error has occurred"));
} }
NyxReturnValue::IoError => { NyxReturnValue::IoError => {
self.helper.nyx_process.shutdown(); self.helper.nyx_process.shutdown();
Err(Error::unknown("QEMU-nyx died")) return Err(Error::unknown("QEMU-nyx died"));
} }
NyxReturnValue::Abort => { NyxReturnValue::Abort => {
self.helper.nyx_process.shutdown(); self.helper.nyx_process.shutdown();
Err(Error::shutting_down()) return Err(Error::shutting_down());
} }
};
if self.observers.observes_stdout() {
let mut stdout = Vec::new();
self.helper.nyx_stdout.rewind()?;
self.helper
.nyx_stdout
.read_to_end(&mut stdout)
.map_err(|e| Error::illegal_state(format!("Failed to read Nyx stdout: {e}")))?;
self.observers.observe_stdout(&stdout);
} }
Ok(exit_kind)
} }
} }

View File

@ -1,13 +1,14 @@
/// [`NyxHelper`] is used to wrap `NyxProcess` /// [`NyxHelper`] is used to wrap `NyxProcess`
use std::{fmt::Debug, path::Path}; use std::{fmt::Debug, fs::File, path::Path};
use libafl::Error; use libafl::Error;
use libnyx::NyxProcess; use libnyx::{NyxConfig, NyxProcess, NyxProcessRole};
use crate::settings::NyxSettings; use crate::settings::NyxSettings;
pub struct NyxHelper { pub struct NyxHelper {
pub nyx_process: NyxProcess, pub nyx_process: NyxProcess,
pub nyx_stdout: File,
pub bitmap_size: usize, pub bitmap_size: usize,
pub bitmap_buffer: *mut u8, pub bitmap_buffer: *mut u8,
@ -31,52 +32,43 @@ impl NyxHelper {
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
let work_dir = share_dir.as_ref().join("workdir");
let share_dir_str = share_dir.as_ref().to_str().ok_or(Error::illegal_argument( let share_dir_str = share_dir.as_ref().to_str().ok_or(Error::illegal_argument(
"`share_dir` contains invalid UTF-8", "`share_dir` contains invalid UTF-8",
))?; ))?;
let work_dir_str = work_dir
.to_str()
.ok_or(Error::illegal_argument("`work_dir` contains invalid UTF-8"))?;
let nyx_process_type = match settings.parent_cpu_id { let mut nyx_config = NyxConfig::load(share_dir_str).map_err(|e| {
None => NyxProcessType::ALONE, Error::illegal_argument(format!("Failed to load Nyx config from share dir: {e}"))
Some(parent_cpu_id) if settings.cpu_id == parent_cpu_id => NyxProcessType::PARENT, })?;
_ => NyxProcessType::CHILD, nyx_config.set_input_buffer_size(settings.input_buffer_size);
}; nyx_config.set_process_role(match settings.parent_cpu_id {
let mut nyx_process = (match nyx_process_type { None => NyxProcessRole::StandAlone,
NyxProcessType::ALONE => NyxProcess::new( Some(parent_cpu_id) if parent_cpu_id == settings.cpu_id => NyxProcessRole::Parent,
share_dir_str, _ => NyxProcessRole::Child,
work_dir_str, });
settings.cpu_id, nyx_config.set_worker_id(settings.cpu_id);
settings.input_buffer_size,
/* input_buffer_write_protection= */ true,
),
NyxProcessType::PARENT => NyxProcess::new_parent(
share_dir_str,
work_dir_str,
settings.cpu_id,
settings.input_buffer_size,
/* input_buffer_write_protection= */ true,
),
NyxProcessType::CHILD => NyxProcess::new_child(
share_dir_str,
work_dir_str,
settings.cpu_id,
/* worker_id= */ settings.cpu_id,
),
})
.map_err(Error::illegal_argument)?;
let mut nyx_process = NyxProcess::new(&mut nyx_config, settings.cpu_id)
.map_err(|e| Error::illegal_state(format!("Failed to create Nyx process: {e}")))?;
nyx_process.option_set_reload_mode(settings.snap_mode); nyx_process.option_set_reload_mode(settings.snap_mode);
nyx_process.option_set_timeout(settings.timeout_secs, settings.timeout_micro_secs); nyx_process.option_set_timeout(settings.timeout_secs, settings.timeout_micro_secs);
nyx_process.option_apply(); nyx_process.option_apply();
let path = Path::new(nyx_config.workdir_path())
.join(format!("hprintf_{}", nyx_config.worker_id()));
let nyx_stdout = File::options()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(path)
.map_err(|e| Error::illegal_state(format!("Failed to create Nyx stdout file: {e}")))?;
let bitmap_size = nyx_process.bitmap_buffer_size(); let bitmap_size = nyx_process.bitmap_buffer_size();
let bitmap_buffer = nyx_process.bitmap_buffer_mut().as_mut_ptr(); let bitmap_buffer = nyx_process.bitmap_buffer_mut().as_mut_ptr();
Ok(Self { Ok(Self {
nyx_process, nyx_process,
nyx_stdout,
bitmap_size, bitmap_size,
bitmap_buffer, bitmap_buffer,
}) })

View File

@ -1,6 +1,6 @@
use typed_builder::TypedBuilder; use typed_builder::TypedBuilder;
const DEFAULT_INPUT_BUFFER_SIZE: u32 = 1024 * 1024; const DEFAULT_INPUT_BUFFER_SIZE: usize = 1024 * 1024;
const DEFAULT_TIMEOUT_SECS: u8 = 2; const DEFAULT_TIMEOUT_SECS: u8 = 2;
const DEFAULT_TIMEOUT_MICRO_SECS: u32 = 0; const DEFAULT_TIMEOUT_MICRO_SECS: u32 = 0;
const DEFAULT_SNAP_MODE: bool = true; const DEFAULT_SNAP_MODE: bool = true;
@ -14,14 +14,14 @@ pub struct NyxSettings {
/// * Standalone: `parent_cpu_id.is_none()`. /// * Standalone: `parent_cpu_id.is_none()`.
/// * Parent: `parent_cpu_id.is_some_and(|parent_cpu_id| parent_cpu_id == cpu_id)`. /// * Parent: `parent_cpu_id.is_some_and(|parent_cpu_id| parent_cpu_id == cpu_id)`.
/// * Child: `parent_cpu_id.is_some_and(|parent_cpu_id| parent_cpu_id != cpu_id)`. /// * Child: `parent_cpu_id.is_some_and(|parent_cpu_id| parent_cpu_id != cpu_id)`.
pub cpu_id: u32, pub cpu_id: usize,
/// The CPU core for the Nyx parent process. The parent process /// The CPU core for the Nyx parent process. The parent process
/// creates the fuzzing snapshot that can then be used by the child /// creates the fuzzing snapshot that can then be used by the child
/// processes. /// processes.
/// ///
/// Not specifying this will start the Nyx process in standalone mode. /// Not specifying this will start the Nyx process in standalone mode.
pub parent_cpu_id: Option<u32>, pub parent_cpu_id: Option<usize>,
/// Reload the VM by using the fuzzing snapshot. You probably want /// Reload the VM by using the fuzzing snapshot. You probably want
/// this to be `true`. /// this to be `true`.
@ -33,7 +33,7 @@ pub struct NyxSettings {
/// ///
/// Default is `1MB`. /// Default is `1MB`.
#[builder(default = DEFAULT_INPUT_BUFFER_SIZE)] #[builder(default = DEFAULT_INPUT_BUFFER_SIZE)]
pub input_buffer_size: u32, pub input_buffer_size: usize,
/// The timeout for a single execution in seconds (until the /// The timeout for a single execution in seconds (until the
/// hypervisor restore snapshot call). /// hypervisor restore snapshot call).