#![cfg(all(test, feature = "alloc", feature = "set"))]
#![allow(dead_code)]
use crate::BStack;
use crate::alloc::{BStackOwnedSlice, BStackOwnedSliceAllocator};
use std::io;
use std::sync::atomic::{AtomicU64, Ordering};
pub(crate) struct Guard(pub std::path::PathBuf);
impl Drop for Guard {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
pub(crate) fn temp_path(prefix: &str) -> std::path::PathBuf {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let id = COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
std::env::temp_dir().join(format!("bstack_fuzz_{prefix}_{pid}_{id}.bin"))
}
pub(crate) fn fill(buf: &mut [u8], id: u64, bias: u64) {
let seed = id ^ bias;
for (i, b) in buf.iter_mut().enumerate() {
*b = ((seed >> ((i % 8) * 8)) & 0xFF) as u8;
}
}
pub(crate) fn check(buf: &[u8], id: u64, bias: u64, ctx: &str) {
let seed = id ^ bias;
for (i, &b) in buf.iter().enumerate() {
let expected = ((seed >> ((i % 8) * 8)) & 0xFF) as u8;
assert_eq!(
b, expected,
"{ctx}: corruption at [{i}]: got {b:#04x}, expected {expected:#04x} (id={id}, bias={bias})"
);
}
}
pub(crate) fn check_is_zero(buf: &[u8], ctx: &str) {
for (i, &b) in buf.iter().enumerate() {
assert_eq!(b, 0, "{ctx}: expected zero at [{i}], got {b:#04x}");
}
}
pub(crate) enum Payload {
Seeded(u64),
Raw(Vec<u8>),
}
impl Payload {
pub(crate) fn len(&self, slice_len: u64) -> u64 {
match self {
Payload::Seeded(_) => slice_len,
Payload::Raw(bytes) => bytes.len() as u64,
}
}
pub(crate) fn write<A: BStackOwnedSliceAllocator>(
&self,
slice: &mut BStackOwnedSlice<'_, A>,
bias: u64,
) -> io::Result<()> {
match self {
Payload::Seeded(id) => {
let mut buf = vec![0u8; slice.len() as usize];
fill(&mut buf, *id, bias);
slice.write(&buf)
}
Payload::Raw(bytes) => slice.write(bytes),
}
}
pub(crate) fn verify<A: BStackOwnedSliceAllocator>(
&self,
slice: &BStackOwnedSlice<'_, A>,
bias: u64,
ctx: &str,
) {
let got = slice.read().unwrap();
match self {
Payload::Seeded(id) => check(&got, *id, bias, ctx),
Payload::Raw(bytes) => {
assert_eq!(got.len(), bytes.len(), "{ctx}: raw payload length mismatch");
assert!(got == *bytes, "{ctx}: raw payload corruption");
}
}
}
pub(crate) fn verify_prefix<A: BStackOwnedSliceAllocator>(
&self,
slice: &BStackOwnedSlice<'_, A>,
n: u64,
bias: u64,
ctx: &str,
) {
let got = slice.read_range(0, n).unwrap();
let n = n as usize;
match self {
Payload::Seeded(id) => check(&got, *id, bias, ctx),
Payload::Raw(bytes) => {
assert!(
bytes.len() >= n,
"{ctx}: stored raw payload shorter than prefix"
);
assert!(got == bytes[..n], "{ctx}: raw payload prefix corruption");
}
}
}
}
pub(crate) fn adversarial_bytes<R: rand::RngExt>(
stack: &BStack,
len: u64,
rng: &mut R,
) -> Option<Vec<u8>> {
if len == 0 {
return Some(Vec::new());
}
let total = stack.len().ok()?;
if total < len {
return None;
}
let aligned_max = (total - len) & !7u64;
let src = if aligned_max == 0 {
0
} else {
rng.random_range(0..=aligned_max / 8) * 8
};
stack.get(src, src + len).ok()
}
pub(crate) enum Operation {
Alloc(u64),
Realloc(u64),
Dealloc,
Check,
Reopen,
}
pub(crate) fn gen_op<R: rand::RngExt>(
rng: &mut R,
cfg: &FuzzConfig,
have_live: bool,
allow_reopen: bool,
) -> Operation {
if !have_live {
return Operation::Alloc(rng.random_range(0..=cfg.max_alloc));
}
let roll: u32 = rng.random_range(0..100);
match roll {
0..=44 => Operation::Alloc(rng.random_range(0..=cfg.max_alloc)),
45..=64 => Operation::Realloc(rng.random_range(0..=cfg.max_alloc)),
65..=79 => Operation::Dealloc,
80..=94 => Operation::Check,
_ if allow_reopen => Operation::Reopen,
_ => Operation::Check,
}
}
pub(crate) fn make_payload<R: rand::RngExt>(
stack: &BStack,
len: u64,
id: u64,
cfg: &FuzzConfig,
rng: &mut R,
) -> Payload {
if rng.random_range(0..100) < cfg.adversarial_pct
&& let Some(bytes) = adversarial_bytes(stack, len, rng)
{
return Payload::Raw(bytes);
}
Payload::Seeded(id)
}
pub(crate) struct FuzzConfig {
pub ops: usize,
pub sessions: usize,
pub ops_per_session: usize,
pub max_alloc: u64,
pub reopen_every: usize,
pub adversarial_pct: u32,
}
impl FuzzConfig {
pub(crate) fn from_env() -> Self {
fn env<T: std::str::FromStr>(key: &str, default: T) -> T {
std::env::var(key)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}
Self {
ops: env("BSTACK_FUZZ_OPS", 10_000),
sessions: env("BSTACK_FUZZ_SESSIONS", 20),
ops_per_session: env("BSTACK_FUZZ_OPS_PER", 100),
max_alloc: env("BSTACK_FUZZ_MAX_ALLOC", 1024),
reopen_every: env("BSTACK_FUZZ_REOPEN_EVERY", 200),
adversarial_pct: env("BSTACK_FUZZ_ADVERSARIAL_PCT", 25),
}
}
}
macro_rules! make_allocator {
($ty:ty, $size:expr) => {
|bs: $crate::BStack| {
if bs.is_empty().unwrap() {
<$ty>::new(bs, $size)
} else {
<$ty>::open(bs)
}
}
};
($ty:ty) => {
<$ty>::new
};
}
pub(crate) use make_allocator;
#[cfg(all(debug_assertions, feature = "fault-injection"))]
pub(crate) mod policies {
use crate::fault::FaultPolicy;
use std::io;
use std::sync::atomic::{AtomicU64, Ordering};
pub(crate) struct FailOpAt {
op: &'static str,
at: u64,
kind: io::ErrorKind,
seen: AtomicU64,
}
impl FailOpAt {
pub(crate) fn new(op: &'static str, at: u64, kind: io::ErrorKind) -> Self {
Self {
op,
at,
kind,
seen: AtomicU64::new(0),
}
}
}
impl FaultPolicy for FailOpAt {
fn next_fault(&self, op: &'static str, _seq: u64) -> Option<io::Error> {
if op != self.op {
return None;
}
let n = self.seen.fetch_add(1, Ordering::SeqCst);
(n == self.at).then(|| io::Error::new(self.kind, format!("injected fault at {op}#{n}")))
}
}
}