qemu: Add QemuConfig to set qemu args via a struct (#2339)

* Add QemuConfig to set qemu args via a struct

* Add derive macro to automate the qemu string args generation

* fix tests
This commit is contained in:
Marco C. 2024-08-14 15:30:14 +02:00 committed by GitHub
parent 6979032ad9
commit 21051dc26f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 538 additions and 15 deletions

View File

@ -5,7 +5,7 @@ macro_rules! assert_unique_feature {
() => {}; () => {};
($first:tt $(,$rest:tt)*) => { ($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")); compile_error!(concat!("features \"", $first, "\" and \"", $rest, "\" cannot be used together"));
)* )*
assert_unique_feature!($($rest),*); assert_unique_feature!($($rest),*);

View File

@ -36,8 +36,8 @@ use libafl_qemu::{
modules::edges::{ modules::edges::{
edges_map_mut_ptr, EdgeCoverageModule, EDGES_MAP_SIZE_IN_USE, MAX_EDGES_FOUND, edges_map_mut_ptr, EdgeCoverageModule, EDGES_MAP_SIZE_IN_USE, MAX_EDGES_FOUND,
}, },
Emulator, NopEmulatorExitHandler, QemuExitError, QemuExitReason, QemuRWError, qemu_config, Emulator, NopEmulatorExitHandler, Qemu, QemuExitError, QemuExitReason,
QemuShutdownCause, Regs, QemuRWError, QemuShutdownCause, Regs,
}; };
use libafl_qemu_sys::GuestPhysAddr; use libafl_qemu_sys::GuestPhysAddr;
@ -87,23 +87,34 @@ pub fn fuzz() {
println!("Breakpoint address = {breakpoint:#x}"); println!("Breakpoint address = {breakpoint:#x}");
let mut run_client = |state: Option<_>, mut mgr, _core_id| { 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 // Initialize QEMU
let args: Vec<String> = env::args().collect(); let qemu = Qemu::builder()
let env: Vec<(String, String)> = env::vars().collect(); .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_modules = tuple_list!(EdgeCoverageModule::default());
let emulator = Emulator::new( let mut emulator = Emulator::new_with_qemu(
args.as_slice(), qemu,
env.as_slice(),
emulator_modules, emulator_modules,
NopEmulatorExitHandler, NopEmulatorExitHandler,
NopCommandManager, NopCommandManager,
) )
.unwrap(); .unwrap();
let qemu = emulator.qemu();
qemu.set_breakpoint(main_addr); qemu.set_breakpoint(main_addr);
unsafe { unsafe {

View File

@ -23,3 +23,4 @@ proc-macro = true
[dependencies] [dependencies]
syn = { version = "2", features = ["full", "extra-traits"] } syn = { version = "2", features = ["full", "extra-traits"] }
quote = "1" quote = "1"
proc-macro2 = "1.0"

View File

@ -59,7 +59,7 @@
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::quote; 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` /// Derive macro to implement `SerdeAny`, to use a type in a `SerdeAnyMap`
#[proc_macro_derive(SerdeAny)] #[proc_macro_derive(SerdeAny)]
@ -69,3 +69,89 @@ pub fn libafl_serdeany_derive(input: TokenStream) -> TokenStream {
libafl_bolts::impl_serdeany!(#name); 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<u32>,
/// }
/// ```
///
/// The above code will expand to:
///
/// ```rust
/// struct MyStruct {
/// foo: String,
/// bar: Option<u32>,
/// }
///
/// 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)?;
}
}

View File

@ -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_targets = { path = "../libafl_targets", version = "0.13.2" }
libafl_qemu_sys = { path = "./libafl_qemu_sys", 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 = [ serde = { version = "1.0", default-features = false, features = [
"alloc", "alloc",
@ -127,6 +128,7 @@ pyo3 = { version = "0.22", optional = true, features = ["multiple-pymethods"] }
bytes-utils = "0.1" bytes-utils = "0.1"
typed-builder = "0.19" typed-builder = "0.19"
memmap2 = "0.9" memmap2 = "0.9"
getset = "0.1"
# Document all features of this crate (for `cargo doc`) # Document all features of this crate (for `cargo doc`)
document-features = { version = "0.2", optional = true } document-features = { version = "0.2", optional = true }

View File

@ -10,7 +10,7 @@ static LIBAFL_QEMU_RUNTIME_TEST: &str = r#"
#include <stdio.h> #include <stdio.h>
#include "libafl_qemu.h" #include "libafl_qemu.h"
int main() {} void __libafl_qemu_testfile() {}
"#; "#;
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]

View File

@ -3,7 +3,7 @@ use paste::paste;
use crate::extern_c_checked; use crate::extern_c_checked;
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 vm_start();
pub fn qemu_main_loop(); pub fn qemu_main_loop();

View File

@ -1,7 +1,7 @@
//! Low-level QEMU library //! Low-level QEMU library
//! //!
//! This module exposes the low-level QEMU library through [`Qemu`]. //! 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 core::fmt;
use std::{ use std::{
@ -33,6 +33,9 @@ use strum::IntoEnumIterator;
use crate::{GuestAddrKind, GuestReg, Regs}; use crate::{GuestAddrKind, GuestReg, Regs};
pub mod qemu_config;
use qemu_config::{QemuConfig, QemuConfigBuilder, QEMU_CONFIG};
#[cfg(emulation_mode = "usermode")] #[cfg(emulation_mode = "usermode")]
mod usermode; mod usermode;
#[cfg(emulation_mode = "usermode")] #[cfg(emulation_mode = "usermode")]
@ -519,6 +522,12 @@ impl From<u8> for HookData {
#[allow(clippy::unused_self)] #[allow(clippy::unused_self)]
impl Qemu { 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)] #[allow(clippy::must_use_candidate, clippy::similar_names)]
pub fn init(args: &[String], env: &[(String, String)]) -> Result<Self, QemuInitError> { pub fn init(args: &[String], env: &[(String, String)]) -> Result<Self, QemuInitError> {
if args.is_empty() { if args.is_empty() {
@ -557,7 +566,7 @@ impl Qemu {
qemu_user_init(argc, argv.as_ptr(), envp.as_ptr()); qemu_user_init(argc, argv.as_ptr(), envp.as_ptr());
#[cfg(emulation_mode = "systemmode")] #[cfg(emulation_mode = "systemmode")]
{ {
qemu_init(argc, argv.as_ptr(), envp.as_ptr()); qemu_init(argc, argv.as_ptr());
libc::atexit(qemu_cleanup_atexit); libc::atexit(qemu_cleanup_atexit);
libafl_qemu_sys::syx_snapshot_init(true); 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. /// 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. /// It is a low-level function and simply kicks QEMU.
/// # Safety /// # Safety

View File

@ -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<QemuConfig> = 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<PathBuf>,
#[builder(default, setter(strip_option))]
format: Option<DiskImageFileFormat>,
#[builder(default, setter(strip_option))]
interface: Option<DriveInterface>,
}
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<R: AsRef<Path>> From<R> 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<R: AsRef<Path>> From<R> 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<R: AsRef<Path>> From<R> 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<R: AsRef<str>> From<R> 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<bool> 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<bool> 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<bool> 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<bool> 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<R: AsRef<Path>> From<R> 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<Qemu, QemuInitError>), 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<Accelerator>,
#[cfg(emulation_mode = "systemmode")]
#[builder(default, setter(strip_option, into))]
bios: Option<Bios>,
#[builder(default, setter(into))]
drives: Vec<Drive>,
#[cfg(emulation_mode = "systemmode")]
#[builder(default, setter(strip_option, into))]
kernel: Option<Kernel>,
#[builder(default, setter(strip_option, into))]
load_vm: Option<LoadVM>,
#[builder(default, setter(strip_option, into))]
machine: Option<Machine>,
#[builder(default, setter(strip_option))]
monitor: Option<Monitor>,
#[builder(default, setter(strip_option, into))]
no_graphic: Option<NoGraphic>,
#[builder(default, setter(strip_option))]
ram_size: Option<RamSize>,
#[builder(default, setter(strip_option))]
serial: Option<Serial>,
#[builder(default, setter(strip_option))]
smp_cpus: Option<SmpCpus>,
#[builder(default, setter(strip_option, into))]
snapshot: Option<Snapshot>,
#[builder(default, setter(strip_option, into))]
vga_pci: Option<VgaPci>,
#[builder(default, setter(strip_option, into))]
start_cpu: Option<StartCPU>,
#[cfg(emulation_mode = "usermode")]
#[builder(setter(into))]
program: Program,
} // Adding something here? Please leave Program as the last field
impl From<QemuConfig> for Result<Qemu, QemuInitError> {
/// 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::<Vec<String>>();
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");
}
}