From 75feedd1a092eadf5ea640527c00cb90f35c5fa7 Mon Sep 17 00:00:00 2001 From: Romain Malmain Date: Fri, 31 Jan 2025 15:43:50 +0100 Subject: [PATCH] Add builder and tests for QASAN (#2898) * Add tests for QASAN from aflplusplus * refactor asan module to use the builder pattern * move injection tests to the new tests directory --- .../binary_only/qemu_launcher/Makefile.toml | 31 +- .../binary_only/qemu_launcher/src/client.rs | 64 ++- .../injection}/.gitignore | 0 .../injection}/Makefile | 0 .../injection}/README.md | 0 .../injection}/example.db | Bin .../injection}/sqltest.c | 0 .../qemu_launcher/tests/injection/test.sh | 30 + .../qemu_launcher/tests/qasan/Makefile | 7 + .../tests/qasan/inputs/double_free.txt | 1 + .../tests/qasan/inputs/memset.txt | 1 + .../tests/qasan/inputs/overflow.txt | 1 + .../tests/qasan/inputs/test_limits.txt | 1 + .../qemu_launcher/tests/qasan/inputs/uaf.txt | 1 + .../tests/qasan/inputs/underflow.txt | 1 + .../qemu_launcher/tests/qasan/qasan.c | 85 +++ .../qemu_launcher/tests/qasan/test.sh | 69 +++ libafl_qemu/src/modules/calls.rs | 5 +- libafl_qemu/src/modules/usermode/asan.rs | 522 +++++++++++------- 19 files changed, 574 insertions(+), 245 deletions(-) rename fuzzers/binary_only/qemu_launcher/{injection_test => tests/injection}/.gitignore (100%) rename fuzzers/binary_only/qemu_launcher/{injection_test => tests/injection}/Makefile (100%) rename fuzzers/binary_only/qemu_launcher/{injection_test => tests/injection}/README.md (100%) rename fuzzers/binary_only/qemu_launcher/{injection_test => tests/injection}/example.db (100%) rename fuzzers/binary_only/qemu_launcher/{injection_test => tests/injection}/sqltest.c (100%) create mode 100755 fuzzers/binary_only/qemu_launcher/tests/injection/test.sh create mode 100644 fuzzers/binary_only/qemu_launcher/tests/qasan/Makefile create mode 100644 fuzzers/binary_only/qemu_launcher/tests/qasan/inputs/double_free.txt create mode 100644 fuzzers/binary_only/qemu_launcher/tests/qasan/inputs/memset.txt create mode 100644 fuzzers/binary_only/qemu_launcher/tests/qasan/inputs/overflow.txt create mode 100644 fuzzers/binary_only/qemu_launcher/tests/qasan/inputs/test_limits.txt create mode 100644 fuzzers/binary_only/qemu_launcher/tests/qasan/inputs/uaf.txt create mode 100644 fuzzers/binary_only/qemu_launcher/tests/qasan/inputs/underflow.txt create mode 100644 fuzzers/binary_only/qemu_launcher/tests/qasan/qasan.c create mode 100755 fuzzers/binary_only/qemu_launcher/tests/qasan/test.sh diff --git a/fuzzers/binary_only/qemu_launcher/Makefile.toml b/fuzzers/binary_only/qemu_launcher/Makefile.toml index 5abd78d8ec..23c00f433a 100644 --- a/fuzzers/binary_only/qemu_launcher/Makefile.toml +++ b/fuzzers/binary_only/qemu_launcher/Makefile.toml @@ -1,3 +1,14 @@ +env_scripts = [''' +#!@duckscript +profile = get_env PROFILE + +if eq ${profile} "dev" + set_env PROFILE_DIR debug +else + set_env PROFILE_DIR ${profile} +end +'''] + [env] PROFILE = { value = "release", condition = { env_not_set = ["PROFILE"] } } PROFILE_DIR = { source = "${PROFILE}", default_value = "release", mapping = { "release" = "release", "dev" = "debug" }, condition = { env_not_set = [ @@ -360,21 +371,11 @@ windows_alias = "unsupported" script_runner = "@shell" script = ''' echo "Profile: ${PROFILE}" -cd injection_test || exit 1 -make -mkdir in || true -echo aaaaaaaaaa > in/a -timeout 10s "$(find ${TARGET_DIR} -name 'qemu_launcher')" -o out -i in -j ../injections.toml -v -- ./static >/dev/null 2>fuzz.log || true -if [ -z "$(grep -Ei "found.*injection" fuzz.log)" ]; then - echo "Fuzzer does not generate any testcases or any crashes" - echo "Logs:" - cat fuzz.log - exit 1 -else - echo "Fuzzer is working" -fi -make clean -#rm -rf in out fuzz.log || true + +export QEMU_LAUNCHER=${TARGET_DIR}/${PROFILE_DIR}/qemu_launcher + +./tests/injection/test.sh || exit 1 +./tests/qasan/test.sh || exit 1 ''' dependencies = ["build_unix"] diff --git a/fuzzers/binary_only/qemu_launcher/src/client.rs b/fuzzers/binary_only/qemu_launcher/src/client.rs index f416508860..d70ad67ba5 100644 --- a/fuzzers/binary_only/qemu_launcher/src/client.rs +++ b/fuzzers/binary_only/qemu_launcher/src/client.rs @@ -94,6 +94,8 @@ impl Client<'_> { let is_cmplog = self.options.is_cmplog_core(core_id); + let is_drcov = self.options.drcov.is_some(); + let extra_tokens = if cfg!(feature = "injections") { injection_module .as_ref() @@ -109,24 +111,47 @@ impl Client<'_> { .client_description(client_description) .extra_tokens(extra_tokens); - if self.options.rerun_input.is_some() && self.options.drcov.is_some() { - // Special code path for re-running inputs with DrCov. - // TODO: Add ASan support, injection support - let drcov = self.options.drcov.as_ref().unwrap(); - let drcov = DrCovModule::builder() - .filename(drcov.clone()) - .full_trace(true) - .build(); - instance_builder - .build() - .run(args, tuple_list!(drcov), state) + if self.options.rerun_input.is_some() { + if is_drcov { + // Special code path for re-running inputs with DrCov and Asan. + // TODO: Add injection support + let drcov = self.options.drcov.as_ref().unwrap(); + + if is_asan { + let modules = tuple_list!( + DrCovModule::builder() + .filename(drcov.clone()) + .full_trace(true) + .build(), + unsafe { AsanModule::builder().env(&env).asan_report().build() } + ); + + instance_builder.build().run(args, modules, state) + } else { + let modules = tuple_list!(DrCovModule::builder() + .filename(drcov.clone()) + .full_trace(true) + .build(),); + + instance_builder.build().run(args, modules, state) + } + } else if is_asan { + let modules = + tuple_list!(unsafe { AsanModule::builder().env(&env).asan_report().build() }); + + instance_builder.build().run(args, modules, state) + } else { + let modules = tuple_list!(); + + instance_builder.build().run(args, modules, state) + } } else if is_asan && is_cmplog { if let Some(injection_module) = injection_module { instance_builder.build().run( args, tuple_list!( CmpLogModule::default(), - AsanModule::default(&env), + AsanModule::builder().env(&env).build(), injection_module, ), state, @@ -134,7 +159,10 @@ impl Client<'_> { } else { instance_builder.build().run( args, - tuple_list!(CmpLogModule::default(), AsanModule::default(&env),), + tuple_list!( + CmpLogModule::default(), + AsanModule::builder().env(&env).build() + ), state, ) } @@ -160,13 +188,15 @@ impl Client<'_> { if let Some(injection_module) = injection_module { instance_builder.build().run( args, - tuple_list!(AsanModule::default(&env), injection_module), + tuple_list!(AsanModule::builder().env(&env).build(), injection_module), state, ) } else { - instance_builder - .build() - .run(args, tuple_list!(AsanModule::default(&env),), state) + instance_builder.build().run( + args, + tuple_list!(AsanModule::builder().env(&env).build()), + state, + ) } } else if is_asan_guest { instance_builder diff --git a/fuzzers/binary_only/qemu_launcher/injection_test/.gitignore b/fuzzers/binary_only/qemu_launcher/tests/injection/.gitignore similarity index 100% rename from fuzzers/binary_only/qemu_launcher/injection_test/.gitignore rename to fuzzers/binary_only/qemu_launcher/tests/injection/.gitignore diff --git a/fuzzers/binary_only/qemu_launcher/injection_test/Makefile b/fuzzers/binary_only/qemu_launcher/tests/injection/Makefile similarity index 100% rename from fuzzers/binary_only/qemu_launcher/injection_test/Makefile rename to fuzzers/binary_only/qemu_launcher/tests/injection/Makefile diff --git a/fuzzers/binary_only/qemu_launcher/injection_test/README.md b/fuzzers/binary_only/qemu_launcher/tests/injection/README.md similarity index 100% rename from fuzzers/binary_only/qemu_launcher/injection_test/README.md rename to fuzzers/binary_only/qemu_launcher/tests/injection/README.md diff --git a/fuzzers/binary_only/qemu_launcher/injection_test/example.db b/fuzzers/binary_only/qemu_launcher/tests/injection/example.db similarity index 100% rename from fuzzers/binary_only/qemu_launcher/injection_test/example.db rename to fuzzers/binary_only/qemu_launcher/tests/injection/example.db diff --git a/fuzzers/binary_only/qemu_launcher/injection_test/sqltest.c b/fuzzers/binary_only/qemu_launcher/tests/injection/sqltest.c similarity index 100% rename from fuzzers/binary_only/qemu_launcher/injection_test/sqltest.c rename to fuzzers/binary_only/qemu_launcher/tests/injection/sqltest.c diff --git a/fuzzers/binary_only/qemu_launcher/tests/injection/test.sh b/fuzzers/binary_only/qemu_launcher/tests/injection/test.sh new file mode 100755 index 0000000000..e24cf32613 --- /dev/null +++ b/fuzzers/binary_only/qemu_launcher/tests/injection/test.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +if [[ ! -x "$QEMU_LAUNCHER" ]]; then + echo "env variable QEMU_LAUNCHER does not point to a valid executable" + echo "QEMU_LAUNCHER should point to qemu_launcher location, but points to ${QEMU_LAUNCHER} instead." + exit 1 +fi + +cd "$SCRIPT_DIR" + +make + +mkdir in || true + +echo aaaaaaaaaa > in/a + +timeout 10s "$QEMU_LAUNCHER" -o out -i in -j ../../injections.toml -v -- ./static >/dev/null 2>fuzz.log || true +if ! grep -Ei "found.*injection" fuzz.log; then + echo "Fuzzer does not generate any testcases or any crashes" + echo "Logs:" + cat fuzz.log + exit 1 +else + echo "Fuzzer is working" +fi + +make clean \ No newline at end of file diff --git a/fuzzers/binary_only/qemu_launcher/tests/qasan/Makefile b/fuzzers/binary_only/qemu_launcher/tests/qasan/Makefile new file mode 100644 index 0000000000..5ed1a5a061 --- /dev/null +++ b/fuzzers/binary_only/qemu_launcher/tests/qasan/Makefile @@ -0,0 +1,7 @@ +all: qasan + +qasan: qasan.c + gcc qasan.c -o qasan + +clean: + rm -rf qasan out stats.txt diff --git a/fuzzers/binary_only/qemu_launcher/tests/qasan/inputs/double_free.txt b/fuzzers/binary_only/qemu_launcher/tests/qasan/inputs/double_free.txt new file mode 100644 index 0000000000..1784810501 --- /dev/null +++ b/fuzzers/binary_only/qemu_launcher/tests/qasan/inputs/double_free.txt @@ -0,0 +1 @@ +D diff --git a/fuzzers/binary_only/qemu_launcher/tests/qasan/inputs/memset.txt b/fuzzers/binary_only/qemu_launcher/tests/qasan/inputs/memset.txt new file mode 100644 index 0000000000..ef6bce1d1d --- /dev/null +++ b/fuzzers/binary_only/qemu_launcher/tests/qasan/inputs/memset.txt @@ -0,0 +1 @@ +M \ No newline at end of file diff --git a/fuzzers/binary_only/qemu_launcher/tests/qasan/inputs/overflow.txt b/fuzzers/binary_only/qemu_launcher/tests/qasan/inputs/overflow.txt new file mode 100644 index 0000000000..2638c45f8e --- /dev/null +++ b/fuzzers/binary_only/qemu_launcher/tests/qasan/inputs/overflow.txt @@ -0,0 +1 @@ +O diff --git a/fuzzers/binary_only/qemu_launcher/tests/qasan/inputs/test_limits.txt b/fuzzers/binary_only/qemu_launcher/tests/qasan/inputs/test_limits.txt new file mode 100644 index 0000000000..96583aabf5 --- /dev/null +++ b/fuzzers/binary_only/qemu_launcher/tests/qasan/inputs/test_limits.txt @@ -0,0 +1 @@ +T \ No newline at end of file diff --git a/fuzzers/binary_only/qemu_launcher/tests/qasan/inputs/uaf.txt b/fuzzers/binary_only/qemu_launcher/tests/qasan/inputs/uaf.txt new file mode 100644 index 0000000000..8c7e5a667f --- /dev/null +++ b/fuzzers/binary_only/qemu_launcher/tests/qasan/inputs/uaf.txt @@ -0,0 +1 @@ +A \ No newline at end of file diff --git a/fuzzers/binary_only/qemu_launcher/tests/qasan/inputs/underflow.txt b/fuzzers/binary_only/qemu_launcher/tests/qasan/inputs/underflow.txt new file mode 100644 index 0000000000..765140b219 --- /dev/null +++ b/fuzzers/binary_only/qemu_launcher/tests/qasan/inputs/underflow.txt @@ -0,0 +1 @@ +U diff --git a/fuzzers/binary_only/qemu_launcher/tests/qasan/qasan.c b/fuzzers/binary_only/qemu_launcher/tests/qasan/qasan.c new file mode 100644 index 0000000000..23b0f6819e --- /dev/null +++ b/fuzzers/binary_only/qemu_launcher/tests/qasan/qasan.c @@ -0,0 +1,85 @@ +// taken from +// https://github.com/AFLplusplus/AFLplusplus/blob/da2d4d8258d725f79c2daa22bf3b1a59c593e472/frida_mode/test/fasan/test.c + +#include +#include +#include +#include +#include +#include + +#define UNUSED_PARAMETER(x) (void)(x) + +#define LOG(x) \ + do { \ + char buf[] = x; \ + write(STDOUT_FILENO, buf, sizeof(buf)); \ + \ + } while (false); + +void LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + char *buf = malloc(10); + + if (buf == NULL) return; + + switch (*data) { + /* Underflow */ + case 'U': + LOG("Underflow\n"); + buf[-1] = '\0'; + free(buf); + break; + /* Overflow */ + case 'O': + LOG("Overflow\n"); + buf[10] = '\0'; + free(buf); + break; + /* Double free */ + case 'D': + LOG("Double free\n"); + free(buf); + free(buf); + break; + /* Use after free */ + case 'A': + LOG("Use after free\n"); + free(buf); + buf[0] = '\0'; + break; + /* Test Limits (OK) */ + case 'T': + LOG("Test-Limits - No Error\n"); + buf[0] = 'A'; + buf[9] = 'I'; + free(buf); + break; + case 'M': + LOG("Memset too many\n"); + memset(buf, '\0', 11); + free(buf); + break; + default: + LOG("Nop - No Error\n"); + break; + } +} + +int main(int argc, char **argv) { + UNUSED_PARAMETER(argc); + UNUSED_PARAMETER(argv); + + char input = '\0'; + + // if (read(STDIN_FILENO, &input, 1) < 0) { + + // LOG("Failed to read stdin\n"); + // return 1; + + // } + + LLVMFuzzerTestOneInput(&input, 1); + + LOG("DONE\n"); + return 0; +} diff --git a/fuzzers/binary_only/qemu_launcher/tests/qasan/test.sh b/fuzzers/binary_only/qemu_launcher/tests/qasan/test.sh new file mode 100755 index 0000000000..dbccd12685 --- /dev/null +++ b/fuzzers/binary_only/qemu_launcher/tests/qasan/test.sh @@ -0,0 +1,69 @@ +#!/bin/bash +set -e + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +if [[ ! -x "$QEMU_LAUNCHER" ]]; then + echo "env variable QEMU_LAUNCHER does not point to a valid executable" + echo "QEMU_LAUNCHER should point to qemu_launcher" + exit 1 +fi + +cd "$SCRIPT_DIR" +make + +tests=( + "overflow" + "underflow" + "double_free" + "memset" + "uaf" + "test_limits" +) + +tests_expected=( + "is 0 bytes to the right of the 10-byte chunk" + "is 1 bytes to the left of the 10-byte chunk" + "is 0 bytes inside the 10-byte chunk" + "is 0 bytes to the right of the 10-byte chunk" + "is 0 bytes inside the 10-byte chunk" + "Test-Limits - No Error" +) + +tests_not_expected=( + "dummy" + "dummy" + "dummy" + "dummy" + "dummy" + "Context:" +) + +for i in "${!tests[@]}" +do + test="${tests[i]}" + expected="${tests_expected[i]}" + not_expected="${tests_not_expected[i]}" + + echo "Running $test detection test..." + OUT=$("$QEMU_LAUNCHER" \ + -r "inputs/$test.txt" \ + --input dummy \ + --output out \ + --asan-cores 0 \ + -- qasan 2>&1 | tr -d '\0') + + if ! echo "$OUT" | grep -q "$expected"; then + echo "ERROR: Expected: $expected." + echo "Output is:" + echo "$OUT" + exit 1 + elif echo "$OUT" | grep -q "$not_expected"; then + echo "ERROR: Did not expect: $not_expected." + echo "Output is:" + echo "$OUT" + exit 1 + else + echo "OK." + fi +done diff --git a/libafl_qemu/src/modules/calls.rs b/libafl_qemu/src/modules/calls.rs index ab1814612c..98e2b85dd6 100644 --- a/libafl_qemu/src/modules/calls.rs +++ b/libafl_qemu/src/modules/calls.rs @@ -572,16 +572,17 @@ where static mut CALLSTACKS: Option>>> = None; #[derive(Debug)] -pub struct FullBacktraceCollector {} +pub struct FullBacktraceCollector; impl FullBacktraceCollector { /// # Safety + /// /// This accesses the global [`CALLSTACKS`] variable and may not be called concurrently. #[expect(rustdoc::private_intra_doc_links)] pub unsafe fn new() -> Self { let callstacks_ptr = &raw mut CALLSTACKS; unsafe { (*callstacks_ptr) = Some(ThreadLocal::new()) }; - Self {} + Self } pub fn reset(&mut self) { diff --git a/libafl_qemu/src/modules/usermode/asan.rs b/libafl_qemu/src/modules/usermode/asan.rs index 23b2e83adf..663625f379 100644 --- a/libafl_qemu/src/modules/usermode/asan.rs +++ b/libafl_qemu/src/modules/usermode/asan.rs @@ -1,22 +1,38 @@ #![allow(clippy::cast_possible_wrap)] #![allow(clippy::needless_pass_by_value)] // default compiler complains about Option<&mut T> otherwise, and this is used extensively. -use std::{borrow::Cow, env, fs, path::PathBuf, sync::Mutex}; + +use core::{fmt, slice}; +use std::{ + borrow::Cow, + env, + fmt::{Debug, Display}, + fs, + path::PathBuf, + pin::Pin, + process, + sync::Mutex, +}; use hashbrown::{HashMap, HashSet}; use libafl::{executors::ExitKind, observers::ObserversTuple}; +use libafl_qemu_sys::GuestAddr; use libc::{ c_void, MAP_ANON, MAP_FAILED, MAP_FIXED, MAP_NORESERVE, MAP_PRIVATE, PROT_READ, PROT_WRITE, }; use meminterval::{Interval, IntervalTree}; use num_enum::{IntoPrimitive, TryFromPrimitive}; +use object::{Object, ObjectSection}; use rangemap::RangeMap; use crate::{ + emu::EmulatorModules, modules::{ - calls::FullBacktraceCollector, snapshot::SnapshotModule, utils::filters::HasAddressFilter, - EmulatorModule, EmulatorModuleTuple, + calls::FullBacktraceCollector, + snapshot::SnapshotModule, + utils::filters::{HasAddressFilter, StdAddressFilter}, + AddressFilter, EmulatorModule, EmulatorModuleTuple, }, - qemu::MemAccessInfo, + qemu::{Hook, MemAccessInfo, QemuHooks, SyscallHookResult}, sys::TCGTemp, Qemu, QemuParams, Regs, }; @@ -40,6 +56,25 @@ pub const SHADOW_PAGE_MASK: GuestAddr = !(SHADOW_PAGE_SIZE as GuestAddr - 1); pub const DEFAULT_REDZONE_SIZE: usize = 128; +#[derive(Debug)] +pub struct AsanModule { + env: Vec<(String, String)>, + enabled: bool, + detect_leaks: bool, + empty: bool, + rt: Pin>, + filter: StdAddressFilter, +} + +pub struct AsanGiovese { + pub alloc_tree: Mutex>, + pub saved_tree: IntervalTree, + pub error_callback: Option, + pub dirty_shadow: Mutex>, + pub saved_shadow: HashMap>, + pub snapshot_shadow: bool, +} + #[derive(IntoPrimitive, TryFromPrimitive, Debug, Clone, Copy)] #[repr(u64)] pub enum QasanAction { @@ -56,14 +91,6 @@ pub enum QasanAction { SwapState, } -impl TryFrom for QasanAction { - type Error = num_enum::TryFromPrimitiveError; - - fn try_from(value: u32) -> Result { - QasanAction::try_from(u64::from(value)) - } -} - #[derive(IntoPrimitive, TryFromPrimitive, Debug, Clone, Copy, PartialEq)] #[repr(i8)] pub enum PoisonKind { @@ -105,8 +132,65 @@ pub enum AsanError { Signal(i32), } -impl core::fmt::Display for AsanError { - fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { +pub struct AsanModuleBuilder { + env: Option>, + detect_leaks: bool, + snapshot: bool, + filter: StdAddressFilter, + error_callback: Option, +} + +#[derive(Debug, Clone)] +pub struct AllocTreeItem { + backtrace: Vec, + free_backtrace: Vec, + allocated: bool, +} + +type AsanErrorFn = Box; + +pub struct AsanErrorCallback(AsanErrorFn); + +impl AsanErrorCallback { + /// Initialize a new [`AsanErrorCallback`] + #[must_use] + pub fn new(error_callback: AsanErrorFn) -> Self { + Self(error_callback) + } + + /// Special [`AsanErrorCallback`] providing a full report in case of QASAN trigger. + /// + /// # Safety + /// + /// The `ASan` error report accesses [`FullBacktraceCollector`] + #[must_use] + pub unsafe fn report() -> Self { + Self::new(Box::new(|rt, qemu, pc, err| { + asan_report(rt, qemu, pc, &err); + })) + } + + pub fn call( + &mut self, + asan_giovese: &AsanGiovese, + qemu: Qemu, + pc: GuestAddr, + error: AsanError, + ) { + self.0(asan_giovese, qemu, pc, error); + } +} + +impl TryFrom for QasanAction { + type Error = num_enum::TryFromPrimitiveError; + + fn try_from(value: u32) -> Result { + QasanAction::try_from(u64::from(value)) + } +} + +impl Display for AsanError { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { match self { AsanError::Read(addr, len) => write!(fmt, "Invalid {len} bytes read at {addr:#x}"), AsanError::Write(addr, len) => { @@ -122,15 +206,6 @@ impl core::fmt::Display for AsanError { } } -pub type AsanErrorCallback = Box; - -#[derive(Debug, Clone)] -pub struct AllocTreeItem { - backtrace: Vec, - free_backtrace: Vec, - allocated: bool, -} - impl AllocTreeItem { #[must_use] pub fn alloc(backtrace: Vec) -> Self { @@ -146,27 +221,8 @@ impl AllocTreeItem { self.allocated = false; } } -use std::pin::Pin; -use libafl_qemu_sys::GuestAddr; -use object::{Object, ObjectSection}; - -use crate::{ - emu::EmulatorModules, - modules::{utils::filters::StdAddressFilter, AddressFilter}, - qemu::{Hook, QemuHooks, SyscallHookResult}, -}; - -pub struct AsanGiovese { - pub alloc_tree: Mutex>, - pub saved_tree: IntervalTree, - pub error_callback: Option, - pub dirty_shadow: Mutex>, - pub saved_shadow: HashMap>, - pub snapshot_shadow: bool, -} - -impl core::fmt::Debug for AsanGiovese { +impl Debug for AsanGiovese { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("AsanGiovese") .field("alloc_tree", &self.alloc_tree) @@ -175,6 +231,211 @@ impl core::fmt::Debug for AsanGiovese { } } +impl AsanModuleBuilder { + #[must_use] + pub fn new( + env: Option>, + detect_leaks: bool, + snapshot: bool, + filter: StdAddressFilter, + error_callback: Option, + ) -> Self { + Self { + env, + detect_leaks, + snapshot, + filter, + error_callback, + } + } + + #[must_use] + pub fn env(self, env: &[(String, String)]) -> Self { + Self::new( + Some(env.to_vec()), + self.detect_leaks, + self.snapshot, + self.filter, + self.error_callback, + ) + } + + #[must_use] + pub fn detect_leaks(self, detect_leaks: bool) -> Self { + Self::new( + self.env, + detect_leaks, + self.snapshot, + self.filter, + self.error_callback, + ) + } + + #[must_use] + pub fn snapshot(self, snapshot: bool) -> Self { + Self::new( + self.env, + self.detect_leaks, + snapshot, + self.filter, + self.error_callback, + ) + } + + #[must_use] + pub fn filter(self, filter: StdAddressFilter) -> Self { + Self::new( + self.env, + self.detect_leaks, + self.snapshot, + filter, + self.error_callback, + ) + } + + #[must_use] + pub fn error_callback(self, callback: AsanErrorCallback) -> Self { + Self::new( + self.env, + self.detect_leaks, + self.snapshot, + self.filter, + Some(callback), + ) + } + + /// Get an ASAN report in case of problem. + /// + /// # Safety + /// + /// The `ASan` error report accesses [`FullBacktraceCollector`]. + /// Check its safety note for more details. + #[must_use] + pub unsafe fn asan_report(self) -> Self { + Self::new( + self.env, + self.detect_leaks, + self.snapshot, + self.filter, + Some(AsanErrorCallback::report()), + ) + } + + #[must_use] + pub fn build(self) -> AsanModule { + AsanModule::new( + self.env.unwrap().as_ref(), + self.detect_leaks, + self.snapshot, + self.filter, + self.error_callback, + ) + } +} + +impl Default for AsanModuleBuilder { + fn default() -> Self { + Self::new(None, false, true, StdAddressFilter::default(), None) + } +} + +impl AsanModule { + #[must_use] + pub fn builder() -> AsanModuleBuilder { + AsanModuleBuilder::default() + } + + #[must_use] + pub fn new( + env: &[(String, String)], + detect_leaks: bool, + snapshot: bool, + filter: StdAddressFilter, + error_callback: Option, + ) -> Self { + let mut rt = AsanGiovese::new(); + + rt.set_snapshot_shadow(snapshot); + if let Some(cb) = error_callback { + rt.set_error_callback(cb); + } + + Self { + env: env.to_vec(), + enabled: true, + detect_leaks, + empty: true, + rt, + filter, + } + } + + #[must_use] + pub fn must_instrument(&self, addr: GuestAddr) -> bool { + self.filter.allowed(&addr) + } + + #[must_use] + pub fn enabled(&self) -> bool { + self.enabled + } + + pub fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + } + + pub fn alloc(&mut self, pc: GuestAddr, start: GuestAddr, end: GuestAddr) { + self.rt.allocation(pc, start, end); + } + + pub fn dealloc(&mut self, qemu: Qemu, pc: GuestAddr, addr: GuestAddr) { + self.rt.deallocation(qemu, pc, addr); + } + + #[must_use] + pub fn is_poisoned(&self, qemu: Qemu, addr: GuestAddr, size: usize) -> bool { + AsanGiovese::is_invalid_access_n(qemu, addr, size) + } + + pub fn read(&mut self, qemu: Qemu, pc: GuestAddr, addr: GuestAddr) { + if self.enabled() && AsanGiovese::is_invalid_access::(qemu, addr) { + self.rt.report_or_crash(qemu, pc, AsanError::Read(addr, N)); + } + } + + pub fn read_n(&mut self, qemu: Qemu, pc: GuestAddr, addr: GuestAddr, size: usize) { + if self.enabled() && AsanGiovese::is_invalid_access_n(qemu, addr, size) { + self.rt + .report_or_crash(qemu, pc, AsanError::Read(addr, size)); + } + } + + pub fn write(&mut self, qemu: Qemu, pc: GuestAddr, addr: GuestAddr) { + if self.enabled() && AsanGiovese::is_invalid_access::(qemu, addr) { + self.rt.report_or_crash(qemu, pc, AsanError::Write(addr, N)); + } + } + + pub fn write_n(&mut self, qemu: Qemu, pc: GuestAddr, addr: GuestAddr, size: usize) { + if self.enabled() && AsanGiovese::is_invalid_access_n(qemu, addr, size) { + self.rt + .report_or_crash(qemu, pc, AsanError::Write(addr, size)); + } + } + + pub fn poison(&mut self, qemu: Qemu, addr: GuestAddr, size: usize, poison: PoisonKind) { + self.rt.poison(qemu, addr, size, poison.into()); + } + + pub fn unpoison(&mut self, qemu: Qemu, addr: GuestAddr, size: usize) { + AsanGiovese::unpoison(qemu, addr, size); + } + + pub fn reset(&mut self, qemu: Qemu) -> AsanRollback { + self.rt.rollback(qemu, self.detect_leaks) + } +} + impl AsanGiovese { unsafe fn init(self: &mut Pin>, qemu_hooks: QemuHooks) { assert_ne!( @@ -435,22 +696,22 @@ impl AsanGiovese { unsafe { let h = qemu.g2h::<*const c_void>(page) as isize; let shadow_addr = ((h >> 3) as *mut i8).offset(SHADOW_OFFSET); - std::slice::from_raw_parts_mut(shadow_addr, SHADOW_PAGE_SIZE) + slice::from_raw_parts_mut(shadow_addr, SHADOW_PAGE_SIZE) } } pub fn report_or_crash(&mut self, qemu: Qemu, pc: GuestAddr, error: AsanError) { if let Some(mut cb) = self.error_callback.take() { - cb(self, qemu, pc, error); + cb.call(self, qemu, pc, error); self.error_callback = Some(cb); } else { - std::process::abort(); + process::abort(); } } pub fn report(&mut self, qemu: Qemu, pc: GuestAddr, error: AsanError) { if let Some(mut cb) = self.error_callback.take() { - cb(self, qemu, pc, error); + cb.call(self, qemu, pc, error); self.error_callback = Some(cb); } } @@ -649,167 +910,6 @@ impl AsanGiovese { } } -pub enum QemuAsanOptions { - None, - Snapshot, - DetectLeaks, - SnapshotDetectLeaks, -} - -pub type AsanChildModule = AsanModule; - -#[derive(Debug)] -pub struct AsanModule { - env: Vec<(String, String)>, - enabled: bool, - detect_leaks: bool, - empty: bool, - rt: Pin>, - filter: StdAddressFilter, -} - -impl AsanModule { - #[must_use] - pub fn default(env: &[(String, String)]) -> Self { - Self::new(StdAddressFilter::default(), &QemuAsanOptions::Snapshot, env) - } - - #[must_use] - pub fn new( - filter: StdAddressFilter, - options: &QemuAsanOptions, - env: &[(String, String)], - ) -> Self { - let (snapshot, detect_leaks) = match options { - QemuAsanOptions::None => (false, false), - QemuAsanOptions::Snapshot => (true, false), - QemuAsanOptions::DetectLeaks => (false, true), - QemuAsanOptions::SnapshotDetectLeaks => (true, true), - }; - - let mut rt = AsanGiovese::new(); - rt.set_snapshot_shadow(snapshot); - - Self { - env: env.to_vec(), - enabled: true, - detect_leaks, - empty: true, - rt, - filter, - } - } - - #[must_use] - pub fn with_error_callback( - filter: StdAddressFilter, - error_callback: AsanErrorCallback, - options: &QemuAsanOptions, - env: &[(String, String)], - ) -> Self { - let (snapshot, detect_leaks) = match options { - QemuAsanOptions::None => (false, false), - QemuAsanOptions::Snapshot => (true, false), - QemuAsanOptions::DetectLeaks => (false, true), - QemuAsanOptions::SnapshotDetectLeaks => (true, true), - }; - - let mut rt = AsanGiovese::new(); - rt.set_snapshot_shadow(snapshot); - rt.set_error_callback(error_callback); - - Self { - env: env.to_vec(), - enabled: true, - detect_leaks, - empty: true, - rt, - filter, - } - } - - /// # Safety - /// The `ASan` error report accesses [`FullBacktraceCollector`] - #[must_use] - pub unsafe fn with_asan_report( - filter: StdAddressFilter, - options: &QemuAsanOptions, - env: &[(String, String)], - ) -> Self { - Self::with_error_callback( - filter, - Box::new(|rt, qemu, pc, err| unsafe { asan_report(rt, qemu, pc, &err) }), - options, - env, - ) - } - - #[must_use] - pub fn must_instrument(&self, addr: GuestAddr) -> bool { - self.filter.allowed(&addr) - } - - #[must_use] - pub fn enabled(&self) -> bool { - self.enabled - } - - pub fn set_enabled(&mut self, enabled: bool) { - self.enabled = enabled; - } - - pub fn alloc(&mut self, pc: GuestAddr, start: GuestAddr, end: GuestAddr) { - self.rt.allocation(pc, start, end); - } - - pub fn dealloc(&mut self, qemu: Qemu, pc: GuestAddr, addr: GuestAddr) { - self.rt.deallocation(qemu, pc, addr); - } - - #[must_use] - pub fn is_poisoned(&self, qemu: Qemu, addr: GuestAddr, size: usize) -> bool { - AsanGiovese::is_invalid_access_n(qemu, addr, size) - } - - pub fn read(&mut self, qemu: Qemu, pc: GuestAddr, addr: GuestAddr) { - if self.enabled() && AsanGiovese::is_invalid_access::(qemu, addr) { - self.rt.report_or_crash(qemu, pc, AsanError::Read(addr, N)); - } - } - - pub fn read_n(&mut self, qemu: Qemu, pc: GuestAddr, addr: GuestAddr, size: usize) { - if self.enabled() && AsanGiovese::is_invalid_access_n(qemu, addr, size) { - self.rt - .report_or_crash(qemu, pc, AsanError::Read(addr, size)); - } - } - - pub fn write(&mut self, qemu: Qemu, pc: GuestAddr, addr: GuestAddr) { - if self.enabled() && AsanGiovese::is_invalid_access::(qemu, addr) { - self.rt.report_or_crash(qemu, pc, AsanError::Write(addr, N)); - } - } - - pub fn write_n(&mut self, qemu: Qemu, pc: GuestAddr, addr: GuestAddr, size: usize) { - if self.enabled() && AsanGiovese::is_invalid_access_n(qemu, addr, size) { - self.rt - .report_or_crash(qemu, pc, AsanError::Write(addr, size)); - } - } - - pub fn poison(&mut self, qemu: Qemu, addr: GuestAddr, size: usize, poison: PoisonKind) { - self.rt.poison(qemu, addr, size, poison.into()); - } - - pub fn unpoison(&mut self, qemu: Qemu, addr: GuestAddr, size: usize) { - AsanGiovese::unpoison(qemu, addr, size); - } - - pub fn reset(&mut self, qemu: Qemu) -> AsanRollback { - self.rt.rollback(qemu, self.detect_leaks) - } -} - impl EmulatorModule for AsanModule where I: Unpin, @@ -1203,7 +1303,7 @@ fn load_file_section<'input, 'arena, Endian: addr2line::gimli::Endianity>( /// has been removed in version v0.23 for some reason. /// TODO: find another cleaner solution. mod addr2line_legacy { - use std::{borrow::Cow, ffi::OsString, fs::File, path::PathBuf, sync::Arc}; + use std::{borrow::Cow, env, ffi::OsString, fs::File, path::PathBuf, sync::Arc}; use addr2line::{gimli, LookupContinuation, LookupResult}; use object::Object; @@ -1223,7 +1323,7 @@ mod addr2line_legacy { r: &R, ) -> Result { let bytes = r.to_slice()?; - let s = std::str::from_utf8(&bytes).map_err(|_| gimli::Error::BadUtf8)?; + let s = str::from_utf8(&bytes).map_err(|_| gimli::Error::BadUtf8)?; Ok(PathBuf::from(s)) } @@ -1269,7 +1369,7 @@ mod addr2line_legacy { loader: &mut F, path: Option, ) -> Option> { - let mut path = path.map_or_else(std::env::current_exe, Ok).ok()?; + let mut path = path.map_or_else(env::current_exe, Ok).ok()?; let dwp_extension = path.extension().map_or_else( || OsString::from("dwp"), |previous_extension| {