diff --git a/README.md b/README.md index af618b3c8..5eb55728f 100644 --- a/README.md +++ b/README.md @@ -51,3 +51,17 @@ environment variable `AFL_NO_CFG_FUZZING` to `1` when building. [american-fuzzy-lop]: http://lcamtuf.coredump.cx/afl/ [AFLplusplus]: https://aflplus.plus/ [rust]: https://www.rust-lang.org + +## IJON + +If you want to use [IJON](https://github.com/AFLplusplus/AFLplusplus/blob/stable/docs/IJON.md) - helping fuzzer coverage through code annotation - then +have a look at the [maze example](afl/examples/maze.rs). + +Note that the IJON macros have been rustyfied to lowercase - hence `IJON_MAX(x)` is `ijon_max(x)` in Rust. + +You will need to the following imports from `afl`, in addition to any macros that you use (e.g., `afl::ijon_max`): + +``` +use afl::ijon_hashint; +use afl::ijon_hashstr; +``` diff --git a/afl/examples/maze.rs b/afl/examples/maze.rs new file mode 100644 index 000000000..363211316 --- /dev/null +++ b/afl/examples/maze.rs @@ -0,0 +1,74 @@ +/* +This example tests the AFL++ IJON feature. + +Solution to the maze: +cddbddcccacaaaccaaaaaabbbbbbbbbbaaccccccccccccaaccaabbbbbbbbbbbbbbbbbbbbbbbbbaacccccccccccccccccccccccccccccdddddddddddddddddddddddddddddbbbbbbbbbbbbbbbd +*/ + +#![allow(clippy::manual_assert, clippy::cast_possible_truncation)] + +#[cfg(fuzzing)] +use afl::{ijon_hashint, ijon_hashstr, ijon_set}; + +fn main() { + afl::fuzz!(|data: &[u8]| { + // 31x31 maze, 0 = open, 1 = wall, 2 = start, 3 = exit + + #[rustfmt::skip] + let maze = [ + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], + [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], + [1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1], + [1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1], + [1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,1], + [1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,1], + [1,0,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,1,0,1], + [1,0,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,1,0,1], + [1,0,1,0,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,1,0,1,0,1], + [1,0,1,0,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,1,0,1,0,1], + [1,0,1,0,1,0,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,0,1,0,1,0,1,0,1,0,1], + [1,0,1,0,1,0,1,0,1,0,1,0,0,0,0,0,0,0,0,1,1,0,1,0,1,0,1,0,1,0,1], + [1,0,1,0,1,0,1,0,1,0,1,0,1,1,1,0,1,1,0,0,1,0,1,0,1,0,1,0,1,0,1], + [1,0,1,0,1,0,1,0,1,0,1,0,1,0,0,0,0,1,1,0,1,0,1,0,1,0,1,0,1,0,1], + [1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,1,0,0,1,0,1,0,1,0,1,0,1,0,1,0,1], + [1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,2,0,0,1,0,0,0,1,0,1,0,1,0,1,0,1], + [1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,1,0,0,1,0,1,0,1,0,1,0,1,0,1,0,1], + [1,0,1,0,1,0,1,0,1,0,1,0,1,0,0,0,0,1,1,0,1,0,1,0,1,0,1,0,1,0,1], + [1,0,1,0,1,0,1,0,1,0,1,0,1,1,1,1,1,1,0,0,1,0,1,0,1,0,1,0,1,0,1], + [1,0,1,0,1,0,1,0,1,0,1,0,0,0,0,0,0,0,0,1,1,0,1,0,1,0,1,0,1,0,1], + [1,0,1,0,1,0,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,0,1,0,1,0,1,0,1,0,1], + [1,0,1,0,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,1,0,1,0,1], + [1,0,1,0,1,0,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,0,1,0,1,0,1,0,1], + [1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,1,0,1], + [1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,1,0,1,0,1], + [1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,1], + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,1,0,1], + [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,1], + [1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,1], + [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], + ]; + + let mut pos: (usize, usize) = (15, 15); // start position + + for &b in data { + let next = match b % 4 { + 0 => (pos.0.wrapping_sub(1), pos.1), // up + 1 => (pos.0 + 1, pos.1), // down + 2 => (pos.0, pos.1.wrapping_sub(1)), // left + _ => (pos.0, pos.1 + 1), // right + }; + + if next.0 < 31 && next.1 < 31 && maze[next.0][next.1] != 1 { + pos = next; + } + + if maze[pos.0][pos.1] == 3 { + panic!("Exited the maze!"); + } + + #[cfg(fuzzing)] + ijon_set!(ijon_hashint(pos.0 as u32, pos.1 as u32)); + } + }); +} diff --git a/afl/src/lib.rs b/afl/src/lib.rs index e9b3d58d0..804a1b3ff 100644 --- a/afl/src/lib.rs +++ b/afl/src/lib.rs @@ -7,9 +7,10 @@ use std::env; use std::io::{self, Read}; +use std::os::raw::c_char; use std::panic; -// those functions are provided by the afl-llvm-rt static library +// those functions are provided by the afl-compiler-rt static library unsafe extern "C" { fn __afl_persistent_loop(counter: usize) -> isize; fn __afl_manual_init(); @@ -18,6 +19,204 @@ unsafe extern "C" { static __afl_fuzz_ptr: *const u8; } +// AFL++ IJON functions in afl-compiler-rt +unsafe extern "C" { + pub fn ijon_max(addr: u32, val: u64); + pub fn ijon_min(addr: u32, val: u64); + pub fn ijon_set(addr: u32, val: u32); + pub fn ijon_inc(addr: u32, val: u32); + pub fn ijon_xor_state(val: u32); + pub fn ijon_reset_state(); + pub fn ijon_simple_hash(x: u64) -> u64; + pub fn ijon_hashint(old: u32, val: u32) -> u32; + pub fn ijon_hashstr(old: u32, val: *const c_char) -> u32; + pub fn ijon_hashmen(old: u32, val: *const u8, len: usize) -> u32; + pub fn ijon_hashstack_backtrace() -> u32; + pub fn ijon_hashstack() -> u32; + pub fn ijon_strdist(a: *const u8, b: *const u8) -> u32; + pub fn ijon_memdist(a: *const u8, b: *const u8, len: usize) -> u32; + pub fn ijon_max_variadic(addr: u32, ...); + pub fn ijon_min_variadic(addr: u32, ...); +} + +#[macro_export] +macro_rules! ijon_inc { + ($x:expr) => {{ + unsafe { + static mut loc: u32 = 0; + if loc == 0 { + let cfile = std::ffi::CString::new(file!()).unwrap(); + loc = afl::ijon_hashstr(line!(), cfile.as_ptr()); + } + afl::ijon_inc(loc, $x) + }; + }}; +} + +#[macro_export] +macro_rules! ijon_max { + ($($x:expr),+ $(,)?) => {{ + unsafe { + static mut loc: u32 = 0; + if loc == 0 { + let cfile = std::ffi::CString::new(file!()).unwrap(); + loc = afl::ijon_hashstr(line!(), cfile.as_ptr()); + } + afl::ijon_max_variadic(_IJON_LOC_CACHE, $($x),+, 0u64) + }; + }}; +} + +#[macro_export] +macro_rules! ijon_min { + ($($x:expr),+ $(,)?) => {{ + unsafe { + static mut loc: u32 = 0; + if loc == 0 { + let cfile = std::ffi::CString::new(file!()).unwrap(); + loc = afl::ijon_hashstr(line!(), cfile.as_ptr()); + } + afl::ijon_min_variadic(loc, $($x),+, 0u64) + }; + }}; +} + +#[macro_export] +macro_rules! ijon_set { + ($x:expr) => {{ + unsafe { + static mut loc: u32 = 0; + if loc == 0 { + let cfile = std::ffi::CString::new(file!()).unwrap(); + loc = afl::ijon_hashstr(line!(), cfile.as_ptr()); + } + afl::ijon_set(loc, $x) + }; + }}; +} + +#[macro_export] +macro_rules! ijon_state { + ($n:expr) => { + unsafe { afl::ijon_xor_state($n) } + }; +} + +#[macro_export] +macro_rules! ijon_ctx { + ($x:expr) => {{ + let cfile = std::ffi::CString::new(file!()).unwrap(); + let hash = unsafe { afl::ijon_hashstr(line!(), cfile.as_ptr()) }; + unsafe { afl::ijon_xor_state(hash) }; + let temp = $x; + unsafe { afl::ijon_xor_state(hash) }; + temp + }}; +} + +#[macro_export] +macro_rules! ijon_max_at { + ($addr:expr, $x:expr) => { + unsafe { afl::ijon_max($addr, $x) } + }; +} + +#[macro_export] +macro_rules! ijon_min_at { + ($addr:expr, $x:expr) => { + unsafe { afl::ijon_min($addr, $x) } + }; +} + +#[macro_export] +macro_rules! _ijon_abs_dist { + ($x:expr, $y:expr) => { + if $x < $y { $y - $x } else { $x - $y } + }; +} + +#[macro_export] +macro_rules! ijon_bits { + ($x:expr) => { + unsafe { + afl::ijon_set(afl::ijon_hashint( + afl::ijon_hashstack(), + if $x == 0 { + 0 + } else { + $x.leading_zeros() as u32 + }, + )) + } + }; +} + +#[macro_export] +macro_rules! ijon_strdist { + ($x:expr, $y:expr) => { + unsafe { + afl::ijon_set(afl::ijon_hashint( + afl::ijon_hashstack(), + afl::ijon_strdist($x, $y), + )) + } + }; +} + +#[macro_export] +macro_rules! ijon_dist { + ($x:expr, $y:expr) => { + unsafe { + afl::ijon_set(afl::ijon_hashint( + afl::ijon_hashstack(), + $crate::_ijon_abs_dist!($x, $y), + )) + } + }; +} + +#[macro_export] +macro_rules! ijon_cmp { + ($x:expr, $y:expr) => { + unsafe { + afl::ijon_inc(afl::ijon_hashint( + afl::ijon_hashstack(), + ($x ^ $y).count_ones(), + )) + } + }; +} + +#[macro_export] +macro_rules! ijon_stack_max { + ($x:expr) => {{ + unsafe { + static mut loc: u32 = 0; + if loc == 0 { + let cfile = std::ffi::CString::new(file!()).unwrap(); + loc = afl::ijon_hashstr(line!(), cfile.as_ptr()); + } + afl::ijon_max(afl::ijon_hashint(loc, afl::ijon_hashstack()), $x) + }; + }}; +} + +#[macro_export] +macro_rules! ijon_stack_min { + ($x:expr) => {{ + unsafe { + static mut loc: u32 = 0; + if loc == 0 { + let cfile = std::ffi::CString::new(file!()).unwrap(); + loc = afl::ijon_hashstr(line!(), cfile.as_ptr()); + } + afl::ijon_min(afl::ijon_hashint(loc, afl::ijon_hashstack()), $x) + }; + }}; +} + +// end if AFL++ IJON functions + #[allow(non_upper_case_globals)] #[doc(hidden)] #[unsafe(no_mangle)] diff --git a/cargo-afl/src/main.rs b/cargo-afl/src/main.rs index ab9a04e15..3ddf36c6f 100644 --- a/cargo-afl/src/main.rs +++ b/cargo-afl/src/main.rs @@ -314,6 +314,7 @@ where -Z llvm-plugins={p}/SanitizerCoveragePCGUARD.so \ -Z llvm-plugins={p}/cmplog-instructions-pass.so \ -Z llvm-plugins={p}/cmplog-routines-pass.so \ + -Z llvm-plugins={p}/afl-llvm-ijon-pass.so " )); diff --git a/cargo-afl/tests/integration.rs b/cargo-afl/tests/integration.rs index 28b09dce1..e606ea9e1 100644 --- a/cargo-afl/tests/integration.rs +++ b/cargo-afl/tests/integration.rs @@ -1,8 +1,13 @@ use std::{ + io::Write, path, process::{self, ExitStatus}, }; +#[allow(dead_code)] +#[path = "../src/common.rs"] +mod common; + fn target_dir_path() -> &'static path::Path { if path::Path::new("../target/debug/cargo-afl").exists() { path::Path::new("../target/debug/") @@ -54,6 +59,50 @@ fn integration_cfg() { } } +#[test] +fn integration_maze() { + if !common::plugins_available().unwrap_or_default() { + #[allow(clippy::explicit_write)] + writeln!( + std::io::stderr(), + "Skipping `integration_maze` test as plugins are unavailable" + ) + .unwrap(); + return; + } + + let temp_dir = tempfile::TempDir::new().expect("Could not create temporary directory"); + let temp_dir_path = temp_dir.path(); + + for _i in 0..3 { + let _: ExitStatus = process::Command::new(cargo_afl_path()) + .arg("afl") + .arg("fuzz") + .arg("-i") + .arg(input_path()) + .arg("-o") + .arg(temp_dir_path) + .args(["-V", "15"]) // 15 seconds + .arg(examples_path("maze")) + .env("AFL_BENCH_UNTIL_CRASH", "1") + .env("AFL_NO_CRASH_README", "1") + .env("AFL_NO_UI", "1") + .stdout(process::Stdio::inherit()) + .stderr(process::Stdio::inherit()) + .status() + .expect("Could not run cargo afl fuzz"); + assert!(temp_dir_path.join("default").join("fuzzer_stats").is_file()); + let crashes = std::fs::read_dir(temp_dir_path.join("default").join("crashes")) + .unwrap() + .count(); + if crashes >= 1 { + return; + } + } + + unreachable!(); +} + fn fuzz_example(name: &str, should_crash: bool) { let temp_dir = tempfile::TempDir::new().expect("Could not create temporary directory"); let temp_dir_path = temp_dir.path();