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_size — before 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<_, _>: ondeallocate, the slab writes aFreeLink({ next_idx: u32, mac: u32 }forSlab, 4 bytes forSizeClassed’sUntypedSlab) at the start of the freed slot. That overwrites the first 4–8 bytes of our poison; effective coverage on a slab-backedPoisonOnFreeis[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 onMmapBackeduntil the OS reclaims, never onSystem.
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 atuser_ptr-8/user_ptr+size, and not the pre-padding beforeuser_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>
impl<I> PoisonOnFree<I>
Sourcepub const fn with_pattern(inner: I, pattern: u8) -> Self
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.
Trait Implementations§
Source§impl<I: Allocator> Allocator for PoisonOnFree<I>
impl<I: Allocator> Allocator for PoisonOnFree<I>
Source§fn reset(&mut self) -> Result<(), AllocError>
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>
fn allocate(&self, layout: NonZeroLayout) -> Result<NonNull<[u8]>, AllocError>
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>
fn allocate_zeroed( &self, layout: NonZeroLayout, ) -> Result<NonNull<[u8]>, AllocError>
Source§unsafe fn usable_size(
&self,
ptr: NonNull<u8>,
layout: NonZeroLayout,
) -> Option<usize>
unsafe fn usable_size( &self, ptr: NonNull<u8>, layout: NonZeroLayout, ) -> Option<usize>
None — implementors that track usable size
override. Read moreSource§fn capacity_bytes(&self) -> Option<usize>
fn capacity_bytes(&self) -> Option<usize>
None for unbounded
allocators like System. Used by Watermark to compute thresholds.Source§fn corruption_events(&self) -> u64
fn corruption_events(&self) -> u64
Source§unsafe fn grow(
&self,
ptr: NonNull<u8>,
old: NonZeroLayout,
new: NonZeroLayout,
) -> Result<NonNull<[u8]>, AllocError>
unsafe fn grow( &self, ptr: NonNull<u8>, old: NonZeroLayout, new: NonZeroLayout, ) -> Result<NonNull<[u8]>, AllocError>
Source§unsafe fn shrink(
&self,
ptr: NonNull<u8>,
old: NonZeroLayout,
new: NonZeroLayout,
) -> Result<NonNull<[u8]>, AllocError>
unsafe fn shrink( &self, ptr: NonNull<u8>, old: NonZeroLayout, new: NonZeroLayout, ) -> Result<NonNull<[u8]>, AllocError>
Source§impl<I: Allocator> Deallocator for PoisonOnFree<I>
impl<I: Allocator> Deallocator for PoisonOnFree<I>
Source§unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: NonZeroLayout)
unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: NonZeroLayout)
Source§impl<I: FixedRange> FixedRange for PoisonOnFree<I>
FixedRange passthrough so this wrapper composes over a lazy_commit
MmapBacked and similar backings.
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.