#[cfg(feature = "bump-alloc")]
use bumpalo::Bump;
use alloc::alloc::{Layout, alloc_zeroed, dealloc};
use core::mem::ManuallyDrop;
use core::ops::{Deref, DerefMut};
use core::ptr::NonNull;
use core::sync::atomic::{Ordering, compiler_fence};
use crate::block::Block;
use crate::error::Error;
pub const ARENA_ALIGN: usize = 64;
const WIPE_ENABLED: bool = cfg!(feature = "zeroize-memory");
#[cfg_attr(not(feature = "zeroize-memory"), allow(dead_code))]
const WIPE_THREAD_THRESHOLD: usize = 64 * 1024 * 1024;
const HAVE_ASM_BARRIER: bool = cfg!(all(not(miri), any(
target_arch = "x86",
target_arch = "x86_64",
target_arch = "arm",
target_arch = "aarch64",
target_arch = "riscv32",
target_arch = "riscv64",
target_arch = "loongarch64",
target_arch = "s390x",
)));
#[inline]
pub unsafe fn secure_wipe_raw(ptr: *mut u8, len: usize) {
if len == 0 {
return;
}
if HAVE_ASM_BARRIER {
unsafe { core::ptr::write_bytes(ptr, 0, len) };
#[cfg(any(
target_arch = "x86",
target_arch = "x86_64",
target_arch = "arm",
target_arch = "aarch64",
target_arch = "riscv32",
target_arch = "riscv64",
target_arch = "loongarch64",
target_arch = "s390x",
))]
unsafe {
core::arch::asm!("/* {0} */", in(reg) ptr, options(nostack, preserves_flags));
}
} else {
unsafe {
for i in 0..len {
core::ptr::write_volatile(ptr.add(i), 0u8);
}
}
}
compiler_fence(Ordering::SeqCst);
}
pub fn secure_wipe(bytes: &mut [u8]) {
unsafe { secure_wipe_raw(bytes.as_mut_ptr(), bytes.len()) };
}
pub fn secure_wipe_u64(words: &mut [u64]) {
unsafe { secure_wipe_raw(words.as_mut_ptr().cast::<u8>(), size_of_val(words)) };
}
pub fn secure_wipe_blocks(blocks: &mut [Block]) {
unsafe { secure_wipe_raw(blocks.as_mut_ptr().cast::<u8>(), size_of_val(blocks)) };
}
#[inline]
pub fn clear_internal_memory(bytes: &mut [u8]) {
#[cfg(feature = "zeroize-memory")]
secure_wipe(bytes);
#[cfg(not(feature = "zeroize-memory"))]
let _ = bytes;
}
#[inline]
pub fn clear_internal_memory_u64(words: &mut [u64]) {
#[cfg(feature = "zeroize-memory")]
secure_wipe_u64(words);
#[cfg(not(feature = "zeroize-memory"))]
let _ = words;
}
#[inline]
pub fn clear_internal_memory_blocks(blocks: &mut [Block]) {
#[cfg(feature = "zeroize-memory")]
secure_wipe_blocks(blocks);
#[cfg(not(feature = "zeroize-memory"))]
let _ = blocks;
}
#[cfg(all(feature = "std", target_os = "linux"))]
mod os {
use core::ffi::c_void;
unsafe extern "C" {
fn mmap(
addr: *mut c_void,
len: usize,
prot: i32,
flags: i32,
fd: i32,
offset: i64,
) -> *mut c_void;
fn munmap(addr: *mut c_void, len: usize) -> i32;
fn madvise(addr: *mut c_void, len: usize, advice: i32) -> i32;
fn sysconf(name: i32) -> isize;
}
const PROT_READ: i32 = 1;
const PROT_WRITE: i32 = 2;
const MAP_PRIVATE: i32 = 0x0002;
const MAP_ANONYMOUS: i32 = 0x0020;
const MAP_FAILED: isize = -1;
const SC_PAGESIZE: i32 = 30;
const MADV_HUGEPAGE: i32 = 14;
pub const HUGE_PAGE: usize = 2 * 1024 * 1024;
pub const MMAP_THRESHOLD: usize = HUGE_PAGE;
pub fn page_size() -> usize {
let n = unsafe { sysconf(SC_PAGESIZE) };
if n <= 0 { 4096 } else { n as usize }
}
pub fn map_aligned(bytes: usize, align: usize) -> Option<(*mut u8, usize)> {
let page = page_size();
let len = bytes.checked_next_multiple_of(page)?;
let over = len.checked_add(align)?;
let raw = unsafe {
mmap(
core::ptr::null_mut(),
over,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS,
-1,
0,
)
};
if raw as isize == MAP_FAILED || raw.is_null() {
return None;
}
let raw = raw.cast::<u8>();
let head = (raw as usize).next_multiple_of(align) - raw as usize;
let tail = align - head;
unsafe {
if head != 0 {
munmap(raw.cast(), head);
}
let base = raw.add(head);
if tail != 0 {
munmap(base.add(len).cast(), tail);
}
Some((base, len))
}
}
pub unsafe fn advise_huge(base: *mut u8, len: usize) -> bool {
unsafe { madvise(base.cast(), len, MADV_HUGEPAGE) == 0 }
}
pub unsafe fn unmap(base: *mut u8, len: usize) {
unsafe { munmap(base.cast(), len) };
}
}
#[cfg(feature = "parallel")]
#[derive(Clone, Copy)]
struct StripeBase(*mut u8);
#[cfg(feature = "parallel")]
impl StripeBase {
#[inline]
unsafe fn at(self, off: usize) -> *mut u8 {
unsafe { self.0.add(off) }
}
}
#[cfg(feature = "parallel")]
unsafe impl Send for StripeBase {}
#[cfg(feature = "parallel")]
unsafe impl Sync for StripeBase {}
#[cfg(feature = "parallel")]
#[cfg_attr(not(feature = "zeroize-memory"), allow(dead_code))]
fn stripe_over<F>(base: *mut u8, len: usize, workers: u32, unit: usize, f: F)
where
F: Fn(*mut u8, usize) + Sync,
{
use core::sync::atomic::AtomicUsize;
let workers = workers.max(1) as usize;
let per = len.div_ceil(workers).next_multiple_of(unit.max(1));
if workers == 1 || per >= len {
f(base, len);
return;
}
let stripes = len.div_ceil(per);
let base = StripeBase(base);
let next = AtomicUsize::new(0);
let next = &next;
let f = &f;
let run = move || {
loop {
let i = next.fetch_add(1, Ordering::Relaxed);
if i >= stripes {
return;
}
let off = i * per;
f(unsafe { base.at(off) }, per.min(len - off));
}
};
std::thread::scope(|scope| {
for _ in 1..workers {
let _ = std::thread::Builder::new().spawn_scoped(scope, run);
}
run();
});
}
#[cfg(not(feature = "parallel"))]
#[cfg_attr(not(feature = "zeroize-memory"), allow(dead_code))]
#[inline]
fn stripe_over<F>(base: *mut u8, len: usize, _workers: u32, _unit: usize, f: F)
where
F: Fn(*mut u8, usize),
{
f(base, len);
}
enum Backing {
Heap,
#[cfg(all(feature = "std", target_os = "linux"))]
Mapped {
base: *mut u8,
len: usize,
huge: bool,
},
}
pub struct Arena {
ptr: NonNull<Block>,
blocks: usize,
capacity: usize,
zeroed: bool,
workers: u32,
backing: Backing,
}
unsafe impl Send for Arena {}
impl Arena {
fn checked_layout(blocks: usize) -> Result<(usize, Layout), Error> {
if blocks == 0 {
return Err(Error::MemoryAllocationError);
}
let bytes = blocks
.checked_mul(size_of::<Block>())
.ok_or(Error::MemoryAllocationError)?;
let layout = Layout::from_size_align(bytes, ARENA_ALIGN)
.map_err(|_| Error::MemoryAllocationError)?;
Ok((bytes, layout))
}
pub fn new(blocks: usize) -> Result<Arena, Error> {
let (_bytes, layout) = Arena::checked_layout(blocks)?;
#[cfg(all(feature = "std", target_os = "linux"))]
if _bytes >= os::MMAP_THRESHOLD
&& let Some((base, len)) = os::map_aligned(_bytes, os::HUGE_PAGE)
{
let huge = unsafe { os::advise_huge(base, len) };
debug_assert_eq!(base as usize % ARENA_ALIGN, 0, "page-aligned implies 64");
return Ok(Arena {
ptr: unsafe { NonNull::new_unchecked(base.cast::<Block>()) },
blocks,
capacity: blocks,
zeroed: true,
workers: 1,
backing: Backing::Mapped { base, len, huge },
});
}
let raw = unsafe { alloc_zeroed(layout) };
let ptr = NonNull::new(raw.cast::<Block>()).ok_or(Error::MemoryAllocationError)?;
Ok(Arena {
ptr,
blocks,
capacity: blocks,
zeroed: true,
workers: 1,
backing: Backing::Heap,
})
}
#[inline]
pub fn set_workers(&mut self, workers: u32) {
self.workers = workers.max(1);
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.blocks
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.blocks == 0
}
#[inline]
#[must_use]
pub fn capacity(&self) -> usize {
self.capacity
}
#[inline]
#[must_use]
pub fn is_known_zeroed(&self) -> bool {
self.zeroed
}
#[inline]
#[must_use]
pub fn as_ptr(&self) -> *const Block {
self.ptr.as_ptr()
}
#[inline]
#[must_use]
pub fn as_mut_ptr(&mut self) -> *mut Block {
self.zeroed = false;
self.ptr.as_ptr()
}
#[inline]
#[must_use]
pub fn as_slice(&self) -> &[Block] {
unsafe { core::slice::from_raw_parts(self.ptr.as_ptr(), self.blocks) }
}
#[inline]
#[must_use]
pub fn as_mut_slice(&mut self) -> &mut [Block] {
self.zeroed = false;
unsafe { core::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.blocks) }
}
pub fn ensure_zeroed(&mut self) {
if self.zeroed {
return;
}
unsafe { core::ptr::write_bytes(self.ptr.as_ptr(), 0, self.capacity) };
self.zeroed = true;
}
fn wipe_visible(&mut self) {
#[cfg(feature = "zeroize-memory")]
{
let bytes = self.blocks * size_of::<Block>();
let workers = if bytes >= WIPE_THREAD_THRESHOLD {
self.workers
} else {
1
};
stripe_over(
self.ptr.as_ptr().cast::<u8>(),
bytes,
workers,
ARENA_ALIGN,
|chunk, chunk_len| unsafe { secure_wipe_raw(chunk, chunk_len) },
);
}
self.zeroed |= WIPE_ENABLED;
}
fn retarget(&mut self, blocks: usize) -> bool {
if blocks > self.capacity {
return false;
}
self.blocks = blocks;
true
}
#[inline]
#[must_use]
pub fn backing_name(&self) -> &'static str {
match self.backing {
Backing::Heap => "heap",
#[cfg(all(feature = "std", target_os = "linux"))]
Backing::Mapped { huge: true, .. } => "mapped+huge",
#[cfg(all(feature = "std", target_os = "linux"))]
Backing::Mapped { huge: false, .. } => "mapped",
}
}
#[inline]
fn layout(&self) -> Layout {
match Layout::from_size_align(self.capacity * size_of::<Block>(), ARENA_ALIGN) {
Ok(layout) => layout,
Err(_) => Layout::new::<Block>(),
}
}
}
impl Drop for Arena {
fn drop(&mut self) {
if !self.zeroed {
#[cfg(feature = "parallel")]
if std::thread::panicking() {
self.workers = 1;
}
self.wipe_visible();
}
#[cfg(all(feature = "internal-api", feature = "std"))]
audit::note_release(self.ptr.as_ptr().cast::<u8>(), self.capacity);
match self.backing {
Backing::Heap => {
let layout = self.layout();
unsafe { dealloc(self.ptr.as_ptr().cast::<u8>(), layout) };
}
#[cfg(all(feature = "std", target_os = "linux"))]
Backing::Mapped { base, len, .. } => {
unsafe { os::unmap(base, len) };
}
}
}
}
#[cfg(all(feature = "internal-api", feature = "std"))]
pub mod audit {
use core::cell::Cell;
use crate::block::Block;
std::thread_local! {
static WATCH: Cell<usize> = const { Cell::new(0) };
static RELEASED: Cell<usize> = const { Cell::new(0) };
static RELEASED_DIRTY: Cell<usize> = const { Cell::new(0) };
}
pub fn watch(blocks: usize) {
WATCH.with(|c| c.set(blocks));
RELEASED.with(|c| c.set(0));
RELEASED_DIRTY.with(|c| c.set(0));
}
#[must_use]
pub fn released() -> usize {
RELEASED.with(Cell::get)
}
#[must_use]
pub fn released_dirty() -> usize {
RELEASED_DIRTY.with(Cell::get)
}
#[must_use]
pub fn is_dirty(arena: &super::Arena) -> bool {
arena.as_slice().iter().any(|b| *b != Block::ZERO)
}
pub(crate) fn note_release(ptr: *mut u8, blocks: usize) {
if WATCH.try_with(Cell::get) != Ok(blocks) || blocks == 0 {
return;
}
RELEASED.with(|c| c.set(c.get() + 1));
let bytes = unsafe { core::slice::from_raw_parts(ptr, blocks * size_of::<Block>()) };
if bytes.iter().any(|b| *b != 0) {
RELEASED_DIRTY.with(|c| c.set(c.get() + 1));
}
}
}
impl core::fmt::Debug for Arena {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Arena")
.field("ptr", &self.ptr.as_ptr())
.field("blocks", &self.blocks)
.field("capacity", &self.capacity)
.field("zeroed", &self.zeroed)
.field("backing", &self.backing_name())
.finish()
}
}
pub struct Workspace {
arena: Option<Arena>,
#[cfg(feature = "bump-alloc")]
bump: Bump,
}
impl Workspace {
#[must_use]
pub fn new() -> Workspace {
Workspace {
arena: None,
#[cfg(feature = "bump-alloc")]
bump: Bump::new(),
}
}
pub fn with_capacity(blocks: usize) -> Result<Workspace, Error> {
let mut workspace = Workspace::new();
workspace.reserve(blocks)?;
Ok(workspace)
}
#[inline]
#[must_use]
pub fn capacity(&self) -> usize {
self.arena.as_ref().map_or(0, Arena::capacity)
}
pub fn reserve(&mut self, blocks: usize) -> Result<(), Error> {
if blocks == 0 || self.capacity() >= blocks {
return Ok(());
}
Arena::checked_layout(blocks)?;
self.arena = None;
self.arena = Some(Arena::new(blocks)?);
Ok(())
}
pub fn acquire(&mut self, blocks: usize) -> Result<ArenaGuard<'_>, Error> {
let arena = self.acquire_owned(blocks)?;
Ok(ArenaGuard {
arena: ManuallyDrop::new(arena),
workspace: self,
})
}
pub fn acquire_owned(&mut self, blocks: usize) -> Result<Arena, Error> {
if blocks == 0 {
return Err(Error::MemoryAllocationError);
}
if self.capacity() < blocks {
Arena::checked_layout(blocks)?;
}
match self.arena.take() {
Some(mut arena) if arena.capacity() >= blocks => {
let fits = arena.retarget(blocks);
debug_assert!(fits, "capacity was just checked");
Ok(arena)
}
Some(arena) => {
drop(arena);
Arena::new(blocks)
}
None => Arena::new(blocks),
}
}
pub fn release(&mut self, mut arena: Arena) {
arena.wipe_visible();
let keep_parked = self
.arena
.as_ref()
.is_some_and(|parked| parked.capacity() >= arena.capacity());
if !keep_parked {
self.arena = Some(arena);
}
}
pub fn clear(&mut self) {
self.arena = None;
#[cfg(feature = "bump-alloc")]
{
self.reset_bump();
self.bump = Bump::new();
}
}
#[cfg(feature = "bump-alloc")]
#[inline]
#[must_use]
pub fn bump(&self) -> &Bump {
&self.bump
}
#[cfg(feature = "bump-alloc")]
#[inline]
#[must_use]
pub fn bump_reserved_bytes(&self) -> usize {
self.bump.allocated_bytes()
}
#[cfg(feature = "bump-alloc")]
pub fn reset_bump(&mut self) {
#[cfg(feature = "zeroize-memory")]
{
unsafe {
for (chunk, len) in self.bump.iter_allocated_chunks_raw() {
secure_wipe_raw(chunk, len);
}
}
}
self.bump.reset();
}
}
impl Default for Workspace {
fn default() -> Workspace {
Workspace::new()
}
}
impl core::fmt::Debug for Workspace {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let mut out = f.debug_struct("Workspace");
out.field("arena", &self.arena);
#[cfg(feature = "bump-alloc")]
out.field("bump_reserved_bytes", &self.bump.allocated_bytes());
out.finish()
}
}
pub struct ArenaGuard<'w> {
arena: ManuallyDrop<Arena>,
workspace: &'w mut Workspace,
}
impl Deref for ArenaGuard<'_> {
type Target = Arena;
#[inline]
fn deref(&self) -> &Arena {
&self.arena
}
}
impl DerefMut for ArenaGuard<'_> {
#[inline]
fn deref_mut(&mut self) -> &mut Arena {
&mut self.arena
}
}
impl Drop for ArenaGuard<'_> {
fn drop(&mut self) {
let arena = unsafe { ManuallyDrop::take(&mut self.arena) };
self.workspace.release(arena);
}
}
impl core::fmt::Debug for ArenaGuard<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ArenaGuard")
.field("arena", &*self.arena)
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(all(feature = "bump-alloc", feature = "zeroize-memory"))]
use alloc::vec::Vec;
#[cfg(feature = "zeroize-memory")]
fn whole_capacity(arena: &Arena) -> &[Block] {
unsafe { core::slice::from_raw_parts(arena.as_ptr(), arena.capacity()) }
}
fn is_all_zero(blocks: &[Block]) -> bool {
blocks.iter().all(|b| *b == Block::ZERO)
}
#[test]
fn arena_is_zeroed_and_aligned() {
let arena = Arena::new(16).expect("16 blocks");
assert_eq!(arena.len(), 16);
assert_eq!(arena.capacity(), 16);
assert_eq!(arena.as_ptr() as usize % ARENA_ALIGN, 0);
assert!(arena.as_slice().iter().all(|b| *b == Block::ZERO));
}
#[test]
fn arena_rejects_zero_and_overflow() {
assert!(matches!(
Arena::new(0).err(),
Some(Error::MemoryAllocationError)
));
assert!(matches!(
Arena::new(usize::MAX / 512).err(),
Some(Error::MemoryAllocationError)
));
}
#[test]
fn arena_is_writable_through_the_slice() {
let mut arena = Arena::new(4).expect("4 blocks");
arena.as_mut_slice()[3].fill(0xCD);
assert_eq!(arena.as_slice()[3].0[0], u64::from_ne_bytes([0xCD; 8]));
assert_eq!(arena.as_slice()[2], Block::ZERO);
}
#[test]
fn wipe_zeroes_everything() {
let mut bytes = [0xAAu8; 64];
secure_wipe(&mut bytes);
assert_eq!(bytes, [0u8; 64]);
let mut words = [0xDEAD_BEEFu64; 8];
secure_wipe_u64(&mut words);
assert_eq!(words, [0u64; 8]);
let mut blocks = [Block::ZERO; 2];
blocks[1].fill(0xFF);
secure_wipe_blocks(&mut blocks);
assert_eq!(blocks[1], Block::ZERO);
}
#[test]
fn secure_wipe_raw_covers_exactly_the_requested_region() {
const N: usize = 64;
for start in 0..16usize {
for len in 0..=(N - 16) {
let mut buf = [0xA5u8; N];
unsafe { secure_wipe_raw(buf.as_mut_ptr().add(start), len) };
for (i, byte) in buf.iter().enumerate() {
let inside = i >= start && i < start + len;
assert_eq!(
*byte == 0,
inside,
"byte {i} wrong for start={start} len={len}: {byte:#04x}"
);
}
}
}
}
#[test]
fn a_mapping_sized_arena_is_zero_aligned_and_writable() {
const N: usize = 4096;
let mut arena = Arena::new(N).expect("4 MiB arena");
assert_eq!(arena.len(), N);
assert_eq!(arena.as_ptr() as usize % ARENA_ALIGN, 0);
assert!(
is_all_zero(arena.as_slice()),
"a fresh arena must be zero on every backing — MAP_ANONYMOUS is \
what makes that true without a single store"
);
arena.as_mut_slice()[0].fill(0x11);
arena.as_mut_slice()[N - 1].fill(0x22);
assert_eq!(arena.as_slice()[0].0[0], u64::from_ne_bytes([0x11; 8]));
assert_eq!(arena.as_slice()[N - 1].0[127], u64::from_ne_bytes([0x22; 8]));
assert_eq!(arena.as_slice()[N / 2], Block::ZERO);
}
#[test]
fn the_backing_matches_the_platform_and_the_size() {
let small = Arena::new(8).expect("8 KiB arena");
assert_eq!(small.backing_name(), "heap", "8 KiB is below the threshold");
let large = Arena::new(4096).expect("4 MiB arena");
if cfg!(all(feature = "std", target_os = "linux")) {
assert!(
large.backing_name().starts_with("mapped"),
"4 MiB should be a mapping on Linux, got {}",
large.backing_name()
);
} else {
assert_eq!(large.backing_name(), "heap");
}
}
#[test]
fn striping_partitions_the_region_exactly() {
use alloc::vec;
for unit in [1usize, 8, 64] {
for len in [1usize, 7, 64, 65, 255, 1024] {
for workers in 1..=5u32 {
let mut buf = vec![0xA5u8; len + 32];
let base = unsafe { buf.as_mut_ptr().add(16) };
stripe_over(base, len, workers, unit, |chunk, chunk_len| {
unsafe { core::ptr::write_bytes(chunk, 0, chunk_len) };
});
for (i, byte) in buf.iter().enumerate() {
let inside = (16..16 + len).contains(&i);
assert_eq!(
*byte == 0,
inside,
"byte {i} wrong for unit={unit} len={len} workers={workers}"
);
}
}
}
}
}
#[test]
#[cfg(feature = "zeroize-memory")]
fn the_release_wipe_clears_everything_on_every_worker_count() {
for workers in [1u32, 2, 4, 8] {
let mut arena = Arena::new(4096).expect("4 MiB arena");
arena.set_workers(workers);
for block in arena.as_mut_slice() {
block.fill(0xC3);
}
assert!(!arena.is_known_zeroed());
arena.wipe_visible();
assert!(
is_all_zero(arena.as_slice()),
"workers={workers} left part of the arena unwiped"
);
assert!(arena.is_known_zeroed());
}
}
#[test]
fn every_mutable_accessor_marks_the_arena_dirty() {
let mut arena = Arena::new(2).expect("2 blocks");
assert!(arena.is_known_zeroed(), "alloc_zeroed establishes it");
let _ = arena.as_slice();
let _ = arena.as_ptr();
assert!(arena.is_known_zeroed(), "shared access cannot write");
let _ = arena.as_mut_slice();
assert!(!arena.is_known_zeroed());
arena.ensure_zeroed();
assert!(arena.is_known_zeroed());
let _ = arena.as_mut_ptr();
assert!(!arena.is_known_zeroed(), "a raw *mut is a write capability");
}
#[test]
fn ensure_zeroed_clears_actual_bytes() {
let mut arena = Arena::new(3).expect("3 blocks");
for block in arena.as_mut_slice() {
block.fill(0xA5);
}
assert!(!is_all_zero(arena.as_slice()));
arena.ensure_zeroed();
assert!(is_all_zero(arena.as_slice()));
assert!(arena.is_known_zeroed());
}
#[test]
fn empty_workspace_allocates_nothing() {
let ws = Workspace::new();
assert_eq!(ws.capacity(), 0);
assert_eq!(Workspace::default().capacity(), 0);
}
#[test]
fn reuse_touches_the_allocator_exactly_once() {
let mut ws = Workspace::with_capacity(32).expect("32 blocks");
assert_eq!(ws.capacity(), 32);
let first = {
let guard = ws.acquire(32).expect("acquire");
assert_eq!(guard.len(), 32);
guard.as_ptr()
};
for _ in 0..10 {
let guard = ws.acquire(32).expect("reacquire");
assert_eq!(guard.as_ptr(), first, "reuse must not reallocate");
}
assert_eq!(ws.capacity(), 32, "capacity survives the round trips");
}
#[test]
fn reuse_after_a_smaller_request_keeps_the_big_allocation() {
let mut ws = Workspace::with_capacity(64).expect("64 blocks");
let big = ws.acquire(64).expect("acquire 64").as_ptr();
{
let guard = ws.acquire(8).expect("acquire 8");
assert_eq!(guard.len(), 8, "visible window shrinks");
assert_eq!(guard.capacity(), 64, "allocation does not");
assert_eq!(guard.as_ptr(), big, "and it is the same allocation");
}
let guard = ws.acquire(64).expect("acquire 64 again");
assert_eq!(guard.len(), 64);
assert_eq!(guard.as_ptr(), big);
}
#[test]
fn reuse_after_a_larger_request_grows_and_still_reuses() {
let mut ws = Workspace::with_capacity(4).expect("4 blocks");
{
let guard = ws.acquire(4).expect("acquire 4");
assert_eq!(guard.capacity(), 4);
}
let grown = {
let guard = ws.acquire(48).expect("grow to 48");
assert_eq!(guard.len(), 48);
assert!(guard.capacity() >= 48);
guard.as_ptr()
};
assert!(ws.capacity() >= 48);
for _ in 0..4 {
let guard = ws.acquire(48).expect("reacquire 48");
assert_eq!(guard.as_ptr(), grown, "growth happens once");
}
}
#[test]
fn release_keeps_the_larger_of_two_arenas() {
let mut ws = Workspace::with_capacity(4).expect("4 blocks");
let small = ws.acquire_owned(4).expect("owned 4");
let large = Arena::new(64).expect("64 blocks");
ws.release(large);
assert_eq!(ws.capacity(), 64);
ws.release(small);
assert_eq!(ws.capacity(), 64, "a smaller arena must not evict a larger");
}
#[test]
fn acquire_rejects_zero_blocks() {
let mut ws = Workspace::new();
assert!(matches!(
ws.acquire(0).err(),
Some(Error::MemoryAllocationError)
));
assert!(matches!(
ws.acquire_owned(0).err(),
Some(Error::MemoryAllocationError)
));
}
#[test]
fn reserve_is_idempotent_and_never_shrinks() {
let mut ws = Workspace::new();
ws.reserve(0).expect("reserving nothing is a no-op");
assert_eq!(ws.capacity(), 0);
ws.reserve(32).expect("reserve 32");
assert_eq!(ws.capacity(), 32);
ws.reserve(8).expect("reserve 8");
assert_eq!(ws.capacity(), 32, "reserve never shrinks");
ws.reserve(32).expect("reserve 32 again");
assert_eq!(ws.capacity(), 32);
}
#[test]
fn clear_drops_the_parked_arena() {
let mut ws = Workspace::with_capacity(16).expect("16 blocks");
assert_eq!(ws.capacity(), 16);
ws.clear();
assert_eq!(ws.capacity(), 0);
assert_eq!(ws.acquire(2).expect("acquire after clear").len(), 2);
}
#[test]
fn owned_acquisition_round_trips_by_hand() {
let mut ws = Workspace::with_capacity(16).expect("16 blocks");
let arena = ws.acquire_owned(16).expect("owned");
let ptr = arena.as_ptr();
assert_eq!(ws.capacity(), 0, "on loan, so nothing is parked");
ws.release(arena);
assert_eq!(ws.capacity(), 16);
assert_eq!(ws.acquire(16).expect("reacquire").as_ptr(), ptr);
}
#[test]
fn dropping_an_owned_arena_instead_of_releasing_it_is_safe() {
let mut ws = Workspace::with_capacity(8).expect("8 blocks");
drop(ws.acquire_owned(8).expect("owned"));
assert_eq!(ws.capacity(), 0, "reuse forfeited, nothing else");
assert_eq!(ws.acquire(8).expect("acquire").len(), 8);
}
#[test]
#[cfg(feature = "zeroize-memory")]
fn a_reused_arena_cannot_leak_the_previous_tenants_bytes() {
let mut ws = Workspace::with_capacity(32).expect("32 blocks");
for round in 0u8..4 {
let mut guard = ws.acquire(32).expect("acquire");
assert!(
is_all_zero(guard.as_slice()),
"round {round} started dirty — release did not wipe"
);
for (i, block) in guard.as_mut_slice().iter_mut().enumerate() {
block.fill(0xC0u8.wrapping_add(round).wrapping_add(i as u8));
}
assert!(!is_all_zero(guard.as_slice()), "the pattern must land");
}
let parked = ws.arena.as_ref().expect("parked");
assert!(is_all_zero(whole_capacity(parked)));
assert!(parked.is_known_zeroed());
}
#[test]
#[cfg(feature = "zeroize-memory")]
fn release_wipes_the_whole_borrowed_window() {
let mut ws = Workspace::with_capacity(64).expect("64 blocks");
{
let mut guard = ws.acquire(64).expect("acquire 64");
for block in guard.as_mut_slice() {
block.fill(0xEE);
}
}
let parked = ws.arena.as_ref().expect("parked");
assert_eq!(parked.capacity(), 64);
assert!(is_all_zero(whole_capacity(parked)), "all 64 blocks wiped");
let guard = ws.acquire(8).expect("acquire 8");
assert!(is_all_zero(guard.as_slice()));
assert!(is_all_zero(whole_capacity(&guard)), "the tail stays zero");
}
#[test]
#[cfg(feature = "zeroize-memory")]
fn a_small_borrower_cannot_dirty_the_tail() {
let mut ws = Workspace::with_capacity(64).expect("64 blocks");
{
let mut guard = ws.acquire(4).expect("acquire 4");
for block in guard.as_mut_slice() {
block.fill(0xB7);
}
assert_eq!(guard.len(), 4);
}
let guard = ws.acquire(64).expect("acquire 64");
assert!(is_all_zero(guard.as_slice()));
}
#[test]
#[cfg(feature = "zeroize-memory")]
fn growth_does_not_carry_bytes_over() {
let mut ws = Workspace::with_capacity(8).expect("8 blocks");
{
let mut guard = ws.acquire(8).expect("acquire 8");
for block in guard.as_mut_slice() {
block.fill(0x5C);
}
}
let guard = ws.acquire(96).expect("grow to 96");
assert!(is_all_zero(guard.as_slice()), "a grown arena is zeroed");
}
#[test]
fn ensure_zeroed_holds_regardless_of_the_wipe_feature() {
let mut ws = Workspace::with_capacity(16).expect("16 blocks");
{
let mut guard = ws.acquire(16).expect("acquire");
for block in guard.as_mut_slice() {
block.fill(0x93);
}
}
let mut guard = ws.acquire(16).expect("reacquire");
guard.ensure_zeroed();
assert!(is_all_zero(guard.as_slice()));
let sum: u64 = guard.as_slice().iter().map(|b| b.0[0]).sum();
assert_eq!(sum, 0);
}
#[test]
fn known_zeroed_never_lies_about_the_capacity() {
let mut ws = Workspace::with_capacity(64).expect("64 blocks");
{
let mut guard = ws.acquire(64).expect("acquire 64");
for block in guard.as_mut_slice() {
block.fill(0x88);
}
}
{
let mut guard = ws.acquire(4).expect("acquire 4");
guard.ensure_zeroed();
assert!(guard.is_known_zeroed());
}
let guard = ws.acquire(64).expect("acquire 64 again");
if guard.is_known_zeroed() {
assert!(
is_all_zero(guard.as_slice()),
"is_known_zeroed() claimed zero while the tail was dirty"
);
}
}
#[test]
fn a_dropped_workspace_wipes_what_it_parked() {
let mut arena = Arena::new(4).expect("4 blocks");
arena.as_mut_slice()[0].fill(0x11);
assert!(!arena.is_known_zeroed(), "Drop will wipe this one");
let mut ws = Workspace::with_capacity(4).expect("4 blocks");
{
let mut guard = ws.acquire(4).expect("acquire");
guard.as_mut_slice()[0].fill(0x22);
}
assert_eq!(
ws.arena.as_ref().map(Arena::is_known_zeroed),
Some(WIPE_ENABLED),
"release wipes iff the feature is on"
);
}
#[test]
fn send_but_not_sync() {
const fn assert_send<T: Send>() {}
assert_send::<Arena>();
assert_send::<Workspace>();
assert_send::<ArenaGuard<'static>>();
}
#[test]
#[cfg(feature = "bump-alloc")]
fn bump_serves_and_recycles_small_buffers() {
let mut ws = Workspace::new();
assert_eq!(ws.bump_reserved_bytes(), 0, "Bump::new allocates nothing");
let first = {
let buf = ws
.bump()
.try_alloc_slice_fill_copy(98usize, 0u8)
.expect("98 bytes");
assert_eq!(buf.len(), 98);
assert!(buf.iter().all(|b| *b == 0));
buf.as_ptr()
};
assert!(ws.bump_reserved_bytes() >= 98);
ws.reset_bump();
assert!(
ws.bump_reserved_bytes() >= 98,
"reset keeps its chunk; only `clear` gives it back"
);
let second = ws
.bump()
.try_alloc_slice_fill_copy(98usize, 0u8)
.expect("98 bytes again")
.as_ptr();
assert_eq!(first, second, "reset must reuse the chunk");
}
#[test]
#[cfg(all(feature = "bump-alloc", feature = "zeroize-memory"))]
fn reset_bump_wipes_the_scratch() {
const LEN: usize = 64;
let mut ws = Workspace::new();
let written = {
let buf = ws
.bump()
.try_alloc_slice_fill_copy(LEN, 0xABu8)
.expect("64 bytes");
assert!(buf.iter().all(|b| *b == 0xAB), "the pattern must land");
buf.as_ptr()
};
ws.reset_bump();
let layout = Layout::from_size_align(LEN, 1).expect("valid layout");
let reclaimed = ws.bump().try_alloc_layout(layout).expect("same region");
assert_eq!(
reclaimed.as_ptr().cast_const(),
written,
"reset must recycle the chunk, or this test proves nothing"
);
let bytes: Vec<u8> =
unsafe { core::slice::from_raw_parts(reclaimed.as_ptr(), LEN) }.to_vec();
assert!(
bytes.iter().all(|b| *b == 0),
"reset_bump must wipe scratch before recycling it, found {bytes:02x?}"
);
}
#[test]
#[cfg(feature = "bump-alloc")]
fn clear_resets_both_halves() {
let mut ws = Workspace::with_capacity(8).expect("8 blocks");
let _ = ws
.bump()
.try_alloc_slice_fill_copy(32usize, 0u8)
.expect("32 bytes");
assert!(ws.bump_reserved_bytes() >= 32);
assert_eq!(ws.capacity(), 8);
ws.clear();
assert_eq!(ws.capacity(), 0, "the arena went back to the allocator");
assert_eq!(ws.bump_reserved_bytes(), 0, "and so did the bump chunks");
assert_eq!(ws.acquire(8).expect("acquire after clear").len(), 8);
assert_eq!(
ws.bump()
.try_alloc_slice_fill_copy(32usize, 0u8)
.expect("32 bytes after clear")
.len(),
32
);
}
}