Skip to main content

PoisonOnFree

Struct PoisonOnFree 

Source
pub struct PoisonOnFree<I> { /* private fields */ }
Expand description

Wrapper that overwrites freed memory with a poison pattern.

Send if I: Send. Sync if I: Sync. No additional synchronization hazards beyond the inner allocator’s.

§Poison coverage caveats

PoisonOnFree writes the pattern across the entire usable extent — [ptr, ptr + usable_size), falling back to [ptr, ptr + size) when the inner reports no usable_sizebefore forwarding to self.inner.deallocate(ptr, layout), so a secret written into the [size, usable_size) slack is covered too. What happens after that handoff is the inner allocator’s business — and some inner allocators reuse the very bytes we just poisoned for their own freelist bookkeeping:

  • Slab<T, _, M> / SizeClassed<_, _>: on deallocate, the slab writes a FreeLink ({ next_idx: u32, mac: u32 } for Slab, 4 bytes for SizeClassed’s UntypedSlab) at the start of the freed slot. That overwrites the first 4–8 bytes of our poison; effective coverage on a slab-backed PoisonOnFree is [ptr + size_of::<FreeLink>(), ptr + size).
  • BumpArena / SharedBumpArena / StackAlloc: deallocate is a no-op (or pure cursor-pop), so poison persists in full.
  • System / MmapBacked: the OS / global allocator may zero the region for security or reuse it for its own metadata; poison may survive on MmapBacked until the OS reclaims, never on System.

The security claim “freed-data disclosure prevented via UAF read” is therefore partial whenever the inner allocator writes back into the just-freed region: the bytes overlapping the inner’s freelist link hold link data rather than poison.

Composition that maximizes poison persistence: PoisonOnFree<Quarantine<Slab>> — poison is written immediately on outer-most dealloc, and Quarantine’s epoch delay keeps the slot off Slab’s freelist for several deallocate calls. During that window a UAF read sees fully-poisoned bytes; once Quarantine evicts to Slab, the first 4–8 bytes are then overwritten with the freelist link as above.

Avoid Quarantine<PoisonOnFree<Slab>> — that composition delays the poison write until eviction, so a UAF read during the quarantine window sees the original (un-poisoned) data. The wrapping order matters for the security property, not just for the layout.

§Composition with Canary

Canary zeros its own pre- and post-canary words on deallocate, so the canary seed itself is wiped regardless of composition order. Coverage of the adjacent bytes still depends on order:

  • Canary<PoisonOnFree<Inner>> (Canary outer, PoisonOnFree inner): on deallocate, Canary verifies+zeros its canary words first, then forwards to PoisonOnFree which poisons the entire inner region — including pre-padding, the user region, and the slot bytes that held the canary words (now overwritten with the poison pattern). Maximum coverage.
  • PoisonOnFree<Canary<Inner>> (PoisonOnFree outer, Canary inner): PoisonOnFree poisons only [user_ptr, user_ptr+size)not the canary words at user_ptr-8 / user_ptr+size, and not the pre-padding before user_ptr-8. Canary then zeros the canary words and forwards. Coverage of the user region is the same; coverage of the padding/canary slots is empty (zeroed canaries, untouched padding). Pick this only if the inner allocator’s freelist link sits in the user region (e.g. Slab<T, _>) — there PoisonOnFree-first writes the poison before the slab overwrites the first 4-8 bytes with the freelist link, so post-link coverage matches the outer composition.

§grow / shrink

PoisonOnFree does not forward grow/shrink to the inner allocator; it uses the Allocator trait defaults, which allocate-copy-then-self.deallocate(old). Routing the old allocation through this wrapper’s poisoning deallocate guarantees the moved-from block (and shrink’s discarded tail) is poisoned. Forwarding to the inner’s native grow/shrink would let a relocating resize free the old block through the inner’s deallocate, leaving the original secret bytes intact and un-poisoned — the gap this choice closes, matching ZeroizeOnFree. The cost is that an inner allocator’s native in-place resize is not used.

Implementations§

Source§

impl<I> PoisonOnFree<I>

Source

pub const fn new(inner: I) -> Self

Wrap with the default poison byte (0xDE).

Source

pub const fn with_pattern(inner: I, pattern: u8) -> Self

Wrap with an explicit poison byte. Use 0x00 for zero-fill if integrating with code that reads zero-filled freed memory by convention; otherwise 0xDE-style sentinels are easier to debug.

Source

pub fn inner(&self) -> &I

Borrow the inner allocator.

Source

pub const fn pattern(&self) -> u8

The poison byte this wrapper writes.

Trait Implementations§

Source§

impl<I: Allocator> Allocator for PoisonOnFree<I>

Source§

fn reset(&mut self) -> Result<(), AllocError>

Bulk-reclaim the inner allocator (arenas only). Forwards the inner’s cursor reclaim; it does not poison the previously-issued bytes — those are overwritten on the per-block deallocate path or by a later allocate. Without this forward a wrapped BumpArena could not be reset at all (the trait default returns Err).

Source§

fn allocate(&self, layout: NonZeroLayout) -> Result<NonNull<[u8]>, AllocError>

Allocate a block satisfying layout. The returned slice’s length is at least layout.size() but may be larger.
Source§

fn allocate_zeroed( &self, layout: NonZeroLayout, ) -> Result<NonNull<[u8]>, AllocError>

Allocate a zero-initialized block.
Source§

unsafe fn usable_size( &self, ptr: NonNull<u8>, layout: NonZeroLayout, ) -> Option<usize>

Usable size of an existing allocation, if the allocator can report it. Defaults to None — implementors that track usable size override. Read more
Source§

fn capacity_bytes(&self) -> Option<usize>

Total bytes this allocator can issue, if bounded. None for unbounded allocators like System. Used by Watermark to compute thresholds.
Source§

fn corruption_events(&self) -> u64

Detected freelist / metadata corruption events observed by this allocator since construction. Read more
Source§

unsafe fn grow( &self, ptr: NonNull<u8>, old: NonZeroLayout, new: NonZeroLayout, ) -> Result<NonNull<[u8]>, AllocError>

Grow an allocation in place if possible, otherwise allocate-copy-free. Read more
Source§

unsafe fn shrink( &self, ptr: NonNull<u8>, old: NonZeroLayout, new: NonZeroLayout, ) -> Result<NonNull<[u8]>, AllocError>

Shrink an allocation in place if possible, otherwise allocate-copy-free. Read more
Source§

impl<I: Allocator> Deallocator for PoisonOnFree<I>

Source§

unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: NonZeroLayout)

Release a previously allocated block. Read more
Source§

impl<I: FixedRange> FixedRange for PoisonOnFree<I>

FixedRange passthrough so this wrapper composes over a lazy_commit MmapBacked and similar backings.

Footgun: the poison-on-free scrub runs only in this wrapper’s deallocate. If you nest it as a backing under an arena — BumpArena<PoisonOnFree<..>> — the arena carves directly from base()/size() and its own deallocate is a no-op, so the scrub never runs. Keep the hardening wrapper outermost (wrapping the allocator), never as the FixedRange an arena consumes.

Source§

fn commit(&self, offset: usize, len: usize) -> Result<(), AllocError>

Pass-through forward so a commit-aware consumer reaches the inner backing when this wrapper sits over a lazy_commit MmapBacked.

Source§

fn base(&self) -> NonNull<u8>

First byte of the owned address range. Read more
Source§

fn size(&self) -> usize

Length in bytes of the owned address range. Read more
Source§

fn contains(&self, ptr: NonNull<u8>) -> bool

Whether ptr lies within [base, base + size). Read more

Auto Trait Implementations§

§

impl<I> Freeze for PoisonOnFree<I>
where I: Freeze,

§

impl<I> RefUnwindSafe for PoisonOnFree<I>
where I: RefUnwindSafe,

§

impl<I> Send for PoisonOnFree<I>
where I: Send,

§

impl<I> Sync for PoisonOnFree<I>
where I: Sync,

§

impl<I> Unpin for PoisonOnFree<I>
where I: Unpin,

§

impl<I> UnsafeUnpin for PoisonOnFree<I>
where I: UnsafeUnpin,

§

impl<I> UnwindSafe for PoisonOnFree<I>
where I: UnwindSafe,

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.