#![cfg(all(test, feature = "alloc", feature = "set"))]
#[allow(clippy::module_inception)]
mod alloc_fuzz_tests {
use crate::alloc::{
BStackOwnedSlice, BStackOwnedSliceAllocator, BStackRange, FirstFitBStackAllocator,
GhostTreeBstackAllocator, SegregatedBStackAllocator, SlabBStackAllocator,
};
use crate::alloc_test_common::{
FuzzConfig, Guard, Operation, Payload, check_is_zero, gen_op, make_allocator, make_payload,
temp_path,
};
use crate::{BStack, CheckedSlabBStackAllocator};
use rand::RngExt;
fn run_bias(rng: &mut impl RngExt) -> u64 {
rng.random_range(0..=u64::MAX)
}
fn run_alloc_dealloc<A, F>(make: F)
where
A: BStackOwnedSliceAllocator,
F: Fn(BStack) -> std::io::Result<A>,
{
let cfg = FuzzConfig::from_env();
let path = temp_path("ad");
let _guard = Guard(path.clone());
let alloc = make(BStack::open(&path).unwrap()).unwrap();
let mut rng = rand::rng();
let bias = run_bias(&mut rng);
let mut live: Vec<(BStackOwnedSlice<'_, A>, Payload)> = Vec::new();
let mut next_id = 0u64;
for _ in 0..cfg.ops {
match gen_op(&mut rng, &cfg, !live.is_empty(), false) {
Operation::Alloc(len) => {
if let Ok(mut s) = alloc.alloc(len) {
let payload = make_payload(alloc.stack(), s.len(), next_id, &cfg, &mut rng);
next_id += 1;
payload.write(&mut s, bias).unwrap();
payload.verify(&s, bias, "alloc_dealloc: post-write");
live.push((s, payload));
}
}
Operation::Dealloc => {
let i = rng.random_range(0..live.len());
let (s, payload) = live.swap_remove(i);
payload.verify(&s, bias, "alloc_dealloc: pre-dealloc");
alloc.dealloc(s).unwrap();
}
Operation::Realloc(_) | Operation::Check => {
let i = rng.random_range(0..live.len());
let (s, payload) = &live[i];
payload.verify(s, bias, "alloc_dealloc: check");
}
Operation::Reopen => {}
}
}
}
fn run_alloc_realloc_dealloc<A, F>(make: F)
where
A: BStackOwnedSliceAllocator,
F: Fn(BStack) -> std::io::Result<A>,
{
let cfg = FuzzConfig::from_env();
let path = temp_path("ard");
let _guard = Guard(path.clone());
let alloc = make(BStack::open(&path).unwrap()).unwrap();
let mut rng = rand::rng();
let bias = run_bias(&mut rng);
let mut live: Vec<(BStackOwnedSlice<'_, A>, Payload)> = Vec::new();
let mut next_id = 0u64;
for _ in 0..cfg.ops {
match gen_op(&mut rng, &cfg, !live.is_empty(), false) {
Operation::Alloc(len) => {
if let Ok(mut s) = alloc.alloc(len) {
let payload = make_payload(alloc.stack(), s.len(), next_id, &cfg, &mut rng);
next_id += 1;
payload.write(&mut s, bias).unwrap();
live.push((s, payload));
}
}
Operation::Realloc(new_len) => {
let i = rng.random_range(0..live.len());
let (s, payload) = live.swap_remove(i);
let old_len = s.len();
match alloc.realloc(s, new_len) {
Ok(mut s2) => {
let preserved = old_len.min(new_len);
payload.verify_prefix(
&s2,
preserved,
bias,
"realloc: preserved prefix",
);
if new_len > old_len {
check_is_zero(
&s2.read().unwrap()[old_len as usize..],
"realloc: zero-extend",
);
}
let np = make_payload(alloc.stack(), s2.len(), next_id, &cfg, &mut rng);
next_id += 1;
np.write(&mut s2, bias).unwrap();
live.push((s2, np));
}
Err(e) => {
if let Some(h) = e.handle {
live.push((h, payload));
}
}
}
}
Operation::Dealloc => {
let i = rng.random_range(0..live.len());
let (s, payload) = live.swap_remove(i);
payload.verify(&s, bias, "alloc_realloc_dealloc: pre-dealloc");
alloc.dealloc(s).unwrap();
}
Operation::Check => {
let i = rng.random_range(0..live.len());
let (s, payload) = &live[i];
payload.verify(s, bias, "alloc_realloc_dealloc: check");
}
Operation::Reopen => {}
}
}
}
fn run_reopen<A, F>(make: F)
where
A: BStackOwnedSliceAllocator,
F: Fn(BStack) -> std::io::Result<A>,
{
let cfg = FuzzConfig::from_env();
let path = temp_path("reopen");
let _guard = Guard(path.clone());
drop(make(BStack::open(&path).unwrap()).unwrap());
let mut rng = rand::rng();
let bias = run_bias(&mut rng);
let mut live: Vec<(BStackRange, Payload)> = Vec::new();
let mut next_id: u64 = 0;
for session in 0..cfg.sessions {
let alloc = make(BStack::open(&path).unwrap()).unwrap();
for (i, (range, payload)) in live.iter().enumerate() {
let s =
unsafe { BStackOwnedSlice::from_raw_parts(&alloc, range.start(), range.len()) };
payload.verify(&s, bias, &format!("reopen s{session} rec{i}"));
}
for _ in 0..cfg.ops_per_session {
match gen_op(&mut rng, &cfg, !live.is_empty(), false) {
Operation::Alloc(len) => {
if let Ok(mut s) = alloc.alloc(len) {
let payload =
make_payload(alloc.stack(), s.len(), next_id, &cfg, &mut rng);
next_id += 1;
payload.write(&mut s, bias).unwrap();
live.push((s.as_range(), payload));
}
}
Operation::Realloc(new_len) => {
let i = rng.random_range(0..live.len());
let (range, payload) = live.swap_remove(i);
let old_len = range.len();
let s = unsafe {
BStackOwnedSlice::from_raw_parts(&alloc, range.start(), range.len())
};
match alloc.realloc(s, new_len) {
Ok(mut s2) => {
let preserved = old_len.min(new_len);
payload.verify_prefix(
&s2,
preserved,
bias,
"reopen realloc: preserved prefix",
);
if new_len > old_len {
check_is_zero(
&s2.read().unwrap()[old_len as usize..],
"reopen realloc: zero-extend",
);
}
let np =
make_payload(alloc.stack(), s2.len(), next_id, &cfg, &mut rng);
next_id += 1;
np.write(&mut s2, bias).unwrap();
live.push((s2.as_range(), np));
}
Err(e) => {
if let Some(h) = e.handle {
live.push((h.as_range(), payload));
}
}
}
}
Operation::Dealloc => {
let i = rng.random_range(0..live.len());
let (range, payload) = live.swap_remove(i);
let s = unsafe {
BStackOwnedSlice::from_raw_parts(&alloc, range.start(), range.len())
};
payload.verify(&s, bias, "reopen: pre-dealloc");
alloc.dealloc(s).unwrap();
}
Operation::Check => {
let i = rng.random_range(0..live.len());
let (range, payload) = &live[i];
let s = unsafe {
BStackOwnedSlice::from_raw_parts(&alloc, range.start(), range.len())
};
payload.verify(&s, bias, "reopen: check");
}
Operation::Reopen => {}
}
}
drop(alloc.into_stack());
}
}
fn run_zero_size_alloc<A, F>(make: F)
where
A: BStackOwnedSliceAllocator,
F: Fn(BStack) -> std::io::Result<A>,
{
let cfg = FuzzConfig::from_env();
let path = temp_path("zalloc");
let _guard = Guard(path.clone());
let alloc = make(BStack::open(&path).unwrap()).unwrap();
let mut slices = Vec::new();
for _ in 0..cfg.ops {
let s = alloc.alloc(0).unwrap();
assert_eq!(s.len(), 0, "zero alloc must have len 0");
assert_eq!(s.start(), 0, "zero alloc must have start 0");
slices.push(s);
}
for s in slices {
alloc.dealloc(s).unwrap();
}
}
fn run_free_zero_slices<A, F>(make: F)
where
A: BStackOwnedSliceAllocator,
F: Fn(BStack) -> std::io::Result<A>,
{
let cfg = FuzzConfig::from_env();
let path = temp_path("fzero");
let _guard = Guard(path.clone());
let alloc = make(BStack::open(&path).unwrap()).unwrap();
let mut rng = rand::rng();
let bias = run_bias(&mut rng);
let mut live: Vec<(BStackOwnedSlice<'_, A>, Payload)> = Vec::new();
let mut next_id = 0u64;
for _ in 0..cfg.ops {
let zero = alloc.alloc(0).unwrap();
alloc.dealloc(zero).unwrap();
if rng.random_bool(0.5) || live.is_empty() {
let len = rng.random_range(16..=256);
if let Ok(mut s) = alloc.alloc(len) {
let payload = make_payload(alloc.stack(), s.len(), next_id, &cfg, &mut rng);
next_id += 1;
payload.write(&mut s, bias).unwrap();
live.push((s, payload));
}
} else {
let idx = rng.random_range(0..live.len());
let (s, payload) = live.swap_remove(idx);
payload.verify(&s, bias, "free_zero_slices");
alloc.dealloc(s).unwrap();
}
}
}
fn run_double_free_error<A, F>(make: F)
where
A: BStackOwnedSliceAllocator,
F: Fn(BStack) -> std::io::Result<A>,
{
let path = temp_path("dfree");
let _guard = Guard(path.clone());
let alloc = make(BStack::open(&path).unwrap()).unwrap();
let before = alloc.alloc(64).unwrap();
let target = alloc.alloc(64).unwrap();
let after = alloc.alloc(64).unwrap();
let (start, len) = (target.start(), target.len());
alloc.dealloc(target).unwrap();
let again = unsafe { BStackOwnedSlice::from_raw_parts(&alloc, start, len) };
let result = alloc.dealloc(again);
assert!(result.is_err(), "double-free must return an error");
alloc.dealloc(before).unwrap();
alloc.dealloc(after).unwrap();
}
macro_rules! fuzz_suite {
($mod_name:ident, $make:expr) => {
mod $mod_name {
use super::*;
#[test]
fn alloc_dealloc() {
super::run_alloc_dealloc($make);
}
#[test]
fn alloc_realloc_dealloc() {
super::run_alloc_realloc_dealloc($make);
}
#[test]
fn reopen() {
super::run_reopen($make);
}
#[test]
fn zero_size_alloc() {
super::run_zero_size_alloc($make);
}
#[test]
fn free_zero_slices() {
super::run_free_zero_slices($make);
}
}
};
}
fuzz_suite!(first_fit, make_allocator!(FirstFitBStackAllocator));
fuzz_suite!(ghost_tree, make_allocator!(GhostTreeBstackAllocator));
fuzz_suite!(slab_8, make_allocator!(SlabBStackAllocator, 8));
fuzz_suite!(slab_16, make_allocator!(SlabBStackAllocator, 16));
fuzz_suite!(slab_64, make_allocator!(SlabBStackAllocator, 64));
fuzz_suite!(
check_slab_16,
make_allocator!(CheckedSlabBStackAllocator, 16)
);
fuzz_suite!(
check_slab_64,
make_allocator!(CheckedSlabBStackAllocator, 64)
);
fuzz_suite!(segregated, make_allocator!(SegregatedBStackAllocator));
mod double_free {
use super::*;
#[test]
fn first_fit() {
super::run_double_free_error(make_allocator!(FirstFitBStackAllocator));
}
#[test]
fn check_slab_16() {
super::run_double_free_error(make_allocator!(CheckedSlabBStackAllocator, 16));
}
#[test]
fn segregated() {
super::run_double_free_error(make_allocator!(SegregatedBStackAllocator));
}
}
}