Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -641,7 +641,7 @@ jobs:
- name: Clippy
run: cargo clippy --tests --all --exclude libafl_nyx --exclude symcc_runtime --exclude runtime_test

android:
ubuntu-cross-android-arm64:
runs-on: ubuntu-24.04
steps:
- uses: dtolnay/rust-toolchain@stable
Expand All @@ -657,6 +657,27 @@ jobs:
- name: Build Android
run: cd libafl && PYO3_CROSS_PYTHON_VERSION=$(python3 -c "print('{}.{}'.format(__import__('sys').version_info.major, __import__('sys').version_info.minor))") cargo ndk -t arm64-v8a build --release

ubuntu-cross-android-x86_64:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: ./.github/workflows/ubuntu-prepare
- uses: Swatinem/rust-cache@v2
- uses: nttld/setup-ndk@v1
id: setup-ndk
with:
ndk-version: r27c
add-to-path: false
- name: cargo-ndk
run: cargo install cargo-ndk
- name: cargo android targets
run: |
rustup target add x86_64-linux-android
- name: Build Android
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
run: cargo ndk -t x86_64 build
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add this to a justfile? We want to have every CI task in a justfile in the future

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I don't really know about the detail. @wtdcode Can you do this in your branch? I could merge such changes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@domenukk It is not a new fuzzer or test but just doing cross-building. I don't see a directory to place such justfiles. ./just seems common library scripts.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create a repo-wide justfile, we'll need it at some point anyway #3099

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But yes can be done later

#run: cargo build --target aarch64-linux-android
# TODO: Figure out how to properly build stuff with clang
#- name: Add clang path to $PATH env
Expand Down
9 changes: 7 additions & 2 deletions fuzzers/forkserver/forkserver_libafl_cc/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
use libafl_bolts::shmem::{ShMemProvider, StdShMemProvider};
use libafl_targets::{
map_input_shared_memory, map_shared_memory, start_forkserver, MaybePersistentForkserverParent,
};

#[no_mangle]
pub extern "C" fn libafl_start_forkserver() {
let Ok(mut shm_provider) = StdShMemProvider::new() else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Android StdShMemProvider spawns a server, we need to double check if this works for this use case (otherwise we should set a different provider here)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I totally agree. But I know little about Android, and I also don't know why the standard shmem provider needs a service wrapper. Maybe this should be checked by someone knowing this detail.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need a service on android to hand out ashmem handles via unix sockets, as that is the only way to get a 'reference' to an existing shared memory allocation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for multi process IPC. For a forkserver we don't need that since the child can just inherit the allocations on fork

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Evian-Zhang probably we want to hard-code AshMemProvider here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, is it possible for someone to setup a android fuzzer test CI so that we can test the correctness? I personally don't know any of android things, so although I'm willing to hard code it, I'm afraid we cannot ensure its correctness.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will do that shortly these days. Will ping you if there is some failures.

Considering the slow performance of QEMU emulation, maybe we can just test simple fuzzers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! I will improve this shmem after your work :)

std::process::exit(1);
};

// Map shared memory region for the edge coverage map
if map_shared_memory().is_err() {
if map_shared_memory(&mut shm_provider).is_err() {
std::process::exit(1);
}
// Map shared memory region for input and its len
if map_input_shared_memory().is_err() {
if map_input_shared_memory(&mut shm_provider).is_err() {
std::process::exit(1);
};
// Start the forkserver
Expand Down
9 changes: 7 additions & 2 deletions fuzzers/forkserver/fuzzbench_forkserver_sand/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
use libafl_bolts::shmem::{ShMemProvider, StdShMemProvider};
use libafl_targets::{
map_input_shared_memory, map_shared_memory, start_forkserver, MaybePersistentForkserverParent,
};

#[no_mangle]
pub extern "C" fn libafl_start_forkserver() {
let Ok(mut shm_provider) = StdShMemProvider::new() else {
std::process::exit(1);
};

// Map shared memory region for the edge coverage map
if map_shared_memory().is_err() {
if map_shared_memory(&mut shm_provider).is_err() {
std::process::exit(1);
}
// Map shared memory region for input and its len
if map_input_shared_memory().is_err() {
if map_input_shared_memory(&mut shm_provider).is_err() {
std::process::exit(1);
};
// Start the forkserver
Expand Down
8 changes: 5 additions & 3 deletions libafl/src/executors/forkserver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,11 @@ fn report_error_and_exit(status: i32) -> Result<(), Error> {
}

/// The length of header bytes which tells shmem size
const SHMEM_FUZZ_HDR_SIZE: usize = 4;
const MAX_INPUT_SIZE_DEFAULT: usize = 1024 * 1024;
const MIN_INPUT_SIZE_DEFAULT: usize = 1;
pub const SHMEM_FUZZ_HDR_SIZE: usize = 4;
/// Maximum default length for input
pub const MAX_INPUT_SIZE_DEFAULT: usize = 1024 * 1024;
/// Minimum default length for input
pub 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 the page size (at least/usually `testcase_size_max + sizeof::<u32>()`)
Expand Down
13 changes: 11 additions & 2 deletions libafl_bolts/src/shmem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
#[cfg(feature = "alloc")]
use alloc::{rc::Rc, string::ToString, vec::Vec};
#[cfg(feature = "alloc")]
use core::{cell::RefCell, fmt, fmt::Display, mem::ManuallyDrop};
use core::{cell::RefCell, fmt, fmt::Display};
use core::{
fmt::Debug,
mem::size_of,
mem::{ManuallyDrop, size_of},
ops::{Deref, DerefMut},
};
#[cfg(feature = "std")]
Expand Down Expand Up @@ -247,6 +247,15 @@ pub trait ShMem: Sized + Debug + Clone + DerefMut<Target = [u8]> {
}
}

/// Consume current shared memory structure, and get the raw pointer to
/// this shared memory.
///
/// Note that calling this method will result in a memory leak.
fn into_raw<T: Sized>(self) -> *mut T {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't 100% get the need for this function, why can't we keep a reference to the shmem around in general?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shms are globally owned and we will need lots of global OnceCell to keep them. Note we can’t change EDGES_MAP or so to OnceCell because of symbol names. Therefore, I guess the intention is to avoid such duplicate cells? I’m okay with either way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, just as @wtdcode said, the whole forksever logic relies on those global raw pointers, if we maintain those raw pointers and the shmem at the same time, we are creating potentially two mutable references to the same memory, which I think the memory leak is a better choice. And the original version of forksever also leaks the memory.

Copy link
Member

@domenukk domenukk May 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep this as a (non-public) function inside the forkserver then, it shouldn't be something most people/not part of the ShMem trait, IMHO.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK.

let mut manually_dropped = ManuallyDrop::new(self);
manually_dropped.as_mut_ptr().cast()
}

/// Get the description of the shared memory mapping
fn description(&self) -> ShMemDescription {
ShMemDescription {
Expand Down
133 changes: 70 additions & 63 deletions libafl_targets/src/forkserver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,31 @@ use std::{
sync::OnceLock,
};

#[cfg(any(target_os = "linux", target_vendor = "apple"))]
use libafl::executors::forkserver::FS_NEW_OPT_AUTODTCT;
#[cfg(feature = "cmplog")]
use libafl::executors::forkserver::SHM_CMPLOG_ENV_VAR;
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_CMPLOG_ENV_VAR, SHM_ENV_VAR,
SHM_FUZZ_ENV_VAR,
AFL_MAP_SIZE_ENV_VAR, FORKSRV_FD, FS_ERROR_SHM_OPEN, FS_NEW_OPT_MAPSIZE,
FS_NEW_OPT_SHDMEM_FUZZ, FS_NEW_VERSION_MAX, FS_OPT_ERROR, MAX_INPUT_SIZE_DEFAULT,
SHM_ENV_VAR, SHM_FUZZ_ENV_VAR, SHM_FUZZ_MAP_SIZE_ENV_VAR, SHMEM_FUZZ_HDR_SIZE,
},
};
use libafl_bolts::os::{ChildHandle, ForkResult};
use libafl_bolts::{
os::{ChildHandle, ForkResult},
shmem::{ShMem, ShMemId, ShMemProvider},
};
use nix::{
sys::signal::{SigHandler, Signal},
unistd::Pid,
};

#[cfg(feature = "cmplog")]
use crate::cmps::CMPLOG_MAP_PTR;
#[cfg(feature = "cmplog_extended_instrumentation")]
use crate::cmps::EXTENDED_CMPLOG_MAP_PTR;
#[cfg(feature = "cmplog")]
use crate::cmps::{AflppCmpLogMap, CMPLOG_MAP_PTR};

use crate::coverage::{__afl_map_size, EDGES_MAP_PTR, INPUT_LENGTH_PTR, INPUT_PTR, SHM_FUZZING};
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
Expand Down Expand Up @@ -54,6 +61,7 @@ fn write_to_forkserver(message: &[u8]) -> Result<(), Error> {
}
Ok(())
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
fn write_all_to_forkserver(message: &[u8]) -> Result<(), Error> {
let mut remain_len = message.len();
while remain_len > 0 {
Expand Down Expand Up @@ -89,6 +97,30 @@ fn read_u32_from_forkserver() -> Result<u32, Error> {
Ok(u32::from_ne_bytes(buf))
}

fn map_shared_memory_common<SHM: ShMemProvider>(
shmem_provider: &mut SHM,
map_env_var: &str,
map_size_env_var: &str,
map_size_default_fallback: usize,
) -> Result<*mut u8, Error> {
let Ok(id_str) = std::env::var(map_env_var) else {
write_error_to_forkserver(FS_ERROR_SHM_OPEN)?;
return Err(Error::illegal_argument(format!(
"Error: shared memory variable {map_env_var} is not set"
)));
};
let map_size = if let Ok(map_size_str) = std::env::var(map_size_env_var) {
map_size_str
.parse()
.map_err(|_| Error::illegal_argument(format!("Invalid {map_size_env_var} value")))?
} else {
map_size_default_fallback
};
let shmem = shmem_provider.shmem_from_id_and_size(ShMemId::from_string(&id_str), map_size)?;

Ok(shmem.into_raw())
}

/// Guard [`map_shared_memory`] is invoked only once
static SHM_MAP_GUARD: OnceLock<()> = OnceLock::new();

Expand All @@ -97,31 +129,18 @@ static SHM_MAP_GUARD: OnceLock<()> = OnceLock::new();
///
/// If anything failed, the forkserver will be notified with
/// [`FS_ERROR_SHM_OPEN`].
pub fn map_shared_memory() -> Result<(), Error> {
pub fn map_shared_memory<SHM: ShMemProvider>(shmem_provider: &mut SHM) -> Result<(), Error> {
if SHM_MAP_GUARD.set(()).is_err() {
return Err(Error::illegal_state("shared memory has been mapped before"));
}
map_shared_memory_internal()
map_shared_memory_internal(shmem_provider)
}

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"));
}
fn map_shared_memory_internal<SHM: ShMemProvider>(shmem_provider: &mut SHM) -> Result<(), Error> {
let target_ptr =
map_shared_memory_common(shmem_provider, SHM_ENV_VAR, AFL_MAP_SIZE_ENV_VAR, 65536)?;
unsafe {
EDGES_MAP_PTR = map.cast();
EDGES_MAP_PTR = target_ptr;
}
Ok(())
}
Expand All @@ -134,32 +153,23 @@ static INPUT_SHM_MAP_GUARD: OnceLock<()> = OnceLock::new();
///
/// If anything failed, the forkserver will be notified with
/// [`FS_ERROR_SHM_OPEN`].
pub fn map_input_shared_memory() -> Result<(), Error> {
pub fn map_input_shared_memory<SHM: ShMemProvider>(shmem_provider: &mut SHM) -> 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()
map_input_shared_memory_internal(shmem_provider)
}

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();
fn map_input_shared_memory_internal<SHM: ShMemProvider>(
shmem_provider: &mut SHM,
) -> Result<(), Error> {
let target_ptr = map_shared_memory_common(
shmem_provider,
SHM_FUZZ_ENV_VAR,
SHM_FUZZ_MAP_SIZE_ENV_VAR,
MAX_INPUT_SIZE_DEFAULT + SHMEM_FUZZ_HDR_SIZE,
)?;
let map: *mut u32 = target_ptr.cast();
unsafe {
INPUT_LENGTH_PTR = map;
INPUT_PTR = map.add(1).cast();
Expand All @@ -177,36 +187,33 @@ static CMPLOG_SHM_MAP_GUARD: OnceLock<()> = OnceLock::new();
/// If anything failed, the forkserver will be notified with
/// [`FS_ERROR_SHM_OPEN`].
#[cfg(feature = "cmplog")]
pub fn map_cmplog_shared_memory() -> Result<(), Error> {
pub fn map_cmplog_shared_memory<SHM: ShMemProvider>(shmem_provider: &mut SHM) -> Result<(), Error> {
if CMPLOG_SHM_MAP_GUARD.set(()).is_err() {
return Err(Error::illegal_state("shared memory has been mapped before"));
}
map_cmplog_shared_memory_internal()
map_cmplog_shared_memory_internal(shmem_provider)
}

#[cfg(feature = "cmplog")]
fn map_cmplog_shared_memory_internal() -> Result<(), Error> {
fn map_cmplog_shared_memory_internal<SHM: ShMemProvider>(
shmem_provider: &mut SHM,
) -> Result<(), Error> {
let Ok(id_str) = std::env::var(SHM_CMPLOG_ENV_VAR) else {
write_error_to_forkserver(FS_ERROR_SHM_OPEN)?;
return Err(Error::illegal_argument(
"Error: variable for cmplog 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_CMPLOG_SHM_ID value"));
return Err(Error::illegal_argument(format!(
"Error: shared memory variable {SHM_CMPLOG_ENV_VAR} is not set"
)));
};
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"));
}
let map_size = size_of::<AflppCmpLogMap>();
let shmem = shmem_provider.shmem_from_id_and_size(ShMemId::from_string(&id_str), map_size)?;

let target_ptr = shmem.into_raw();
unsafe {
CMPLOG_MAP_PTR = map.cast();
CMPLOG_MAP_PTR = target_ptr;
}
#[cfg(feature = "cmplog_extended_instrumentation")]
unsafe {
EXTENDED_CMPLOG_MAP_PTR = map.cast();
EXTENDED_CMPLOG_MAP_PTR = target_ptr;
}
Ok(())
}
Expand Down
Loading