From 871548c3663677223e162d9c4937a4d41c57b867 Mon Sep 17 00:00:00 2001 From: jma <94166787+jma-qb@users.noreply.github.com> Date: Fri, 23 May 2025 10:22:00 +0200 Subject: [PATCH] Allow passing Qemu object to QemuBytesCoverageSugar (#3261) * attempt to return Qemu object as a parameter to QemuBytesCoverageSugar * apply clippy suggestions from precommit.sh * python qemu sugar: add option to enable stdout --------- Co-authored-by: Dongjia "toka" Zhang --- fuzzers/binary_only/python_qemu/fuzzer.py | 10 +++- libafl_qemu/src/emu/builder.rs | 29 +++++++++- libafl_sugar/src/qemu.rs | 70 +++++++++++++++++------ 3 files changed, 89 insertions(+), 20 deletions(-) diff --git a/fuzzers/binary_only/python_qemu/fuzzer.py b/fuzzers/binary_only/python_qemu/fuzzer.py index 295159cad5..6a0619719a 100644 --- a/fuzzers/binary_only/python_qemu/fuzzer.py +++ b/fuzzers/binary_only/python_qemu/fuzzer.py @@ -6,7 +6,7 @@ import lief MAX_SIZE = 0x100 BINARY_PATH = "./a.out" -emu = qemu.Qemu(["qemu-x86_64", BINARY_PATH], []) +emu = qemu.Qemu(["qemu-x86_64", BINARY_PATH]) elf = lief.parse(BINARY_PATH) test_one_input = elf.get_function_address("LLVMFuzzerTestOneInput") @@ -41,5 +41,11 @@ def harness(b): emu.run() -fuzz = sugar.QemuBytesCoverageSugar(["./in"], "./out", 3456, [0, 1, 2, 3]) +# Create a fuzzer using the launcher +# with 4 instances bounds to cores 0-3 +# LLMP uses port 3456 to synchronize +# stdout from the target is NOT redirected to /dev/null +fuzz = sugar.QemuBytesCoverageSugar( + ["./in"], "./out", 3456, [0, 1, 2, 3], enable_stdout=True +) fuzz.run(emu, harness) diff --git a/libafl_qemu/src/emu/builder.rs b/libafl_qemu/src/emu/builder.rs index fcdbe5f328..31eb33b161 100644 --- a/libafl_qemu/src/emu/builder.rs +++ b/libafl_qemu/src/emu/builder.rs @@ -6,8 +6,8 @@ use libafl_bolts::tuples::{Append, Prepend, tuple_list}; #[cfg(feature = "systemmode")] use crate::FastSnapshotManager; use crate::{ - Emulator, NopEmulatorDriver, NopSnapshotManager, QemuInitError, QemuParams, StdEmulatorDriver, - StdSnapshotManager, + Emulator, NopEmulatorDriver, NopSnapshotManager, Qemu, QemuInitError, QemuParams, + StdEmulatorDriver, StdSnapshotManager, command::{NopCommandManager, StdCommandManager}, config::QemuConfigBuilder, modules::{EmulatorModule, EmulatorModuleTuple}, @@ -160,6 +160,31 @@ where self.command_manager, ) } + + #[allow(clippy::type_complexity)] + pub fn build_with_qemu( + self, + qemu: Qemu, + ) -> Result, QemuInitError> + where + ET: EmulatorModuleTuple, + { + // The logic from Emulator::new needs to be duplicated here because of type mismatch on modules + // between Emulator::new and Emulator::new_wit_qemu + let emulator_hooks = + unsafe { super::EmulatorHooks::new(crate::QemuHooks::get_unchecked()) }; + let emulator_modules = unsafe { super::EmulatorModules::new(emulator_hooks, self.modules) }; + + unsafe { + Ok(Emulator::new_with_qemu( + qemu, + emulator_modules, + self.driver, + self.snapshot_manager, + self.command_manager, + )) + } + } } impl EmulatorBuilder diff --git a/libafl_sugar/src/qemu.rs b/libafl_sugar/src/qemu.rs index dee302788e..63b8442302 100644 --- a/libafl_sugar/src/qemu.rs +++ b/libafl_sugar/src/qemu.rs @@ -85,6 +85,9 @@ where /// Fuzz `iterations` number of times, instead of indefinitely; implies use of `fuzz_loop_for` #[builder(default = None)] iterations: Option, + /// Disable redirection of stdout to /dev/null on unix build targets + #[builder(default = None)] + enable_stdout: Option, } impl Debug for QemuBytesCoverageSugar<'_, H> @@ -111,17 +114,27 @@ where }, ) .field("iterations", &self.iterations) + .field("enable_stdout", &self.enable_stdout) .finish() } } +/// Enum to allow passing either qemu cli parameters or a running qemu instance +#[derive(Debug, Copy, Clone)] +pub enum QemuSugarParameter<'a> { + /// Argument list to pass to initialize Qemu + QemuCli(&'a [String]), + /// Already existing Qemu instance + Qemu(&'a Qemu), +} + impl QemuBytesCoverageSugar<'_, H> where H: FnMut(&[u8]), { /// Run the fuzzer #[expect(clippy::too_many_lines)] - pub fn run(&mut self, qemu_cli: &[String]) { + pub fn run(&mut self, qemu: QemuSugarParameter) { let conf = match self.configuration.as_ref() { Some(name) => EventConfig::from_name(name), None => EventConfig::AlwaysUnique, @@ -146,7 +159,7 @@ where let shmem_provider = StdShMemProvider::new().expect("Failed to init shared memory"); - let monitor = MultiMonitor::new(|s| log::info!("{s}")); + let monitor = MultiMonitor::new(|s| println!("{s}")); // Create an observation channel to keep track of the execution time let time_observer = TimeObserver::new("time"); @@ -254,11 +267,17 @@ where ExitKind::Ok }; - let emulator = Emulator::empty() - .qemu_parameters(qemu_cli.to_owned()) - .modules(modules) - .build() - .expect("Could not initialize Emulator"); + let emulator = match qemu { + QemuSugarParameter::QemuCli(qemu_cli) => Emulator::empty() + .qemu_parameters(qemu_cli.to_owned()) + .modules(modules) + .build() + .expect("Could not initialize Emulator"), + QemuSugarParameter::Qemu(qemu) => Emulator::empty() + .modules(modules) + .build_with_qemu(*qemu) + .expect("Could not initialize Emulator"), + }; let executor = QemuExecutor::new( emulator, @@ -377,11 +396,17 @@ where ExitKind::Ok }; - let emulator = Emulator::empty() - .qemu_parameters(qemu_cli.to_owned()) - .modules(modules) - .build() - .expect("Could not initialize Emulator"); + let emulator = match qemu { + QemuSugarParameter::QemuCli(qemu_cli) => Emulator::empty() + .qemu_parameters(qemu_cli.to_owned()) + .modules(modules) + .build() + .expect("Could not initialize Emulator"), + QemuSugarParameter::Qemu(qemu) => Emulator::empty() + .modules(modules) + .build_with_qemu(*qemu) + .expect("Could not initialize Emulator"), + }; let mut executor = QemuExecutor::new( emulator, @@ -488,8 +513,14 @@ where .remote_broker_addr(self.remote_broker_addr); #[cfg(unix)] - let launcher = launcher.stdout_file(Some("/dev/null")); + if self.enable_stdout.unwrap_or(false) { + launcher.build().launch().expect("Launcher failed"); + } else { + let launcher = launcher.stdout_file(Some("/dev/null")); + launcher.build().launch().expect("Launcher failed"); + } + #[cfg(not(unix))] launcher.build().launch().expect("Launcher failed"); } } @@ -500,8 +531,10 @@ pub mod pybind { use std::path::PathBuf; use libafl_bolts::core_affinity::Cores; + use libafl_qemu::pybind::Qemu; use pyo3::{prelude::*, types::PyBytes}; + use super::QemuSugarParameter; use crate::qemu; #[pyclass(unsendable)] @@ -515,6 +548,7 @@ pub mod pybind { iterations: Option, tokens_file: Option, timeout: Option, + enable_stdout: Option, } #[pymethods] @@ -530,7 +564,8 @@ pub mod pybind { use_cmplog=None, iterations=None, tokens_file=None, - timeout=None + timeout=None, + enable_stdout=None, ))] fn new( input_dirs: Vec, @@ -541,6 +576,7 @@ pub mod pybind { iterations: Option, tokens_file: Option, timeout: Option, + enable_stdout: Option, ) -> Self { Self { input_dirs, @@ -551,12 +587,13 @@ pub mod pybind { iterations, tokens_file, timeout, + enable_stdout, } } /// Run the fuzzer #[expect(clippy::needless_pass_by_value)] - pub fn run(&self, qemu_cli: Vec, harness: PyObject) { + pub fn run(&self, qemu: &Qemu, harness: PyObject) { qemu::QemuBytesCoverageSugar::builder() .input_dirs(&self.input_dirs) .output_dir(self.output_dir.clone()) @@ -574,8 +611,9 @@ pub mod pybind { .timeout(self.timeout) .tokens_file(self.tokens_file.clone()) .iterations(self.iterations) + .enable_stdout(self.enable_stdout) .build() - .run(&qemu_cli); + .run(QemuSugarParameter::Qemu(&qemu.qemu)); } }