diff --git a/fuzzers/qemu/qemu_systemmode/build.rs b/fuzzers/qemu/qemu_systemmode/build.rs index a6f4f86bf4..7e6c08f7a2 100644 --- a/fuzzers/qemu/qemu_systemmode/build.rs +++ b/fuzzers/qemu/qemu_systemmode/build.rs @@ -5,7 +5,7 @@ macro_rules! assert_unique_feature { () => {}; ($first:tt $(,$rest:tt)*) => { $( - #[cfg(all(not(doc), feature = $first, feature = $rest))] + #[cfg(all(not(any(doc, clippy)), feature = $first, feature = $rest))] compile_error!(concat!("features \"", $first, "\" and \"", $rest, "\" cannot be used together")); )* assert_unique_feature!($($rest),*); diff --git a/fuzzers/qemu/qemu_systemmode/src/fuzzer_classic.rs b/fuzzers/qemu/qemu_systemmode/src/fuzzer_classic.rs index 14ddd0c5c1..2d4ec1945e 100644 --- a/fuzzers/qemu/qemu_systemmode/src/fuzzer_classic.rs +++ b/fuzzers/qemu/qemu_systemmode/src/fuzzer_classic.rs @@ -36,8 +36,8 @@ use libafl_qemu::{ modules::edges::{ edges_map_mut_ptr, EdgeCoverageModule, EDGES_MAP_SIZE_IN_USE, MAX_EDGES_FOUND, }, - Emulator, NopEmulatorExitHandler, QemuExitError, QemuExitReason, QemuRWError, - QemuShutdownCause, Regs, + qemu_config, Emulator, NopEmulatorExitHandler, Qemu, QemuExitError, QemuExitReason, + QemuRWError, QemuShutdownCause, Regs, }; use libafl_qemu_sys::GuestPhysAddr; @@ -87,23 +87,34 @@ pub fn fuzz() { println!("Breakpoint address = {breakpoint:#x}"); let mut run_client = |state: Option<_>, mut mgr, _core_id| { + let target_dir = env::var("TARGET_DIR").expect("TARGET_DIR env not set"); // Initialize QEMU - let args: Vec = env::args().collect(); - let env: Vec<(String, String)> = env::vars().collect(); + let qemu = Qemu::builder() + .machine("mps2-an385") + .monitor(qemu_config::Monitor::Null) + .kernel(format!("{target_dir}/example.elf")) + .serial(qemu_config::Serial::Null) + .no_graphic(true) + .snapshot(true) + .drives([qemu_config::Drive::builder() + .interface(qemu_config::DriveInterface::None) + .format(qemu_config::DiskImageFileFormat::Qcow2) + .file(format!("{target_dir}/dummy.qcow2")) + .build()]) + .start_cpu(false) + .build() + .expect("Failed to initialized QEMU"); let emulator_modules = tuple_list!(EdgeCoverageModule::default()); - let emulator = Emulator::new( - args.as_slice(), - env.as_slice(), + let mut emulator = Emulator::new_with_qemu( + qemu, emulator_modules, NopEmulatorExitHandler, NopCommandManager, ) .unwrap(); - let qemu = emulator.qemu(); - qemu.set_breakpoint(main_addr); unsafe { diff --git a/libafl_derive/Cargo.toml b/libafl_derive/Cargo.toml index b3e6fb3b42..49d9fb6310 100644 --- a/libafl_derive/Cargo.toml +++ b/libafl_derive/Cargo.toml @@ -23,3 +23,4 @@ proc-macro = true [dependencies] syn = { version = "2", features = ["full", "extra-traits"] } quote = "1" +proc-macro2 = "1.0" diff --git a/libafl_derive/src/lib.rs b/libafl_derive/src/lib.rs index a6f1522085..a803ebd397 100644 --- a/libafl_derive/src/lib.rs +++ b/libafl_derive/src/lib.rs @@ -59,7 +59,7 @@ use proc_macro::TokenStream; use quote::quote; -use syn::{parse_macro_input, DeriveInput}; +use syn::{parse_macro_input, Data::Struct, DeriveInput, Field, Fields::Named, Type}; /// Derive macro to implement `SerdeAny`, to use a type in a `SerdeAnyMap` #[proc_macro_derive(SerdeAny)] @@ -69,3 +69,89 @@ pub fn libafl_serdeany_derive(input: TokenStream) -> TokenStream { libafl_bolts::impl_serdeany!(#name); }) } + +/// Derive macro to implement `Display` for a struct where all fields implement `Display`. +/// The result is the space separated concatenation of all fields' display. +/// Order of declaration is preserved. +/// Specifically handled cases: +/// Options: Some => inner type display None => "". +/// Vec: inner type display space separated concatenation. +/// Generics and other more or less exotic stuff are not supported. +/// +/// # Examples +/// +/// ```rust +/// use libafl_derive; +/// +/// #[derive(libafl_derive::Display)] +/// struct MyStruct { +/// foo: String, +/// bar: Option, +/// } +/// ``` +/// +/// The above code will expand to: +/// +/// ```rust +/// struct MyStruct { +/// foo: String, +/// bar: Option, +/// } +/// +/// impl core::fmt::Display for MyStruct { +/// fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { +/// f.write_fmt(format_args!(" {0}", self.foo))?; +/// if let Some(opt) = &self.bar { +/// f.write_fmt(format_args!(" {0}", opt))?; +/// } +/// Ok(()) +/// } +/// } +/// ``` +#[proc_macro_derive(Display)] +pub fn libafl_display(input: TokenStream) -> TokenStream { + let DeriveInput { ident, data, .. } = parse_macro_input!(input as DeriveInput); + + if let Struct(s) = data { + if let Named(fields) = s.fields { + let fields_fmt = fields.named.iter().map(libafl_display_field_by_type); + + return quote! { + impl core::fmt::Display for #ident { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + #(#fields_fmt)* + Ok(()) + } + } + } + .into(); + } + } + panic!("Only structs are supported"); +} + +fn libafl_display_field_by_type(it: &Field) -> proc_macro2::TokenStream { + let fmt = " {}"; + let ident = &it.ident; + if let Type::Path(type_path) = &it.ty { + if type_path.qself.is_none() && type_path.path.segments.len() == 1 { + let segment = &type_path.path.segments[0]; + if segment.ident == "Option" { + return quote! { + if let Some(opt) = &self.#ident { + write!(f, #fmt, opt)?; + } + }; + } else if segment.ident == "Vec" { + return quote! { + for e in &self.#ident { + write!(f, #fmt, e)?; + } + }; + } + } + } + quote! { + write!(f, #fmt, self.#ident)?; + } +} diff --git a/libafl_qemu/Cargo.toml b/libafl_qemu/Cargo.toml index de5351a8ad..a11e0fc0f1 100644 --- a/libafl_qemu/Cargo.toml +++ b/libafl_qemu/Cargo.toml @@ -96,6 +96,7 @@ libafl_bolts = { path = "../libafl_bolts", version = "0.13.2", default-features ] } libafl_targets = { path = "../libafl_targets", version = "0.13.2" } libafl_qemu_sys = { path = "./libafl_qemu_sys", version = "0.13.2" } +libafl_derive = { path = "../libafl_derive", version = "0.13.2" } serde = { version = "1.0", default-features = false, features = [ "alloc", @@ -127,6 +128,7 @@ pyo3 = { version = "0.22", optional = true, features = ["multiple-pymethods"] } bytes-utils = "0.1" typed-builder = "0.19" memmap2 = "0.9" +getset = "0.1" # Document all features of this crate (for `cargo doc`) document-features = { version = "0.2", optional = true } diff --git a/libafl_qemu/build_linux.rs b/libafl_qemu/build_linux.rs index 83b3420590..a400351675 100644 --- a/libafl_qemu/build_linux.rs +++ b/libafl_qemu/build_linux.rs @@ -10,7 +10,7 @@ static LIBAFL_QEMU_RUNTIME_TEST: &str = r#" #include #include "libafl_qemu.h" -int main() {} +void __libafl_qemu_testfile() {} "#; #[allow(clippy::too_many_lines)] diff --git a/libafl_qemu/libafl_qemu_sys/src/systemmode.rs b/libafl_qemu/libafl_qemu_sys/src/systemmode.rs index d0d1c1d763..5850390455 100644 --- a/libafl_qemu/libafl_qemu_sys/src/systemmode.rs +++ b/libafl_qemu/libafl_qemu_sys/src/systemmode.rs @@ -3,7 +3,7 @@ use paste::paste; use crate::extern_c_checked; extern_c_checked! { - pub fn qemu_init(argc: i32, argv: *const *const u8, envp: *const *const u8); + pub fn qemu_init(argc: i32, argv: *const *const u8); pub fn vm_start(); pub fn qemu_main_loop(); diff --git a/libafl_qemu/src/qemu/mod.rs b/libafl_qemu/src/qemu/mod.rs index 7ae0735dd8..b6767406f1 100644 --- a/libafl_qemu/src/qemu/mod.rs +++ b/libafl_qemu/src/qemu/mod.rs @@ -1,7 +1,7 @@ //! Low-level QEMU library //! //! This module exposes the low-level QEMU library through [`Qemu`]. -//! To access higher-level features of QEMU, it is recommanded to use [`crate::Emulator`] instead. +//! To access higher-level features of QEMU, it is recommended to use [`crate::Emulator`] instead. use core::fmt; use std::{ @@ -33,6 +33,9 @@ use strum::IntoEnumIterator; use crate::{GuestAddrKind, GuestReg, Regs}; +pub mod qemu_config; +use qemu_config::{QemuConfig, QemuConfigBuilder, QEMU_CONFIG}; + #[cfg(emulation_mode = "usermode")] mod usermode; #[cfg(emulation_mode = "usermode")] @@ -519,6 +522,12 @@ impl From for HookData { #[allow(clippy::unused_self)] impl Qemu { + /// For more details about the parameters check + /// [the QEMU documentation](https://www.qemu.org/docs/master/about/). + pub fn builder() -> QemuConfigBuilder { + QemuConfig::builder() + } + #[allow(clippy::must_use_candidate, clippy::similar_names)] pub fn init(args: &[String], env: &[(String, String)]) -> Result { if args.is_empty() { @@ -557,7 +566,7 @@ impl Qemu { qemu_user_init(argc, argv.as_ptr(), envp.as_ptr()); #[cfg(emulation_mode = "systemmode")] { - qemu_init(argc, argv.as_ptr(), envp.as_ptr()); + qemu_init(argc, argv.as_ptr()); libc::atexit(qemu_cleanup_atexit); libafl_qemu_sys::syx_snapshot_init(true); } @@ -595,6 +604,14 @@ impl Qemu { } } + /// Get QEMU configuration. + /// Returns `Some` only if QEMU was initialized with the builder. + /// Returns `None` if QEMU was initialized with `init` and raw string args. + #[must_use] + pub fn get_config(&self) -> Option<&'static QemuConfig> { + QEMU_CONFIG.get() + } + /// This function will run the emulator until the next breakpoint / sync exit, or until finish. /// It is a low-level function and simply kicks QEMU. /// # Safety diff --git a/libafl_qemu/src/qemu/qemu_config.rs b/libafl_qemu/src/qemu/qemu_config.rs new file mode 100644 index 0000000000..d1ab3fff0a --- /dev/null +++ b/libafl_qemu/src/qemu/qemu_config.rs @@ -0,0 +1,406 @@ +use core::{ + fmt, + fmt::{Display, Formatter}, +}; +use std::{ + path::{Path, PathBuf}, + sync::OnceLock, +}; + +use getset::Getters; +use libafl_derive; +use strum_macros; +use typed_builder::TypedBuilder; + +use crate::{Qemu, QemuInitError}; + +pub(super) static QEMU_CONFIG: OnceLock = OnceLock::new(); + +#[cfg(emulation_mode = "systemmode")] +#[derive(Debug, strum_macros::Display, Clone)] +#[strum(prefix = "-accel ", serialize_all = "lowercase")] +pub enum Accelerator { + Kvm, + Tcg, +} + +#[derive(Debug, strum_macros::Display, Clone)] +#[strum(prefix = "if=", serialize_all = "lowercase")] +pub enum DriveInterface { + Floppy, + Ide, + Mtd, + None, + Pflash, + Scsi, + Sd, + Virtio, +} + +#[derive(Debug, strum_macros::Display, Clone)] +#[strum(prefix = "format=", serialize_all = "lowercase")] +pub enum DiskImageFileFormat { + Qcow2, + Raw, +} + +#[derive(Debug, Clone, Default, TypedBuilder)] +pub struct Drive { + #[builder(default, setter(strip_option, into))] + file: Option, + #[builder(default, setter(strip_option))] + format: Option, + #[builder(default, setter(strip_option))] + interface: Option, +} + +impl Display for Drive { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "-drive")?; + + let mut is_first_option = true; + let mut separator = || { + if is_first_option { + is_first_option = false; + " " + } else { + "," + } + }; + + if let Some(file) = &self.file { + write!(f, "{}file={}", separator(), file.to_str().unwrap())?; + } + if let Some(format) = &self.format { + write!(f, "{}{format}", separator())?; + } + if let Some(interface) = &self.interface { + write!(f, "{}{interface}", separator())?; + } + + Ok(()) + } +} + +#[derive(Debug, strum_macros::Display, Clone)] +#[strum(prefix = "-serial ", serialize_all = "lowercase")] +pub enum Serial { + None, + Null, + Stdio, +} + +#[derive(Debug, strum_macros::Display, Clone)] +#[strum(prefix = "-monitor ", serialize_all = "lowercase")] +pub enum Monitor { + None, + Null, + Stdio, +} + +/// Set the directory for the BIOS, VGA BIOS and keymaps. +/// Corresponds to the `-L` option of QEMU. +#[cfg(emulation_mode = "systemmode")] +#[derive(Debug, Clone)] +pub struct Bios { + path: PathBuf, +} + +#[cfg(emulation_mode = "systemmode")] +impl Display for Bios { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "-L {}", self.path.to_str().unwrap()) + } +} + +#[cfg(emulation_mode = "systemmode")] +impl> From for Bios { + fn from(path: R) -> Self { + Self { + path: path.as_ref().to_path_buf(), + } + } +} + +#[cfg(emulation_mode = "systemmode")] +#[derive(Debug, Clone)] +pub struct Kernel { + path: PathBuf, +} + +#[cfg(emulation_mode = "systemmode")] +impl Display for Kernel { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "-kernel {}", self.path.to_str().unwrap()) + } +} + +#[cfg(emulation_mode = "systemmode")] +impl> From for Kernel { + fn from(path: R) -> Self { + Self { + path: path.as_ref().to_path_buf(), + } + } +} + +#[derive(Debug, Clone)] +pub struct LoadVM { + path: PathBuf, +} + +impl Display for LoadVM { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "-loadvm {}", self.path.to_str().unwrap()) + } +} + +impl> From for LoadVM { + fn from(path: R) -> Self { + Self { + path: path.as_ref().to_path_buf(), + } + } +} + +#[derive(Debug, Clone)] +pub struct Machine { + name: String, +} + +impl Display for Machine { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "-machine {}", self.name) + } +} + +impl> From for Machine { + fn from(name: R) -> Self { + Self { + name: name.as_ref().to_string(), + } + } +} + +#[derive(Debug, Clone, strum_macros::Display)] +pub enum Snapshot { + #[strum(serialize = "-snapshot")] + ENABLE, + #[strum(serialize = "")] + DISABLE, +} + +impl From for Snapshot { + fn from(snapshot: bool) -> Self { + if snapshot { + Snapshot::ENABLE + } else { + Snapshot::DISABLE + } + } +} + +/// When set to DISABLE, corresponds to the `-S` option of QEMU. +#[derive(Debug, Clone, strum_macros::Display)] +pub enum StartCPU { + #[strum(serialize = "")] + ENABLE, + #[strum(serialize = "-S")] + DISABLE, +} + +impl From for StartCPU { + fn from(start_cpu: bool) -> Self { + if start_cpu { + StartCPU::ENABLE + } else { + StartCPU::DISABLE + } + } +} + +#[derive(Debug, Clone, strum_macros::Display)] +pub enum NoGraphic { + #[strum(serialize = "-nographic")] + ENABLE, + #[strum(serialize = "")] + DISABLE, +} + +impl From for NoGraphic { + fn from(no_graphic: bool) -> Self { + if no_graphic { + NoGraphic::ENABLE + } else { + NoGraphic::DISABLE + } + } +} + +#[derive(Debug, Clone)] +pub enum RamSize { + MB(u32), + GB(u32), +} + +impl Display for RamSize { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + RamSize::MB(mb) => write!(f, "-m {mb}M"), + RamSize::GB(gb) => write!(f, "-m {gb}G"), + } + } +} + +#[derive(Debug, Clone)] +pub struct SmpCpus { + pub cpus: u32, +} + +impl Display for SmpCpus { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "-smp {}", self.cpus) + } +} + +#[derive(Debug, Clone, strum_macros::Display)] +pub enum VgaPci { + #[strum(serialize = "-device VGA")] + ENABLE, + #[strum(serialize = "")] + DISABLE, +} + +impl From for VgaPci { + fn from(vga_pci: bool) -> Self { + if vga_pci { + VgaPci::ENABLE + } else { + VgaPci::DISABLE + } + } +} + +#[cfg(emulation_mode = "usermode")] +#[derive(Debug, Clone)] +pub struct Program { + path: PathBuf, +} + +#[cfg(emulation_mode = "usermode")] +impl Display for Program { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.path.to_str().unwrap()) + } +} + +#[cfg(emulation_mode = "usermode")] +impl> From for Program { + fn from(path: R) -> Self { + Self { + path: path.as_ref().to_path_buf(), + } + } +} + +#[derive(Debug, Clone, libafl_derive::Display, TypedBuilder, Getters)] +#[builder(build_method(into = Result), builder_method(vis = "pub(crate)", + doc = "Since Qemu is a zero sized struct, this is not a completely standard builder pattern. \ + The Qemu configuration is not stored in the Qemu struct after build() but in QEMU_CONFIG \ + Therefore, to use the derived builder and avoid boilerplate a builder for QemuConfig is \ + derived. \ + The QemuConfig::builder is called in Qemu::builder() which is the only place where it should \ + be called, in this way the one to one matching of Qemu and QemuConfig is enforced. Therefore \ + its visibility is pub(crate)"))] +#[getset(get = "pub")] +pub struct QemuConfig { + #[cfg(emulation_mode = "systemmode")] + #[builder(default, setter(strip_option))] + accelerator: Option, + #[cfg(emulation_mode = "systemmode")] + #[builder(default, setter(strip_option, into))] + bios: Option, + #[builder(default, setter(into))] + drives: Vec, + #[cfg(emulation_mode = "systemmode")] + #[builder(default, setter(strip_option, into))] + kernel: Option, + #[builder(default, setter(strip_option, into))] + load_vm: Option, + #[builder(default, setter(strip_option, into))] + machine: Option, + #[builder(default, setter(strip_option))] + monitor: Option, + #[builder(default, setter(strip_option, into))] + no_graphic: Option, + #[builder(default, setter(strip_option))] + ram_size: Option, + #[builder(default, setter(strip_option))] + serial: Option, + #[builder(default, setter(strip_option))] + smp_cpus: Option, + #[builder(default, setter(strip_option, into))] + snapshot: Option, + #[builder(default, setter(strip_option, into))] + vga_pci: Option, + #[builder(default, setter(strip_option, into))] + start_cpu: Option, + #[cfg(emulation_mode = "usermode")] + #[builder(setter(into))] + program: Program, +} // Adding something here? Please leave Program as the last field + +impl From for Result { + /// This method is necessary to make the API resemble a typical builder pattern, i.e. + /// `Qemu::builder().foo(bar).build()`, while still leveraging `TypedBuilder` for this + /// non-standard use case where `Qemu` doesn't store the configuration. + /// Internally, `TypedBuilder` is used to generate a builder for `QemuConfig`. + /// This `QemuConfig.into()` method is used by the derived `QemuConfigBuilder.build()` + /// to go from `QemuConfigBuilder` to `QemuConfig`, and finally to `Qemu` in one fn. + /// + /// # Errors + /// returns `QemuInitError` if the Qemu initialization fails, including cases where Qemu has + /// already been initialized. + fn from(config: QemuConfig) -> Self { + let args = config + .to_string() + .split(' ') + .map(std::string::ToString::to_string) + .collect::>(); + let qemu = Qemu::init(&args, &[])?; + QEMU_CONFIG + .set(config) + .map_err(|_| unreachable!("BUG: QEMU_CONFIG was already set but Qemu was not init!"))?; + Ok(qemu) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(emulation_mode = "usermode")] + fn usermode() { + let program = "/bin/pwd"; + let qemu = Qemu::builder().program("/bin/pwd").build().unwrap(); + let config = qemu.get_config().unwrap(); + assert_eq!(config.to_string().trim(), program.trim()); + } + + #[test] + fn drive_no_file_fmt() { + let drive = Drive::builder() + .format(DiskImageFileFormat::Raw) + .interface(DriveInterface::Ide) + .build(); + assert_eq!(drive.to_string(), "-drive format=raw,if=ide"); + } + + #[test] + #[cfg(emulation_mode = "systemmode")] + fn accelerator_kvm_to_string() { + let accel = Accelerator::Kvm; + assert_eq!(accel.to_string(), "-accel kvm"); + } +}