Refactor to new forkserver (#3183)

* Refactor to new forkserver

* Fix fuzzer examples and delete forkserver.c

* Fix clippy and doc warnings

* Fix symbol error

* Format Cargo.toml; Fix wrong doc link

* Fix silly typo.

* Rename ForkServer to Forkserver to make it more consistent

* Fix build.rs

* Merge StdForkserverParent and PersistentForkserverParent since the forkserver parent has not idea of whether it is persistent and the persistent version can handle the non-persistent version

* Fix clippy

* Do not take ownership for last_child_pid since it may be in persistent mode
This commit is contained in:
EvianZhang 2025-05-05 16:45:12 +08:00 committed by GitHub
parent 4ae6f34ab4
commit c0e32cdbba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 535 additions and 457 deletions

View File

@ -1,9 +1,19 @@
use libafl_targets::{map_shared_memory, start_forkserver};
use libafl_targets::{
map_input_shared_memory, map_shared_memory, start_forkserver, MaybePersistentForkserverParent,
};
#[no_mangle]
pub extern "C" fn libafl_start_forkserver() {
// Map shared memory region for the edge coverage map
map_shared_memory();
if map_shared_memory().is_err() {
std::process::exit(1);
}
// Map shared memory region for input and its len
if map_input_shared_memory().is_err() {
std::process::exit(1);
};
// Start the forkserver
start_forkserver();
if start_forkserver(&mut MaybePersistentForkserverParent::new()).is_err() {
std::process::exit(1);
};
}

View File

@ -1,9 +1,19 @@
use libafl_targets::{map_shared_memory, start_forkserver};
use libafl_targets::{
map_input_shared_memory, map_shared_memory, start_forkserver, MaybePersistentForkserverParent,
};
#[no_mangle]
pub extern "C" fn libafl_start_forkserver() {
// Map shared memory region for the edge coverage map
map_shared_memory();
if map_shared_memory().is_err() {
std::process::exit(1);
}
// Map shared memory region for input and its len
if map_input_shared_memory().is_err() {
std::process::exit(1);
};
// Start the forkserver
start_forkserver();
if start_forkserver(&mut MaybePersistentForkserverParent::new()).is_err() {
std::process::exit(1);
};
}

View File

@ -17,12 +17,14 @@ use std::{
process::{Child, Command, Stdio},
};
#[cfg(feature = "regex")]
use libafl_bolts::tuples::{Handle, Handled, MatchNameRef};
use libafl_bolts::{
AsSlice, AsSliceMut, InputLocation, TargetArgs, Truncate,
fs::{InputFile, get_unique_std_input_file},
os::{dup2, pipes::Pipe},
shmem::{ShMem, ShMemProvider, UnixShMem, UnixShMemProvider},
tuples::{Handle, Handled, MatchNameRef, Prepend, RefIndexable},
tuples::{Prepend, RefIndexable},
};
use libc::RLIM_INFINITY;
use nix::{
@ -49,45 +51,65 @@ use crate::{
state::HasExecutions,
};
const FORKSRV_FD: i32 = 198;
/// Pinned fd number for forkserver communication
pub const FORKSRV_FD: i32 = 198;
#[expect(clippy::cast_possible_wrap)]
const FS_NEW_ERROR: i32 = 0xeffe0000_u32 as i32;
const FS_NEW_VERSION_MIN: u32 = 1;
const FS_NEW_VERSION_MAX: u32 = 1;
/// Minimum number for new version
pub const FS_NEW_VERSION_MIN: u32 = 1;
/// Maximum number for new version
pub const FS_NEW_VERSION_MAX: u32 = 1;
/// Whether forkserver option customization for old forkserver is enabled
#[expect(clippy::cast_possible_wrap)]
const FS_OPT_ENABLED: i32 = 0x80000001_u32 as i32;
pub const FS_OPT_ENABLED: i32 = 0x80000001_u32 as i32;
/// Set map size option for new forkserver
#[expect(clippy::cast_possible_wrap)]
const FS_NEW_OPT_MAPSIZE: i32 = 1_u32 as i32;
pub const FS_NEW_OPT_MAPSIZE: i32 = 1_u32 as i32;
/// Set map size option for old forkserver
#[expect(clippy::cast_possible_wrap)]
const FS_OPT_MAPSIZE: i32 = 0x40000000_u32 as i32;
pub const FS_OPT_MAPSIZE: i32 = 0x40000000_u32 as i32;
/// Enable shared memory fuzzing option for old forkserver
#[expect(clippy::cast_possible_wrap)]
const FS_OPT_SHDMEM_FUZZ: i32 = 0x01000000_u32 as i32;
pub const FS_OPT_SHDMEM_FUZZ: i32 = 0x01000000_u32 as i32;
/// Enable shared memory fuzzing option for new forkserver
#[expect(clippy::cast_possible_wrap)]
const FS_NEW_OPT_SHDMEM_FUZZ: i32 = 2_u32 as i32;
pub const FS_NEW_OPT_SHDMEM_FUZZ: i32 = 2_u32 as i32;
/// Enable autodict option for new forkserver
#[expect(clippy::cast_possible_wrap)]
const FS_NEW_OPT_AUTODTCT: i32 = 0x00000800_u32 as i32;
pub const FS_NEW_OPT_AUTODTCT: i32 = 0x00000800_u32 as i32;
/// Enable autodict option for old forkserver
#[expect(clippy::cast_possible_wrap)]
const FS_OPT_AUTODTCT: i32 = 0x10000000_u32 as i32;
pub const FS_OPT_AUTODTCT: i32 = 0x10000000_u32 as i32;
/// Failed to set map size
#[expect(clippy::cast_possible_wrap)]
const FS_ERROR_MAP_SIZE: i32 = 1_u32 as i32;
pub const FS_ERROR_MAP_SIZE: i32 = 1_u32 as i32;
/// Failed to map address
#[expect(clippy::cast_possible_wrap)]
const FS_ERROR_MAP_ADDR: i32 = 2_u32 as i32;
pub const FS_ERROR_MAP_ADDR: i32 = 2_u32 as i32;
/// Failed to open shared memory
#[expect(clippy::cast_possible_wrap)]
const FS_ERROR_SHM_OPEN: i32 = 4_u32 as i32;
pub const FS_ERROR_SHM_OPEN: i32 = 4_u32 as i32;
/// Failed to do `shmat`
#[expect(clippy::cast_possible_wrap)]
const FS_ERROR_SHMAT: i32 = 8_u32 as i32;
pub const FS_ERROR_SHMAT: i32 = 8_u32 as i32;
/// Failed to do `mmap`
#[expect(clippy::cast_possible_wrap)]
const FS_ERROR_MMAP: i32 = 16_u32 as i32;
pub const FS_ERROR_MMAP: i32 = 16_u32 as i32;
/// Old cmplog error
#[expect(clippy::cast_possible_wrap)]
const FS_ERROR_OLD_CMPLOG: i32 = 32_u32 as i32;
pub const FS_ERROR_OLD_CMPLOG: i32 = 32_u32 as i32;
/// Old QEMU cmplog error
#[expect(clippy::cast_possible_wrap)]
const FS_ERROR_OLD_CMPLOG_QEMU: i32 = 64_u32 as i32;
pub const FS_ERROR_OLD_CMPLOG_QEMU: i32 = 64_u32 as i32;
/// Flag indicating this is an error
#[expect(clippy::cast_possible_wrap)]
pub const FS_OPT_ERROR: i32 = 0xf800008f_u32 as i32;
/// Forkserver message. We'll reuse it in a testcase.
const FAILED_TO_START_FORKSERVER_MSG: &str = "Failed to start forkserver";
@ -121,6 +143,10 @@ fn report_error_and_exit(status: i32) -> Result<(), Error> {
const SHMEM_FUZZ_HDR_SIZE: usize = 4;
const MAX_INPUT_SIZE_DEFAULT: usize = 1024 * 1024;
const MIN_INPUT_SIZE_DEFAULT: usize = 1;
/// Environment variable key for shared memory id for input and its len
pub const SHM_FUZZ_ENV_VAR: &str = "__AFL_SHM_FUZZ_ID";
/// Environment variable key for shared memory id for edge map
pub const SHM_ENV_VAR: &str = "__AFL_SHM_ID";
/// The default signal to use to kill child processes
const KILL_SIGNAL_DEFAULT: Signal = Signal::SIGTERM;
@ -341,7 +367,7 @@ impl Forkserver {
log::warn!("AFL_MAP_SIZE not set. If it is unset, the forkserver may fail to start up");
}
if env::var("__AFL_SHM_ID").is_err() {
if env::var(SHM_ENV_VAR).is_err() {
return Err(Error::unknown("__AFL_SHM_ID not set. It is necessary to set this env, otherwise the forkserver cannot communicate with the fuzzer".to_string()));
}
@ -391,6 +417,8 @@ impl Forkserver {
};
command.env("ASAN_OPTIONS", asan_options);
}
#[cfg(not(feature = "regex"))]
let _ = dump_asan_logs;
let fsrv_handle = match command
.env("LD_BIND_NOW", "1")
@ -980,7 +1008,7 @@ where
// # Safety
// This is likely single threade here, we're likely fine if it's not.
unsafe {
shmem.write_to_env("__AFL_SHM_FUZZ_ID")?;
shmem.write_to_env(SHM_FUZZ_ENV_VAR)?;
}
let size_in_bytes = (self.max_input_size + SHMEM_FUZZ_HDR_SIZE).to_ne_bytes();

View File

@ -58,7 +58,12 @@ common = [
] # Compile common C code defining sanitizer options and cross-platform intrinsics
coverage = ["common"] # Compile C code definining coverage maps
cmplog = ["common"] # Compile C code defining cmp log maps
forkserver = ["common"] # Compile C code for forkserver support
forkserver = [
"common",
"nix",
"libafl/std",
"libafl/fork",
] # Compile C code for forkserver support
windows_asan = ["common"] # Compile C code for ASAN on Windows
whole_archive = [] # use +whole-archive to ensure the presence of weak symbols
cmplog_extended_instrumentation = [
@ -74,6 +79,7 @@ rustversion = "1.0.17"
libafl = { workspace = true, features = [], default-features = false }
libafl_bolts = { workspace = true, features = [] }
libc = { workspace = true }
nix = { workspace = true, optional = true }
hashbrown = { workspace = true, default-features = true }
once_cell = "1.19.0"
log = { workspace = true }

View File

@ -241,43 +241,25 @@ fn main() {
}
}
#[cfg(any(feature = "forkserver", feature = "windows_asan"))]
let target_family = std::env::var("CARGO_CFG_TARGET_FAMILY").unwrap();
#[cfg(feature = "forkserver")]
#[cfg(feature = "windows_asan")]
{
if target_family == "unix" {
println!("cargo:rerun-if-changed=src/forkserver.c");
let target_family = std::env::var("CARGO_CFG_TARGET_FAMILY").unwrap();
if target_family == "windows" {
println!("cargo:rerun-if-changed=src/windows_asan.c");
let mut forkserver = cc::Build::new();
let mut windows_asan = cc::Build::new();
#[cfg(feature = "whole_archive")]
{
forkserver.link_lib_modifier("+whole-archive");
windows_asan.link_lib_modifier("+whole-archive");
}
forkserver
.file(src_dir.join("forkserver.c"))
.compile("forkserver");
windows_asan
.file(src_dir.join("windows_asan.c"))
.compile("windows_asan");
}
}
#[cfg(feature = "windows_asan")]
if target_family == "windows" {
println!("cargo:rerun-if-changed=src/windows_asan.c");
let mut windows_asan = cc::Build::new();
#[cfg(feature = "whole_archive")]
{
windows_asan.link_lib_modifier("+whole-archive");
}
windows_asan
.file(src_dir.join("windows_asan.c"))
.compile("windows_asan");
}
// NOTE: Sanitizer interfaces doesn't require common
#[cfg(feature = "sanitizer_interfaces")]
if env::var("CARGO_CFG_TARGET_POINTER_WIDTH").unwrap() == "64" {

View File

@ -33,6 +33,9 @@ pub use __afl_acc_memop_ptr_local as ACCOUNTING_MEMOP_MAP;
pub static mut MAX_EDGES_FOUND: usize = 0;
unsafe extern "C" {
/// The sharedmemort fuzzing flag
pub static mut __afl_sharedmem_fuzzing: core::ffi::c_uint;
/// The area pointer points to the edges map.
pub static mut __afl_area_ptr: *mut u8;
@ -49,6 +52,9 @@ unsafe extern "C" {
}
pub use __afl_acc_memop_ptr as ACCOUNTING_MEMOP_MAP_PTR;
pub use __afl_area_ptr as EDGES_MAP_PTR;
pub use __afl_fuzz_len as INPUT_LENGTH_PTR;
pub use __afl_fuzz_ptr as INPUT_PTR;
pub use __afl_sharedmem_fuzzing as SHM_FUZZING;
/// Return Tokens from the compile-time token section
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
@ -71,6 +77,16 @@ pub fn autotokens() -> Result<Tokens, Error> {
#[allow(non_upper_case_globals)] // expect breaks here for some reason
#[unsafe(no_mangle)]
pub static mut __afl_map_size: usize = EDGES_MAP_DEFAULT_SIZE;
/// The pointer points to the AFL++ inputs
#[allow(non_upper_case_globals)] // expect breaks here for some reason
#[unsafe(no_mangle)]
pub static mut __afl_fuzz_ptr: *mut u8 = core::ptr::null_mut();
#[allow(non_upper_case_globals)] // expect breaks here for some reason
static mut __afl_fuzz_len_local: u32 = 0;
/// The pointer points to the length of AFL++ inputs
#[allow(non_upper_case_globals)] // expect breaks here for some reason
#[unsafe(no_mangle)]
pub static mut __afl_fuzz_len: *mut u32 = &raw mut __afl_fuzz_len_local;
#[cfg(any(
feature = "sancov_pcguard_edges",

View File

@ -1,381 +0,0 @@
#include "common.h"
#include "android-ashmem.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#ifndef USEMMAP
#include <sys/shm.h>
#else
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#endif
#include <sys/wait.h>
#include <sys/types.h>
#define write_error(s) \
fprintf(stderr, "Error at %s:%d: %s\n", __FILE__, __LINE__, s)
// AFL++ constants
#define FORKSRV_FD 198
#define MAX_FILE (1024 * 1024)
#define SHMEM_FUZZ_HDR_SIZE 4
#define SHM_ENV_VAR "__AFL_SHM_ID"
#define SHM_FUZZ_ENV_VAR "__AFL_SHM_FUZZ_ID"
#define DEFAULT_PERMISSION 0600
/* Reporting errors */
#define FS_OPT_ERROR 0xf800008f
#define FS_OPT_GET_ERROR(x) ((x & 0x00ffff00) >> 8)
#define FS_OPT_SET_ERROR(x) ((x & 0x0000ffff) << 8)
#define FS_ERROR_MAP_SIZE 1
#define FS_ERROR_MAP_ADDR 2
#define FS_ERROR_SHM_OPEN 4
#define FS_ERROR_SHMAT 8
#define FS_ERROR_MMAP 16
#define FS_ERROR_OLD_CMPLOG 32
#define FS_ERROR_OLD_CMPLOG_QEMU 64
#define FS_NEW_VERSION_MAX 1
#define FS_NEW_OPT_MAPSIZE 0x1
#define FS_NEW_OPT_SHDMEM_FUZZ 0x2
#define FS_NEW_OPT_AUTODICT 0x800
/* Reporting options */
#define FS_OPT_ENABLED 0x80000001
#define FS_OPT_MAPSIZE 0x40000000
#define FS_OPT_SNAPSHOT 0x20000000
#define FS_OPT_AUTODICT 0x10000000
#define FS_OPT_SHDMEM_FUZZ 0x01000000
#define FS_OPT_NEWCMPLOG 0x02000000
#define FS_OPT_OLD_AFLPP_WORKAROUND 0x0f000000
// FS_OPT_MAX_MAPSIZE is 8388608 = 0x800000 = 2^23 = 1 << 22
#define FS_OPT_MAX_MAPSIZE ((0x00fffffeU >> 1) + 1)
#define FS_OPT_GET_MAPSIZE(x) (((x & 0x00fffffe) >> 1) + 1)
#define FS_OPT_SET_MAPSIZE(x) \
(x <= 1 || x > FS_OPT_MAX_MAPSIZE ? 0 : ((x - 1) << 1))
// Set by this macro
// https://github.com/AFLplusplus/AFLplusplus/blob/stable/src/afl-cc.c#L993
int __afl_sharedmem_fuzzing __attribute__((weak));
extern uint8_t *__afl_area_ptr;
extern size_t __afl_map_size;
extern uint8_t *__token_start;
extern uint8_t *__token_stop;
uint8_t *__afl_fuzz_ptr;
static uint32_t __afl_fuzz_len_local;
uint32_t *__afl_fuzz_len = &__afl_fuzz_len_local;
int already_initialized_shm;
int already_initialized_forkserver;
static int child_pid;
static void (*old_sigterm_handler)(int) = 0;
static uint8_t is_persistent;
void __afl_set_persistent_mode(uint8_t mode) {
is_persistent = mode;
}
/* Error reporting to forkserver controller */
static void send_forkserver_error(int error) {
uint32_t status;
if (!error || error > 0xffff) return;
status = (FS_OPT_ERROR | FS_OPT_SET_ERROR(error));
if (write(FORKSRV_FD + 1, (char *)&status, 4) != 4) { return; }
}
/* Ensure we kill the child on termination */
static void at_exit(int signal) {
(void)signal;
if (child_pid > 0) {
kill(child_pid, SIGKILL);
child_pid = -1;
}
_exit(0);
}
/* SHM fuzzing setup. */
void __afl_map_shm(void) {
if (already_initialized_shm) return;
already_initialized_shm = 1;
char *id_str = getenv(SHM_ENV_VAR);
if (id_str) {
#ifdef USEMMAP
const char *shm_file_path = id_str;
int shm_fd = -1;
unsigned char *shm_base = NULL;
/* create the shared memory segment as if it was a file */
shm_fd = shm_open(shm_file_path, O_RDWR, DEFAULT_PERMISSION);
if (shm_fd == -1) {
fprintf(stderr, "shm_open() failed\n");
send_forkserver_error(FS_ERROR_SHM_OPEN);
exit(1);
}
shm_base =
mmap(0, __afl_map_size, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
close(shm_fd);
shm_fd = -1;
if (shm_base == MAP_FAILED) {
fprintf(stderr, "mmap() failed\n");
perror("mmap for map");
send_forkserver_error(FS_ERROR_MMAP);
exit(2);
}
__afl_area_ptr = shm_base;
#else
uint32_t shm_id = atoi(id_str);
__afl_area_ptr = (uint8_t *)shmat(shm_id, NULL, 0);
/* Whooooops. */
if (!__afl_area_ptr || __afl_area_ptr == (void *)-1) {
send_forkserver_error(FS_ERROR_SHMAT);
perror("shmat for map");
exit(1);
}
#endif
/* Write something into the bitmap so that even with low AFL_INST_RATIO,
our parent doesn't give up on us. */
__afl_area_ptr[0] = 1;
} else {
fprintf(stderr,
"Error: variable for edge coverage shared memory is not set\n");
send_forkserver_error(FS_ERROR_SHM_OPEN);
exit(1);
}
}
static void map_input_shared_memory() {
char *id_str = getenv(SHM_FUZZ_ENV_VAR);
if (id_str) {
uint8_t *map = NULL;
#ifdef USEMMAP
const char *shm_file_path = id_str;
int shm_fd = -1;
/* create the shared memory segment as if it was a file */
shm_fd = shm_open(shm_file_path, O_RDWR, DEFAULT_PERMISSION);
if (shm_fd == -1) {
fprintf(stderr, "shm_open() failed for fuzz\n");
send_forkserver_error(FS_ERROR_SHM_OPEN);
exit(1);
}
map = (uint8_t *)mmap(0, MAX_FILE + sizeof(uint32_t), PROT_READ, MAP_SHARED,
shm_fd, 0);
#else
uint32_t shm_id = atoi(id_str);
map = (uint8_t *)shmat(shm_id, NULL, 0);
#endif
/* Whooooops. */
if (!map || map == (void *)-1) {
perror("Could not access fuzzing shared memory");
send_forkserver_error(FS_ERROR_SHM_OPEN);
exit(1);
}
__afl_fuzz_len = (uint32_t *)map;
__afl_fuzz_ptr = map + sizeof(uint32_t);
} else {
fprintf(stderr, "Error: variable for fuzzing shared memory is not set\n");
send_forkserver_error(FS_ERROR_SHM_OPEN);
exit(1);
}
}
/* Fork server logic. */
void __afl_start_forkserver(void) {
if (already_initialized_forkserver) return;
already_initialized_forkserver = 1;
struct sigaction orig_action;
sigaction(SIGTERM, NULL, &orig_action);
old_sigterm_handler = orig_action.sa_handler;
signal(SIGTERM, at_exit);
uint32_t already_read_first = 0;
uint32_t was_killed;
uint32_t version = 0x41464c00 + FS_NEW_VERSION_MAX;
uint32_t tmp = version ^ 0xffffffff;
uint32_t status = version;
uint32_t status2 = version;
uint8_t *msg = (uint8_t *)&status;
uint8_t *reply = (uint8_t *)&status2;
uint8_t child_stopped = 0;
void (*old_sigchld_handler)(int) = signal(SIGCHLD, SIG_DFL);
int autotokens_on = __token_start != NULL && __token_stop != NULL;
/* Phone home and tell the parent that we're OK. If parent isn't there,
assume we're not running in forkserver mode and just execute program. */
// return because possible non-forkserver usage
if (write(FORKSRV_FD + 1, msg, 4) != 4) { return; }
if (read(FORKSRV_FD, reply, 4) != 4) { _exit(1); }
if (tmp != status2) {
write_error("wrong forkserver message from AFL++ tool");
_exit(1);
}
status = FS_NEW_OPT_MAPSIZE;
if (__afl_sharedmem_fuzzing) { status |= FS_NEW_OPT_SHDMEM_FUZZ; }
if (autotokens_on) { status |= FS_NEW_OPT_AUTODICT; }
if (write(FORKSRV_FD + 1, msg, 4) != 4) { _exit(1); }
// Now send the parameters for the set options, increasing by option number
// FS_NEW_OPT_MAPSIZE - we always send the map size
status = __afl_map_size;
if (write(FORKSRV_FD + 1, msg, 4) != 4) { _exit(1); }
// FS_NEW_OPT_AUTODICT - send autotokens
if (autotokens_on) {
// pass the autotokens through the forkserver FD
uint32_t len = (__token_stop - __token_start), offset = 0;
if (write(FORKSRV_FD + 1, &len, 4) != 4) {
fprintf(stderr, "Error: could not send autotokens len\n");
_exit(1);
}
while (len != 0) {
int32_t ret;
ret = write(FORKSRV_FD + 1, __token_start + offset, len);
if (ret < 1) {
write_error("could not send autotokens");
_exit(1);
}
len -= ret;
offset += ret;
}
}
// send welcome message as final message
status = version;
if (write(FORKSRV_FD + 1, msg, 4) != 4) { _exit(1); }
if (__afl_sharedmem_fuzzing) { map_input_shared_memory(); }
while (1) {
int status;
/* Wait for parent by reading from the pipe. Abort if read fails. */
if (already_read_first) {
already_read_first = 0;
} else {
if (read(FORKSRV_FD, &was_killed, 4) != 4) {
// write_error("read from afl-fuzz");
_exit(1);
}
}
/* If we stopped the child in persistent mode, but there was a race
condition and afl-fuzz already issued SIGKILL, write off the old
process. */
if (child_stopped && was_killed) {
child_stopped = 0;
if (waitpid(child_pid, &status, 0) < 0) {
write_error("child_stopped && was_killed");
_exit(1);
}
}
if (!child_stopped) {
/* Once woken up, create a clone of our process. */
child_pid = fork();
if (child_pid < 0) {
write_error("fork");
_exit(1);
}
/* In child process: close fds, resume execution. */
if (!child_pid) {
//(void)nice(-20);
signal(SIGCHLD, old_sigchld_handler);
signal(SIGTERM, old_sigterm_handler);
close(FORKSRV_FD);
close(FORKSRV_FD + 1);
return;
}
} else {
/* Special handling for persistent mode: if the child is alive but
currently stopped, simply restart it with SIGCONT. */
kill(child_pid, SIGCONT);
child_stopped = 0;
}
/* In parent process: write PID to pipe, then wait for child. */
if (write(FORKSRV_FD + 1, &child_pid, 4) != 4) {
write_error("write to afl-fuzz");
_exit(1);
}
if (waitpid(child_pid, &status, is_persistent ? WUNTRACED : 0) < 0) {
write_error("waitpid");
_exit(1);
}
/* In persistent mode, the child stops itself with SIGSTOP to indicate
a successful run. In this case, we want to wake it up without forking
again. */
if (WIFSTOPPED(status)) child_stopped = 1;
/* Relay wait status to pipe, then loop back. */
if (write(FORKSRV_FD + 1, &status, 4) != 4) {
write_error("writing to afl-fuzz");
_exit(1);
}
}
}

View File

@ -1,26 +1,433 @@
//! Forkserver logic into targets
unsafe extern "C" {
/// Map a shared memory region for the edge coverage map.
fn __afl_map_shm();
/// Start the forkserver.
fn __afl_start_forkserver();
use std::{
os::fd::{AsFd, AsRawFd, BorrowedFd},
sync::OnceLock,
};
use core::sync::atomic::{AtomicBool, Ordering};
use libafl::{
Error,
executors::forkserver::{
FORKSRV_FD, FS_ERROR_SHM_OPEN, FS_NEW_OPT_AUTODTCT, FS_NEW_OPT_MAPSIZE,
FS_NEW_OPT_SHDMEM_FUZZ, FS_NEW_VERSION_MAX, FS_OPT_ERROR, SHM_ENV_VAR, SHM_FUZZ_ENV_VAR,
},
};
use libafl_bolts::os::{ChildHandle, ForkResult};
use nix::{
sys::signal::{SigHandler, Signal},
unistd::Pid,
};
use crate::coverage::{__afl_map_size, EDGES_MAP_PTR, INPUT_LENGTH_PTR, INPUT_PTR, SHM_FUZZING};
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
use crate::coverage::{__token_start, __token_stop};
/// SAFETY:
///
/// This fd will be closed after being forked as a child. Thus this fd shall never be
/// used after that.
const FORKSRV_R_FD: BorrowedFd<'static> = unsafe { BorrowedFd::borrow_raw(FORKSRV_FD) };
/// SAFETY:
///
/// This fd will be closed after being forked as a child. Thus this fd shall never be
/// used after that.
const FORKSRV_W_FD: BorrowedFd<'static> = unsafe { BorrowedFd::borrow_raw(FORKSRV_FD + 1) };
fn fs_opt_set_error(error: i32) -> i32 {
(error & 0xFFFF) << 8
}
fn write_to_forkserver(message: &[u8]) -> Result<(), Error> {
let bytes_written = nix::unistd::write(FORKSRV_W_FD, message)?;
if bytes_written != message.len() {
return Err(Error::illegal_state(format!(
"Could not write to target fd. Expected {} bytes, wrote {bytes_written} bytes",
message.len()
)));
}
Ok(())
}
fn write_all_to_forkserver(message: &[u8]) -> Result<(), Error> {
let mut remain_len = message.len();
while remain_len > 0 {
let bytes_written = nix::unistd::write(FORKSRV_W_FD, message)?;
remain_len -= bytes_written;
}
Ok(())
}
fn write_u32_to_forkserver(message: u32) -> Result<(), Error> {
write_to_forkserver(&message.to_ne_bytes())
}
fn write_error_to_forkserver(error: i32) -> Result<(), Error> {
if error == 0 || error > 0xFFFF {
return Err(Error::illegal_argument("illegal error sent to forkserver"));
}
#[expect(clippy::cast_sign_loss)]
write_u32_to_forkserver((fs_opt_set_error(error) | FS_OPT_ERROR) as u32)
}
fn read_from_forkserver(message: &mut [u8]) -> Result<(), Error> {
let bytes_read = nix::unistd::read(FORKSRV_R_FD.as_fd().as_raw_fd(), message)?;
if bytes_read != message.len() {
return Err(Error::illegal_state(format!(
"Could not read from st pipe. Expected {} bytes, got {bytes_read} bytes",
message.len()
)));
}
Ok(())
}
fn read_u32_from_forkserver() -> Result<u32, Error> {
let mut buf = [0u8; 4];
read_from_forkserver(&mut buf)?;
Ok(u32::from_ne_bytes(buf))
}
/// Guard [`map_shared_memory`] is invoked only once
static SHM_MAP_GUARD: OnceLock<()> = OnceLock::new();
/// Map a shared memory region for the edge coverage map.
/// The [`EDGES_MAP_PTR`] will be updated.
///
/// # Note
///
/// The function's logic is written in C and this code is a wrapper.
pub fn map_shared_memory() {
unsafe { __afl_map_shm() }
/// If anything failed, the forkserver will be notified with
/// [`FS_ERROR_SHM_OPEN`].
pub fn map_shared_memory() -> Result<(), Error> {
if SHM_MAP_GUARD.set(()).is_err() {
return Err(Error::illegal_state("shared memory has been mapped before"));
}
map_shared_memory_internal()
}
/// Start the forkserver from this point. Any shared memory must be created before.
///
/// # Note
///
/// The forkserver logic is written in C and this code is a wrapper.
pub fn start_forkserver() {
unsafe { __afl_start_forkserver() }
fn map_shared_memory_internal() -> Result<(), Error> {
let Ok(id_str) = std::env::var(SHM_ENV_VAR) else {
write_error_to_forkserver(FS_ERROR_SHM_OPEN)?;
return Err(Error::illegal_argument(
"Error: variable for edge coverage shared memory is not set",
));
};
let Ok(shm_id) = id_str.parse() else {
write_error_to_forkserver(FS_ERROR_SHM_OPEN)?;
return Err(Error::illegal_argument("Invalid __AFL_SHM_ID value"));
};
let map = unsafe { libc::shmat(shm_id, core::ptr::null(), 0) };
if map.is_null() || core::ptr::eq(map, libc::MAP_FAILED) {
write_error_to_forkserver(FS_ERROR_SHM_OPEN)?;
return Err(Error::illegal_state("shmat for map"));
}
unsafe {
EDGES_MAP_PTR = map.cast();
}
Ok(())
}
/// Guard [`map_input_shared_memory`] is invoked only once
static INPUT_SHM_MAP_GUARD: OnceLock<()> = OnceLock::new();
/// Map the input shared memory region.
/// The [`INPUT_LENGTH_PTR`] and [`INPUT_PTR`] will be updated.
///
/// If anything failed, the forkserver will be notified with
/// [`FS_ERROR_SHM_OPEN`].
pub fn map_input_shared_memory() -> Result<(), Error> {
if INPUT_SHM_MAP_GUARD.set(()).is_err() {
return Err(Error::illegal_state("shared memory has been mapped before"));
}
map_input_shared_memory_internal()
}
fn map_input_shared_memory_internal() -> Result<(), Error> {
let Ok(id_str) = std::env::var(SHM_FUZZ_ENV_VAR) else {
write_error_to_forkserver(FS_ERROR_SHM_OPEN)?;
return Err(Error::illegal_argument(
"Error: variable for fuzzing shared memory is not set",
));
};
let Ok(shm_id) = id_str.parse() else {
write_error_to_forkserver(FS_ERROR_SHM_OPEN)?;
return Err(Error::illegal_argument("Invalid __AFL_SHM_FUZZ_ID value"));
};
let map = unsafe { libc::shmat(shm_id, core::ptr::null(), 0) };
if map.is_null() || core::ptr::eq(map, libc::MAP_FAILED) {
write_error_to_forkserver(FS_ERROR_SHM_OPEN)?;
return Err(Error::illegal_state(
"Could not access fuzzing shared memory",
));
}
let map: *mut u32 = map.cast();
unsafe {
INPUT_LENGTH_PTR = map;
INPUT_PTR = map.add(1).cast();
}
Ok(())
}
/// Parent to handle all logics with forkserver children
pub trait ForkserverParent {
/// Conduct initializing routine before fuzzing loop.
///
/// Usually, several signal handlers are registered in this function.
fn pre_fuzzing(&mut self) -> Result<(), Error>;
/// Spawn a child after the forkserver is ready.
///
/// If the forkserver has killed previous child, `was_killed` will be
/// set `true`.
///
/// The actual forking should be conduct in this function, and in persistent mode,
/// some tricks can be done to "fool" the forkserver that a child has been spawned.
fn spawn_child(&mut self, was_killed: bool) -> Result<ForkResult, Error>;
/// Interact with spawned child until the child has done its part.
///
/// This function should return a status indicating the status of child. Usually,
/// that status is determined by `waitpid`.
fn handle_child_requests(&mut self) -> Result<i32, Error>;
}
/// Whether the forkserver loop is going to stop soon.
///
/// This will be set to true if user send SIGTERM.
static STOP_SOON: AtomicBool = AtomicBool::new(false);
/// Set [`STOP_SOON`] to be `true`. Then the forkserver parent will kill all children
/// and then exit asynchrously.
extern "C" fn std_handle_sigterm(_signal: libc::c_int) {
STOP_SOON.store(true, Ordering::Relaxed);
}
/// Forkserver parent that can handle both non-persistent and persistent mode
#[derive(Debug, Default)]
pub struct MaybePersistentForkserverParent {
last_child_pid: Option<i32>,
/// This field is only touched for persistent mode to indicating
/// whether the child is temporarily stopped or terminated
child_stopped: bool,
old_sigchld_handler: Option<SigHandler>,
old_sigterm_handler: Option<SigHandler>,
}
impl MaybePersistentForkserverParent {
/// Create a new forkserver parent.
#[must_use]
pub fn new() -> Self {
MaybePersistentForkserverParent::default()
}
}
impl ForkserverParent for MaybePersistentForkserverParent {
fn pre_fuzzing(&mut self) -> Result<(), Error> {
let old_sigchld_handler =
(unsafe { nix::sys::signal::signal(Signal::SIGCHLD, SigHandler::SigDfl) })
.inspect_err(|_| {
log::error!("Fail to swap signal handler for SIGCHLD.");
})?;
self.old_sigchld_handler = Some(old_sigchld_handler);
let old_sigterm_handler = (unsafe {
nix::sys::signal::signal(Signal::SIGTERM, SigHandler::Handler(std_handle_sigterm))
})
.inspect_err(|_| {
log::error!("Fail to swap signal handler for SIGTERM.");
})?;
self.old_sigterm_handler = Some(old_sigterm_handler);
Ok(())
}
fn spawn_child(&mut self, was_killed: bool) -> Result<ForkResult, Error> {
if STOP_SOON.load(Ordering::Relaxed) {
if let Some(child_pid) = self.last_child_pid.take() {
nix::sys::signal::kill(Pid::from_raw(child_pid), Signal::SIGKILL)?;
}
std::process::exit(0);
}
// If we stopped the child in persistent mode, but there was a race
// condition and afl-fuzz already issued SIGKILL, write off the old
// process.
if self.child_stopped && was_killed {
self.child_stopped = false;
// unwrap here: child_stopped is set as true only if it has spawned
// a child, wait it, and get a stopped signal. Moreover, was_killed is
// true only if the forkserver killed such child. In all cases, the
// last_child_pid will never be None.
if nix::sys::wait::waitpid(
Pid::from_raw(self.last_child_pid.take().unwrap()),
None,
)
.is_err()
{
return Err(Error::illegal_state("child_stopped && was_killed"));
}
}
if self.child_stopped {
// Special handling for persistent mode: if the child is alive but
// currently stopped, simply restart it with SIGCONT.
// unwrap here: child_stopped is true only if last_child_pid is some.
let child_pid = *self.last_child_pid.as_ref().unwrap();
nix::sys::signal::kill(Pid::from_raw(child_pid), Signal::SIGCONT)?;
self.child_stopped = false;
Ok(ForkResult::Parent(ChildHandle { pid: child_pid }))
} else {
// Once woken up, create a clone of our process.
let fork_result = (unsafe { libafl_bolts::os::fork() }).inspect_err(|_| {
log::error!("fork");
})?;
match &fork_result {
ForkResult::Parent(child_pid) => {
self.last_child_pid = Some(child_pid.pid);
}
ForkResult::Child => unsafe {
// unwrap here: the field is assigned in `pre_fuzzing`
nix::sys::signal::signal(Signal::SIGCHLD, self.old_sigchld_handler.take().unwrap())
.inspect_err(|_| {
log::error!("Fail to restore signal handler for SIGCHLD.");
})?;
// unwrap here: the field is assigned in `pre_fuzzing`
nix::sys::signal::signal(Signal::SIGTERM, self.old_sigterm_handler.take().unwrap())
.inspect_err(|_| {
log::error!("Fail to restore signal handler for SIGTERM.");
})?;
},
}
Ok(fork_result)
}
}
fn handle_child_requests(&mut self) -> Result<i32, Error> {
let mut status = 0i32;
// unwrap here: the field is assigned if we are parent process in `spawn_child`
if unsafe { libc::waitpid(*self.last_child_pid.as_ref().unwrap(), &raw mut status, 0) < 0 } {
return Err(Error::illegal_state("waitpid"));
}
if libc::WIFSTOPPED(status) {
self.child_stopped = true;
}
Ok(status)
}
}
/// Success state when [`start_forkserver`] returned.
#[derive(Debug)]
pub enum ForkserverState {
/// There is no AFL forkserver responded. In such case,
/// we should allow user to do a normal execution.
NoAfl,
/// Current process is a spawned child.
Child,
}
/// Guard [`start_forkserver`] is invoked only once
static FORKSERVER_GUARD: OnceLock<()> = OnceLock::new();
/// Start a forkserver. This function will handle all communication
/// with AFL forkserver end, and use `forkserver_parent` to interact
/// with forked child.
///
/// This function will spawn a child in each round, and in the root process,
/// the loop will never return if everything is OK.
///
/// Before invoking this function, you should initialize [`EDGES_MAP_PTR`],
/// [`INPUT_PTR`] and [`INPUT_LENGTH_PTR`] properly. [`map_shared_memory`] and
/// [`map_input_shared_memory`] can be used, for example.
pub fn start_forkserver<P: ForkserverParent>(
forkserver_parent: &mut P,
) -> Result<ForkserverState, Error> {
if FORKSERVER_GUARD.set(()).is_err() {
return Err(Error::illegal_state("forkserver has been started before"));
}
start_forkserver_internal(forkserver_parent)
}
const VERSION: u32 = 0x41464c00 + FS_NEW_VERSION_MAX;
fn start_forkserver_internal<P: ForkserverParent>(
forkserver_parent: &mut P,
) -> Result<ForkserverState, Error> {
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
let autotokens_on = unsafe { !__token_start.is_null() && !__token_stop.is_null() };
let sharedmem_fuzzing = unsafe { SHM_FUZZING == 1 };
// Parent supports testcases via shared map - and the user wants to use it. Tell AFL.
// Phone home and tell the parent that we're OK. If parent isn't there, assume we're
// not running in forkserver mode and just execute program.
if write_u32_to_forkserver(VERSION).is_err() {
return Ok(ForkserverState::NoAfl);
}
let reply = read_u32_from_forkserver()?;
if reply != VERSION ^ 0xFFFFFFFF {
return Err(Error::illegal_state(
"wrong forkserver message from AFL++ tool",
));
}
let mut status = FS_NEW_OPT_MAPSIZE;
if sharedmem_fuzzing {
status |= FS_NEW_OPT_SHDMEM_FUZZ;
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
if autotokens_on {
status |= FS_NEW_OPT_AUTODTCT;
}
#[expect(clippy::cast_sign_loss)]
write_u32_to_forkserver(status as u32)?;
// Now send the parameters for the set options, increasing by option number
// FS_NEW_OPT_MAPSIZE - we always send the map size
write_u32_to_forkserver(unsafe { __afl_map_size as u32 })?;
// FS_NEW_OPT_AUTODICT - send autotokens
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
if autotokens_on {
#[expect(clippy::cast_sign_loss)]
let tokens_len = unsafe { __token_stop.offset_from(__token_start) } as u32;
write_u32_to_forkserver(tokens_len).inspect_err(|_| {
log::error!("Error: could not send autotokens len");
})?;
write_all_to_forkserver(unsafe {
core::slice::from_raw_parts(__token_start, tokens_len as usize)
})
.inspect_err(|_| {
log::error!("could not send autotokens");
})?;
}
// send welcome message as final message
write_u32_to_forkserver(VERSION)?;
forkserver_parent.pre_fuzzing()?;
loop {
// Wait for parent by reading from the pipe. Abort if read fails.
let was_killed = read_u32_from_forkserver()?;
let fork_result = forkserver_parent.spawn_child(was_killed != 0)?;
match fork_result {
ForkResult::Child => {
// FORKSRV_FD is for communication with AFL, we don't need it in the child
let _ = nix::unistd::close(FORKSRV_R_FD.as_raw_fd());
let _ = nix::unistd::close(FORKSRV_W_FD.as_raw_fd());
return Ok(ForkserverState::Child);
}
ForkResult::Parent(child_pid) => {
#[expect(clippy::cast_sign_loss)]
write_u32_to_forkserver(child_pid.pid as u32).inspect_err(|_| {
log::error!("write to afl-fuzz");
})?;
}
}
let status = forkserver_parent.handle_child_requests()?;
// Relay wait status to AFL pipe, then loop back.
#[expect(clippy::cast_sign_loss)]
write_u32_to_forkserver(status as u32).inspect_err(|_| {
log::error!("writing to afl-fuzz");
})?;
}
}

View File

@ -127,7 +127,7 @@ pub mod windows_asan;
#[cfg(all(windows, feature = "std", feature = "windows_asan"))]
pub use windows_asan::*;
#[cfg(all(unix, feature = "forkserver"))]
#[cfg(all(unix, feature = "std", feature = "forkserver"))]
pub mod forkserver;
#[cfg(all(unix, feature = "forkserver"))]
#[cfg(all(unix, feature = "std", feature = "forkserver"))]
pub use forkserver::*;