Soft recovery from crashes in libafl qemu usermode (#3073)

* soft recovery from crashes in qemu

* regen bindings for clippy

* configurable crash behaviour
This commit is contained in:
Romain Malmain 2025-03-14 16:56:03 +01:00 committed by GitHub
parent d4a86cdeeb
commit d67296f34e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 1028 additions and 925 deletions

View File

@ -103,6 +103,7 @@ log = "0.4.22"
meminterval = "0.4.1" meminterval = "0.4.1"
mimalloc = { version = "0.1.43", default-features = false } mimalloc = { version = "0.1.43", default-features = false }
nix = { version = "0.29.0", default-features = false } nix = { version = "0.29.0", default-features = false }
num-derive = { version = "0.4.2", default-features = false }
num_enum = { version = "0.7.3", default-features = false } num_enum = { version = "0.7.3", default-features = false }
num-traits = { version = "0.2.19", default-features = false } num-traits = { version = "0.2.19", default-features = false }
paste = "1.0.15" paste = "1.0.15"

View File

@ -53,7 +53,7 @@ use libafl_qemu::{
edges::StdEdgeCoverageModule, edges::StdEdgeCoverageModule,
}, },
Emulator, GuestReg, MmapPerms, QemuExecutor, QemuExitError, QemuExitReason, QemuShutdownCause, Emulator, GuestReg, MmapPerms, QemuExecutor, QemuExitError, QemuExitReason, QemuShutdownCause,
Regs, Regs, TargetSignalHandling,
}; };
use libafl_targets::{edges_map_mut_ptr, EDGES_MAP_ALLOCATED_SIZE, MAX_EDGES_FOUND}; use libafl_targets::{edges_map_mut_ptr, EDGES_MAP_ALLOCATED_SIZE, MAX_EDGES_FOUND};
#[cfg(unix)] #[cfg(unix)]
@ -194,6 +194,10 @@ fn fuzz(
.modules(modules) .modules(modules)
.build()?; .build()?;
// return to harness instead of crashing the process.
// greatly speeds up crash recovery.
emulator.set_target_crash_handling(&TargetSignalHandling::RaiseSignal);
let qemu = emulator.qemu(); let qemu = emulator.qemu();
let mut elf_buffer = Vec::new(); let mut elf_buffer = Vec::new();
@ -359,13 +363,15 @@ fn fuzz(
qemu.write_reg(Regs::Rip, test_one_input_ptr).unwrap(); qemu.write_reg(Regs::Rip, test_one_input_ptr).unwrap();
qemu.write_reg(Regs::Rsp, stack_ptr).unwrap(); qemu.write_reg(Regs::Rsp, stack_ptr).unwrap();
match qemu.run() { let qemu_ret = qemu.run();
match qemu_ret {
Ok(QemuExitReason::Breakpoint(_)) => {} Ok(QemuExitReason::Breakpoint(_)) => {}
Ok(QemuExitReason::End(QemuShutdownCause::HostSignal(signal))) => { Ok(QemuExitReason::Crash) => return ExitKind::Crash,
signal.handle(); Ok(QemuExitReason::Timeout) => return ExitKind::Timeout,
}
Err(QemuExitError::UnexpectedExit) => return ExitKind::Crash, Err(QemuExitError::UnexpectedExit) => return ExitKind::Crash,
_ => panic!("Unexpected QEMU exit."), _ => panic!("Unexpected QEMU exit: {qemu_ret:?}"),
} }
} }

View File

@ -109,7 +109,7 @@ hashbrown = { workspace = true, default-features = true, features = [
"serde", "serde",
] } # A faster hashmap, nostd compatible ] } # A faster hashmap, nostd compatible
num-traits = { workspace = true, default-features = true } num-traits = { workspace = true, default-features = true }
num-derive = "0.4.2" num-derive = { workspace = true }
num_enum = { workspace = true, default-features = true } num_enum = { workspace = true, default-features = true }
goblin = "0.9.2" goblin = "0.9.2"
libc = { workspace = true } libc = { workspace = true }
@ -133,6 +133,7 @@ bytes-utils = "0.1.4"
typed-builder = { workspace = true } typed-builder = { workspace = true }
memmap2 = "0.9.5" memmap2 = "0.9.5"
getset = "0.1.3" getset = "0.1.3"
# Document all features of this crate (for `cargo doc`) # Document all features of this crate (for `cargo doc`)
document-features = { workspace = true, optional = true } document-features = { workspace = true, optional = true }

View File

@ -11,7 +11,7 @@ use crate::cargo_add_rpath;
pub const QEMU_URL: &str = "https://github.com/AFLplusplus/qemu-libafl-bridge"; pub const QEMU_URL: &str = "https://github.com/AFLplusplus/qemu-libafl-bridge";
pub const QEMU_DIRNAME: &str = "qemu-libafl-bridge"; pub const QEMU_DIRNAME: &str = "qemu-libafl-bridge";
pub const QEMU_REVISION: &str = "3c60ef9b83107a160021075b485831edecb5a1c3"; pub const QEMU_REVISION: &str = "fea68856b9410ca6f0076a6bf9ccc4b4b11aa09c";
pub struct BuildResult { pub struct BuildResult {
pub qemu_path: PathBuf, pub qemu_path: PathBuf,

View File

@ -1,5 +1,5 @@
/* 1.87.0-nightly */ /* 1.87.0-nightly */
/* qemu git hash: 06c738f64a4a92d5fc8184c9b5a9fe9340f4a63f */ /* qemu git hash: fea68856b9410ca6f0076a6bf9ccc4b4b11aa09c */
/* automatically generated by rust-bindgen 0.71.1 */ /* automatically generated by rust-bindgen 0.71.1 */
#[repr(C)] #[repr(C)]

View File

@ -1,5 +1,5 @@
/* 1.87.0-nightly */ /* 1.87.0-nightly */
/* qemu git hash: 06c738f64a4a92d5fc8184c9b5a9fe9340f4a63f */ /* qemu git hash: fea68856b9410ca6f0076a6bf9ccc4b4b11aa09c */
/* automatically generated by rust-bindgen 0.71.1 */ /* automatically generated by rust-bindgen 0.71.1 */
#[repr(C)] #[repr(C)]

View File

@ -117,6 +117,7 @@ where
} }
} }
} }
impl<C, CM, ED, ET, QP, I, S, SM> EmulatorBuilder<C, CM, ED, ET, QP, I, S, SM> impl<C, CM, ED, ET, QP, I, S, SM> EmulatorBuilder<C, CM, ED, ET, QP, I, S, SM>
where where
I: Unpin, I: Unpin,

View File

@ -265,6 +265,9 @@ where
} }
_ => panic!("Unhandled QEMU shutdown cause: {shutdown_cause:?}."), _ => panic!("Unhandled QEMU shutdown cause: {shutdown_cause:?}."),
}, },
EmulatorExitResult::Crash => {
return Ok(Some(EmulatorDriverResult::EndOfRun(ExitKind::Crash)));
}
EmulatorExitResult::Timeout => { EmulatorExitResult::Timeout => {
return Ok(Some(EmulatorDriverResult::EndOfRun(ExitKind::Timeout))); return Ok(Some(EmulatorDriverResult::EndOfRun(ExitKind::Timeout)));
} }

View File

@ -188,6 +188,9 @@ where
} }
_ => panic!("Unhandled QEMU shutdown cause: {shutdown_cause:?}."), _ => panic!("Unhandled QEMU shutdown cause: {shutdown_cause:?}."),
}, },
EmulatorExitResult::Crash => {
return Ok(Some(EmulatorDriverResult::EndOfRun(ExitKind::Crash)));
}
EmulatorExitResult::Timeout => { EmulatorExitResult::Timeout => {
return Ok(Some(EmulatorDriverResult::EndOfRun(ExitKind::Timeout))); return Ok(Some(EmulatorDriverResult::EndOfRun(ExitKind::Timeout)));
} }

View File

@ -57,6 +57,7 @@ pub enum EmulatorExitResult<C> {
QemuExit(QemuShutdownCause), // QEMU ended for some reason. QemuExit(QemuShutdownCause), // QEMU ended for some reason.
Breakpoint(Breakpoint<C>), // Breakpoint triggered. Contains the address of the trigger. Breakpoint(Breakpoint<C>), // Breakpoint triggered. Contains the address of the trigger.
CustomInsn(CustomInsn<C>), // Synchronous backdoor: The guest triggered a backdoor and should return to LibAFL. CustomInsn(CustomInsn<C>), // Synchronous backdoor: The guest triggered a backdoor and should return to LibAFL.
Crash, // Crash
Timeout, // Timeout Timeout, // Timeout
} }
@ -75,6 +76,9 @@ where
EmulatorExitResult::CustomInsn(sync_exit) => { EmulatorExitResult::CustomInsn(sync_exit) => {
write!(f, "{sync_exit:?}") write!(f, "{sync_exit:?}")
} }
EmulatorExitResult::Crash => {
write!(f, "Crash")
}
EmulatorExitResult::Timeout => { EmulatorExitResult::Timeout => {
write!(f, "Timeout") write!(f, "Timeout")
} }
@ -219,6 +223,9 @@ where
EmulatorExitResult::CustomInsn(sync_exit) => { EmulatorExitResult::CustomInsn(sync_exit) => {
write!(f, "Sync exit: {sync_exit:?}") write!(f, "Sync exit: {sync_exit:?}")
} }
EmulatorExitResult::Crash => {
write!(f, "Crash")
}
EmulatorExitResult::Timeout => { EmulatorExitResult::Timeout => {
write!(f, "Timeout") write!(f, "Timeout")
} }
@ -455,6 +462,7 @@ where
QemuExitReason::End(qemu_shutdown_cause) => { QemuExitReason::End(qemu_shutdown_cause) => {
EmulatorExitResult::QemuExit(qemu_shutdown_cause) EmulatorExitResult::QemuExit(qemu_shutdown_cause)
} }
QemuExitReason::Crash => EmulatorExitResult::Crash,
QemuExitReason::Timeout => EmulatorExitResult::Timeout, QemuExitReason::Timeout => EmulatorExitResult::Timeout,
QemuExitReason::Breakpoint(bp_addr) => { QemuExitReason::Breakpoint(bp_addr) => {
let bp = self let bp = self

View File

@ -1,6 +1,6 @@
use libafl_qemu_sys::{GuestAddr, MmapPerms, VerifyAccess}; use libafl_qemu_sys::{GuestAddr, MmapPerms, VerifyAccess};
use crate::{Emulator, GuestMaps, NopSnapshotManager}; use crate::{Emulator, GuestMaps, NopSnapshotManager, TargetSignalHandling};
pub type StdSnapshotManager = NopSnapshotManager; pub type StdSnapshotManager = NopSnapshotManager;
@ -88,4 +88,12 @@ impl<C, CM, ED, ET, I, S, SM> Emulator<C, CM, ED, ET, I, S, SM> {
pub fn unmap(&self, addr: GuestAddr, size: usize) -> Result<(), String> { pub fn unmap(&self, addr: GuestAddr, size: usize) -> Result<(), String> {
self.qemu.unmap(addr, size) self.qemu.unmap(addr, size)
} }
/// Set how QEMU should handle crashing signals:
/// Check [`TargetSignalHandling`] documentation for more details.
pub fn set_target_crash_handling(&self, handling: &TargetSignalHandling) {
unsafe {
self.qemu.set_target_crash_handling(handling);
}
}
} }

View File

@ -45,6 +45,9 @@ use crate::{EmulatorModules, Qemu, QemuSignalContext, run_target_crash_hooks};
type EmulatorInProcessExecutor<'a, C, CM, ED, EM, ET, H, I, OT, S, SM, Z> = type EmulatorInProcessExecutor<'a, C, CM, ED, EM, ET, H, I, OT, S, SM, Z> =
StatefulInProcessExecutor<'a, EM, Emulator<C, CM, ED, ET, I, S, SM>, H, I, OT, S, Z>; StatefulInProcessExecutor<'a, EM, Emulator<C, CM, ED, ET, I, S, SM>, H, I, OT, S, Z>;
#[cfg(feature = "systemmode")]
pub(crate) static BREAK_ON_TMOUT: AtomicBool = AtomicBool::new(false);
pub struct QemuExecutor<'a, C, CM, ED, EM, ET, H, I, OT, S, SM, Z> { pub struct QemuExecutor<'a, C, CM, ED, EM, ET, H, I, OT, S, SM, Z> {
inner: EmulatorInProcessExecutor<'a, C, CM, ED, EM, ET, H, I, OT, S, SM, Z>, inner: EmulatorInProcessExecutor<'a, C, CM, ED, EM, ET, H, I, OT, S, SM, Z>,
first_exec: bool, first_exec: bool,
@ -99,7 +102,7 @@ pub unsafe fn inproc_qemu_crash_handler<E, EM, ET, I, OF, S, Z>(
qemu.run_signal_handler(signal.into(), info, puc); qemu.run_signal_handler(signal.into(), info, puc);
} }
// if we are there, we can safely to execution // if we are there, we can safely resume from the signal handler.
return; return;
} }
QemuSignalContext::InQemuSignalHandlerHost => { QemuSignalContext::InQemuSignalHandlerHost => {
@ -120,7 +123,7 @@ pub unsafe fn inproc_qemu_crash_handler<E, EM, ET, I, OF, S, Z>(
// run qemu hooks then report the crash. // run qemu hooks then report the crash.
log::debug!( log::debug!(
"QEMU Target signal received that should be handled by host. Most likely a target crash." "QEMU Target signal received that should be handled by host. It is a target crash."
); );
log::debug!("Running crash hooks."); log::debug!("Running crash hooks.");
@ -157,9 +160,6 @@ pub unsafe fn inproc_qemu_crash_handler<E, EM, ET, I, OF, S, Z>(
} }
} }
#[cfg(feature = "systemmode")]
pub(crate) static BREAK_ON_TMOUT: AtomicBool = AtomicBool::new(false);
/// # Safety /// # Safety
/// Can call through the `unix_signal_handler::inproc_timeout_handler`. /// Can call through the `unix_signal_handler::inproc_timeout_handler`.
/// Calling this method multiple times concurrently can lead to race conditions. /// Calling this method multiple times concurrently can lead to race conditions.

View File

@ -101,6 +101,9 @@ pub enum QemuExitReason {
/// Synchronous exit: The guest triggered a backdoor and should return to `LibAFL`. /// Synchronous exit: The guest triggered a backdoor and should return to `LibAFL`.
SyncExit, SyncExit,
// Target crash, and it has been requested to be handled by the harness.
Crash,
/// Timeout, and it has been requested to be handled by the harness. /// Timeout, and it has been requested to be handled by the harness.
Timeout, Timeout,
} }
@ -184,6 +187,7 @@ impl Display for QemuExitReason {
QemuExitReason::End(shutdown_cause) => write!(f, "End: {shutdown_cause:?}"), QemuExitReason::End(shutdown_cause) => write!(f, "End: {shutdown_cause:?}"),
QemuExitReason::Breakpoint(bp) => write!(f, "Breakpoint: {bp}"), QemuExitReason::Breakpoint(bp) => write!(f, "Breakpoint: {bp}"),
QemuExitReason::SyncExit => write!(f, "Sync Exit"), QemuExitReason::SyncExit => write!(f, "Sync Exit"),
QemuExitReason::Crash => write!(f, "Crash"),
QemuExitReason::Timeout => write!(f, "Timeout"), QemuExitReason::Timeout => write!(f, "Timeout"),
} }
} }
@ -711,6 +715,8 @@ impl Qemu {
#[cfg(feature = "systemmode")] #[cfg(feature = "systemmode")]
libafl_qemu_sys::libafl_exit_reason_kind_TIMEOUT => QemuExitReason::Timeout, libafl_qemu_sys::libafl_exit_reason_kind_TIMEOUT => QemuExitReason::Timeout,
libafl_qemu_sys::libafl_exit_reason_kind_CRASH => QemuExitReason::Crash,
_ => return Err(QemuExitError::UnknownKind), _ => return Err(QemuExitError::UnknownKind),
}) })
} }

View File

@ -16,11 +16,38 @@ use pyo3::{IntoPyObject, Py, PyRef, PyRefMut, Python, pyclass, pymethods};
use crate::{CPU, Qemu, qemu::QEMU_IS_RUNNING}; use crate::{CPU, Qemu, qemu::QEMU_IS_RUNNING};
/// Choose how QEMU target signals should be handled.
/// It's main use is to describe how crashes and timeouts should be treated.
pub enum TargetSignalHandling {
/// Return to harness with the associated exit request on target crashing or timeout signal.
/// The snapshot mechanism should make sure to recover correctly from the crash.
/// For instance, snapshots do not take into account side effects related to file descriptors.
/// If it could have an impact in case of a crash, prefer the other policy.
///
/// *Warning*: this policy should be used with [`SnapshotModule`]. It can be used without
/// snapshotting, but it is up to the user to make sure the recovery is possible without
/// corrupting the target.
ReturnToHarness,
/// Propagate target signal to host (following QEMU target to host signal translation) by
/// raising the proper signal.
/// This the safe policy, since the target is completely reset.
/// However, it could make the fuzzer much slower if many crashes are triggered during the
/// fuzzing campaign.
RaiseSignal,
}
pub struct QemuMappingsViewer<'a> { pub struct QemuMappingsViewer<'a> {
qemu: &'a Qemu, qemu: &'a Qemu,
mappings: Vec<MapInfo>, mappings: Vec<MapInfo>,
} }
impl Default for TargetSignalHandling {
/// Historically, `LibAFL` QEMU raises the target signal to the host.
fn default() -> Self {
TargetSignalHandling::RaiseSignal
}
}
impl<'a> QemuMappingsViewer<'a> { impl<'a> QemuMappingsViewer<'a> {
/// Capture the memory mappings of Qemu at the moment when we create this object /// Capture the memory mappings of Qemu at the moment when we create this object
/// Thus if qemu make updates to the mappings, they won't be reflected to this object. /// Thus if qemu make updates to the mappings, they won't be reflected to this object.
@ -411,6 +438,24 @@ impl Qemu {
libc::raise(signal.into()); libc::raise(signal.into());
} }
} }
/// Set the target crash handling policy according to [`TargetSignalHandling`]'s documentation.
///
/// # Safety
///
/// It has an important impact on how crashes are handled by QEMU on target crashing signals.
/// Please make sure to read the documentation of [`TargetSignalHandling`] before touching
/// this.
pub unsafe fn set_target_crash_handling(&self, handling: &TargetSignalHandling) {
match handling {
TargetSignalHandling::ReturnToHarness => unsafe {
libafl_qemu_sys::libafl_set_return_on_crash(true);
},
TargetSignalHandling::RaiseSignal => unsafe {
libafl_qemu_sys::libafl_set_return_on_crash(false);
},
}
}
} }
#[cfg(feature = "python")] #[cfg(feature = "python")]