#![cfg(all(
test,
debug_assertions,
feature = "fault-injection",
feature = "alloc",
feature = "set"
))]
#[allow(clippy::module_inception)]
mod alloc_fault_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::fault::FaultPolicy;
use crate::{BStack, CheckedSlabBStackAllocator};
use rand::{RngExt, SeedableRng, rngs::StdRng};
use std::io;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
struct RandomFaults {
seed: u64,
per_mille: u64,
counter: AtomicU64,
}
impl RandomFaults {
fn new(seed: u64, per_mille: u64) -> Self {
Self {
seed,
per_mille,
counter: AtomicU64::new(0),
}
}
}
impl FaultPolicy for RandomFaults {
fn next_fault(&self, op: &'static str, _seq: u64) -> Option<io::Error> {
let n = self.counter.fetch_add(1, Ordering::Relaxed);
let mut z = self
.seed
.wrapping_add(n.wrapping_mul(0x9E37_79B9_7F4A_7C15));
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^= z >> 31;
(z % 1000 < self.per_mille)
.then(|| io::Error::other(format!("injected fault at {op} (n={n})")))
}
}
fn per_mille() -> u64 {
std::env::var("BSTACK_FAULT_PER_MILLE")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(30)
}
fn reopen_and_verify<A, F>(
alloc: A,
make: &F,
live: &[(BStackRange, Payload)],
bias: u64,
ctx: &str,
) -> A
where
A: BStackOwnedSliceAllocator,
F: Fn(BStack) -> io::Result<A>,
{
alloc.stack().set_fault_policy(None);
let stack = alloc.into_stack();
let alloc = make(stack).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!("{ctx} rec{i}"));
}
alloc
}
fn run_fault_fuzz<A, F>(make: F, seed_salt: u64)
where
A: BStackOwnedSliceAllocator,
F: Fn(BStack) -> io::Result<A>,
{
let cfg = FuzzConfig::from_env();
let path = temp_path("fault");
let _guard = Guard(path.clone());
let master_seed = std::env::var("BSTACK_FUZZ_SEED")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or_else(|| rand::rng().random_range(0..=u64::MAX));
eprintln!("[alloc_fault_tests salt={seed_salt:#06x}] BSTACK_FUZZ_SEED={master_seed}");
let mut rng = StdRng::seed_from_u64(master_seed ^ seed_salt);
let bias = rng.random_range(0..=u64::MAX);
let fault_seed = rng.random_range(0..=u64::MAX);
let policy: Arc<dyn FaultPolicy> = Arc::new(RandomFaults::new(fault_seed, per_mille()));
let mut alloc = make(BStack::open(&path).unwrap()).unwrap();
let mut live: Vec<(BStackRange, Payload)> = Vec::new();
let mut next_id = 0u64;
for step in 0..cfg.ops {
let mut faulted = false;
match gen_op(&mut rng, &cfg, !live.is_empty(), false) {
Operation::Alloc(len) => {
alloc.stack().set_fault_policy(Some(policy.clone()));
let r = alloc.alloc(len);
alloc.stack().set_fault_policy(None);
match r {
Ok(mut s) => {
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));
}
Err(_) => faulted = true,
}
}
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())
};
alloc.stack().set_fault_policy(Some(policy.clone()));
let r = alloc.realloc(s, new_len);
alloc.stack().set_fault_policy(None);
match r {
Ok(mut s2) => {
let preserved = old_len.min(new_len);
payload.verify_prefix(
&s2,
preserved,
bias,
"fault realloc: preserved prefix",
);
if new_len > old_len {
check_is_zero(
&s2.read().unwrap()[old_len as usize..],
"fault 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) => {
faulted = true;
if let Some(mut h) = e.handle {
if h.len() == old_len {
payload.verify(
&h,
bias,
"fault realloc err: untouched original",
);
} else {
let preserved = old_len.min(new_len);
payload.verify_prefix(
&h,
preserved,
bias,
"fault realloc err: committed-new prefix",
);
if h.len() > preserved {
check_is_zero(
&h.read_range(preserved, h.len()).unwrap(),
"fault realloc err: committed-new zero-extend",
);
}
}
let np =
make_payload(alloc.stack(), h.len(), next_id, &cfg, &mut rng);
next_id += 1;
np.write(&mut h, bias).unwrap();
live.push((h.as_range(), np));
}
}
}
}
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())
};
alloc.stack().set_fault_policy(Some(policy.clone()));
let r = alloc.dealloc(s);
alloc.stack().set_fault_policy(None);
match r {
Ok(()) => {}
Err(e) => {
faulted = true;
if let Some(h) = e.handle {
payload.verify(&h, bias, "fault dealloc err: retained");
live.push((h.as_range(), payload));
}
}
}
}
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, "fault: check");
}
Operation::Reopen => {}
}
let periodic = cfg.reopen_every > 0 && step > 0 && step % cfg.reopen_every == 0;
if faulted || periodic {
if let Ok(snap) = std::env::var("BSTACK_SNAPSHOT_PATH") {
let _ = std::fs::copy(&path, &snap);
let _ = std::fs::write(format!("{snap}.step"), format!("{step}"));
}
alloc = reopen_and_verify(alloc, &make, &live, bias, &format!("reopen@{step}"));
}
}
let _alloc = reopen_and_verify(alloc, &make, &live, bias, "final");
}
macro_rules! fault_suite {
($mod_name:ident, $make:expr, $salt:expr) => {
mod $mod_name {
use super::*;
#[test]
fn fault_fuzz() {
super::run_fault_fuzz($make, $salt);
}
}
};
}
fault_suite!(first_fit, make_allocator!(FirstFitBStackAllocator), 0x1111);
fault_suite!(
ghost_tree,
make_allocator!(GhostTreeBstackAllocator),
0x2222
);
fault_suite!(slab_16, make_allocator!(SlabBStackAllocator, 16), 0x3333);
fault_suite!(slab_64, make_allocator!(SlabBStackAllocator, 64), 0x4444);
fault_suite!(
check_slab_16,
make_allocator!(CheckedSlabBStackAllocator, 16),
0x5555
);
fault_suite!(
check_slab_64,
make_allocator!(CheckedSlabBStackAllocator, 64),
0x6666
);
fault_suite!(
segregated,
make_allocator!(SegregatedBStackAllocator),
0x7777
);
}