Skip to content

Commit

Permalink
Add builder and tests for QASAN (#2898)
Browse files Browse the repository at this point in the history
* Add tests for QASAN from aflplusplus

* refactor asan module to use the builder pattern

* move injection tests to the new tests directory
  • Loading branch information
rmalmain authored Jan 31, 2025
1 parent 37fc43f commit 75feedd
Show file tree
Hide file tree
Showing 19 changed files with 574 additions and 245 deletions.
31 changes: 16 additions & 15 deletions fuzzers/binary_only/qemu_launcher/Makefile.toml
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down Expand Up @@ -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"]

Expand Down
64 changes: 47 additions & 17 deletions fuzzers/binary_only/qemu_launcher/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -109,32 +111,58 @@ 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,
)
} else {
instance_builder.build().run(
args,
tuple_list!(CmpLogModule::default(), AsanModule::default(&env),),
tuple_list!(
CmpLogModule::default(),
AsanModule::builder().env(&env).build()
),
state,
)
}
Expand All @@ -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
Expand Down
30 changes: 30 additions & 0 deletions fuzzers/binary_only/qemu_launcher/tests/injection/test.sh
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions fuzzers/binary_only/qemu_launcher/tests/qasan/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
all: qasan

qasan: qasan.c
gcc qasan.c -o qasan

clean:
rm -rf qasan out stats.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
D
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
M
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
O
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
T
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
U
85 changes: 85 additions & 0 deletions fuzzers/binary_only/qemu_launcher/tests/qasan/qasan.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// taken from
// https://github.com/AFLplusplus/AFLplusplus/blob/da2d4d8258d725f79c2daa22bf3b1a59c593e472/frida_mode/test/fasan/test.c

#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>

#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;
}
69 changes: 69 additions & 0 deletions fuzzers/binary_only/qemu_launcher/tests/qasan/test.sh
Original file line number Diff line number Diff line change
@@ -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
5 changes: 3 additions & 2 deletions libafl_qemu/src/modules/calls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -572,16 +572,17 @@ where
static mut CALLSTACKS: Option<ThreadLocal<UnsafeCell<Vec<GuestAddr>>>> = 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) {
Expand Down
Loading

0 comments on commit 75feedd

Please sign in to comment.