//! Reusable buffer pool to avoid allocations in hot paths //! //! This module provides a thread-safe pool of `BytesMut` buffers //! that can be reused across connections to reduce allocation pressure. #![allow(dead_code)] use bytes::BytesMut; use crossbeam_queue::ArrayQueue; use std::ops::{Deref, DerefMut}; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; // ============= Configuration ============= /// Default buffer size /// CHANGED: Reduced from 64KB to 16KB to match TLS record size and prevent bufferbloat. pub const DEFAULT_BUFFER_SIZE: usize = 16 * 1024; /// Default maximum number of pooled buffers pub const DEFAULT_MAX_BUFFERS: usize = 1024; // Buffers that grew beyond this multiple of `buffer_size` are dropped rather // than returned to the pool, preventing memory amplification from a single // large-payload connection permanently holding oversized allocations. const MAX_POOL_BUFFER_OVERSIZE_MULT: usize = 4; // ============= Buffer Pool ============= /// Thread-safe pool of reusable buffers pub struct BufferPool { /// Queue of available buffers buffers: ArrayQueue, /// Size of each buffer buffer_size: usize, /// Maximum number of buffers to pool max_buffers: usize, /// High-water mark of buffers ever allocated by this pool. /// Incremented on every new allocation (miss or preallocate) and never /// decremented. It does NOT represent current live buffer count. allocated: AtomicUsize, /// Number of times we had to create a new buffer misses: AtomicUsize, /// Number of successful reuses hits: AtomicUsize, } impl BufferPool { /// Create a new buffer pool with default settings pub fn new() -> Self { Self::with_config(DEFAULT_BUFFER_SIZE, DEFAULT_MAX_BUFFERS) } /// Create a buffer pool with custom configuration pub fn with_config(buffer_size: usize, max_buffers: usize) -> Self { Self { buffers: ArrayQueue::new(max_buffers), buffer_size, max_buffers, allocated: AtomicUsize::new(0), misses: AtomicUsize::new(0), hits: AtomicUsize::new(0), } } /// Get a buffer from the pool, or create a new one if empty pub fn get(self: &Arc) -> PooledBuffer { match self.buffers.pop() { Some(mut buffer) => { self.hits.fetch_add(1, Ordering::Relaxed); buffer.clear(); PooledBuffer { buffer: Some(buffer), pool: Arc::clone(self), } } None => { self.misses.fetch_add(1, Ordering::Relaxed); self.allocated.fetch_add(1, Ordering::Relaxed); PooledBuffer { buffer: Some(BytesMut::with_capacity(self.buffer_size)), pool: Arc::clone(self), } } } } /// Try to get a buffer, returns None if pool is empty pub fn try_get(self: &Arc) -> Option { self.buffers.pop().map(|mut buffer| { self.hits.fetch_add(1, Ordering::Relaxed); buffer.clear(); PooledBuffer { buffer: Some(buffer), pool: Arc::clone(self), } }) } /// Return a buffer to the pool fn return_buffer(&self, mut buffer: BytesMut) { buffer.clear(); // Accept buffers within [buffer_size, buffer_size * MAX_POOL_BUFFER_OVERSIZE_MULT]. // The lower bound prevents pool capacity from shrinking over time. // The upper bound drops buffers that grew excessively (e.g. to serve a large // payload) so they do not permanently inflate pool memory or get handed to a // future connection that only needs a small allocation. let max_acceptable = self.buffer_size.saturating_mul(MAX_POOL_BUFFER_OVERSIZE_MULT); if buffer.capacity() >= self.buffer_size && buffer.capacity() <= max_acceptable { let _ = self.buffers.push(buffer); } } /// Get pool statistics pub fn stats(&self) -> PoolStats { PoolStats { pooled: self.buffers.len(), allocated: self.allocated.load(Ordering::Relaxed), max_buffers: self.max_buffers, buffer_size: self.buffer_size, hits: self.hits.load(Ordering::Relaxed), misses: self.misses.load(Ordering::Relaxed), } } /// Get buffer size pub const fn buffer_size(&self) -> usize { self.buffer_size } /// Preallocate buffers to fill the pool pub fn preallocate(&self, count: usize) { let to_alloc = count.min(self.max_buffers); for _ in 0..to_alloc { if self.buffers.push(BytesMut::with_capacity(self.buffer_size)).is_err() { break; } self.allocated.fetch_add(1, Ordering::Relaxed); } } } impl Default for BufferPool { fn default() -> Self { Self::new() } } // ============= Pool Statistics ============= /// Statistics about buffer pool usage #[derive(Debug, Clone)] pub struct PoolStats { /// Current number of buffers sitting idle in the pool queue pub pooled: usize, /// High-water mark of buffers ever allocated by this pool. /// This is monotonically non-decreasing; it does NOT equal `pooled + in-use` /// because dropped buffers (pool full, wrong capacity) reduce live count /// without decrementing this field. pub allocated: usize, /// Maximum buffers allowed pub max_buffers: usize, /// Size of each buffer pub buffer_size: usize, /// Number of cache hits (reused buffer) pub hits: usize, /// Number of cache misses (new allocation) pub misses: usize, } impl PoolStats { /// Get hit rate as percentage pub fn hit_rate(&self) -> f64 { let total = self.hits + self.misses; if total == 0 { 0.0 } else { (self.hits as f64 / total as f64) * 100.0 } } } // ============= Pooled Buffer ============= /// A buffer that automatically returns to the pool when dropped pub struct PooledBuffer { buffer: Option, pool: Arc, } impl PooledBuffer { /// Take the inner buffer, preventing return to pool pub fn take(mut self) -> BytesMut { self.buffer.take().unwrap_or_default() } /// Get the capacity of the buffer pub fn capacity(&self) -> usize { self.buffer.as_ref().map(|b| b.capacity()).unwrap_or(0) } /// Check if buffer is empty pub fn is_empty(&self) -> bool { self.buffer.as_ref().map(|b| b.is_empty()).unwrap_or(true) } /// Get the length of data in buffer pub fn len(&self) -> usize { self.buffer.as_ref().map(|b| b.len()).unwrap_or(0) } /// Clear the buffer pub fn clear(&mut self) { if let Some(ref mut b) = self.buffer { b.clear(); } } } impl Deref for PooledBuffer { type Target = BytesMut; fn deref(&self) -> &Self::Target { self.buffer .as_ref() .expect("PooledBuffer: attempted to deref after buffer was taken") } } impl DerefMut for PooledBuffer { fn deref_mut(&mut self) -> &mut Self::Target { self.buffer .as_mut() .expect("PooledBuffer: attempted to deref_mut after buffer was taken") } } impl Drop for PooledBuffer { fn drop(&mut self) { if let Some(buffer) = self.buffer.take() { self.pool.return_buffer(buffer); } } } impl AsRef<[u8]> for PooledBuffer { fn as_ref(&self) -> &[u8] { self.buffer.as_ref().map(|b| b.as_ref()).unwrap_or(&[]) } } impl AsMut<[u8]> for PooledBuffer { fn as_mut(&mut self) -> &mut [u8] { self.buffer.as_mut().map(|b| b.as_mut()).unwrap_or(&mut []) } } // ============= Scoped Buffer ============= /// A buffer that can be used for a scoped operation /// Useful for ensuring buffer is returned even on early return pub struct ScopedBuffer<'a> { buffer: &'a mut PooledBuffer, } impl<'a> ScopedBuffer<'a> { /// Create a new scoped buffer pub fn new(buffer: &'a mut PooledBuffer) -> Self { buffer.clear(); Self { buffer } } } impl<'a> Deref for ScopedBuffer<'a> { type Target = BytesMut; fn deref(&self) -> &Self::Target { self.buffer.deref() } } impl<'a> DerefMut for ScopedBuffer<'a> { fn deref_mut(&mut self) -> &mut Self::Target { self.buffer.deref_mut() } } impl<'a> Drop for ScopedBuffer<'a> { fn drop(&mut self) { self.buffer.clear(); } } #[cfg(test)] mod tests { use super::*; #[test] fn test_pool_basic() { let pool = Arc::new(BufferPool::with_config(1024, 10)); // Get a buffer let mut buf1 = pool.get(); buf1.extend_from_slice(b"hello"); assert_eq!(&buf1[..], b"hello"); // Drop returns to pool drop(buf1); let stats = pool.stats(); assert_eq!(stats.pooled, 1); assert_eq!(stats.hits, 0); assert_eq!(stats.misses, 1); // Get again - should reuse let buf2 = pool.get(); assert!(buf2.is_empty()); // Buffer was cleared let stats = pool.stats(); assert_eq!(stats.pooled, 0); assert_eq!(stats.hits, 1); } #[test] fn test_pool_multiple_buffers() { let pool = Arc::new(BufferPool::with_config(1024, 10)); // Get multiple buffers let buf1 = pool.get(); let buf2 = pool.get(); let buf3 = pool.get(); let stats = pool.stats(); assert_eq!(stats.allocated, 3); assert_eq!(stats.pooled, 0); // Return all drop(buf1); drop(buf2); drop(buf3); let stats = pool.stats(); assert_eq!(stats.pooled, 3); } #[test] fn test_pool_overflow() { let pool = Arc::new(BufferPool::with_config(1024, 2)); // Get 3 buffers (more than max) let buf1 = pool.get(); let buf2 = pool.get(); let buf3 = pool.get(); // Return all - only 2 should be pooled drop(buf1); drop(buf2); drop(buf3); let stats = pool.stats(); assert_eq!(stats.pooled, 2); } #[test] fn test_pool_take() { let pool = Arc::new(BufferPool::with_config(1024, 10)); let mut buf = pool.get(); buf.extend_from_slice(b"data"); // Take ownership, buffer should not return to pool let taken = buf.take(); assert_eq!(&taken[..], b"data"); let stats = pool.stats(); assert_eq!(stats.pooled, 0); } #[test] fn test_pool_preallocate() { let pool = Arc::new(BufferPool::with_config(1024, 10)); pool.preallocate(5); let stats = pool.stats(); assert_eq!(stats.pooled, 5); assert_eq!(stats.allocated, 5); } #[test] fn test_pool_try_get() { let pool = Arc::new(BufferPool::with_config(1024, 10)); // Pool is empty, try_get returns None assert!(pool.try_get().is_none()); // Add a buffer to pool pool.preallocate(1); // Now try_get should succeed once while the buffer is held let buf = pool.try_get(); assert!(buf.is_some()); // While buffer is held, pool is empty assert!(pool.try_get().is_none()); // Drop buffer -> returns to pool, should be obtainable again drop(buf); assert!(pool.try_get().is_some()); } #[test] fn test_hit_rate() { let pool = Arc::new(BufferPool::with_config(1024, 10)); // First get is a miss let buf1 = pool.get(); drop(buf1); // Second get is a hit let buf2 = pool.get(); drop(buf2); // Third get is a hit let _buf3 = pool.get(); let stats = pool.stats(); assert_eq!(stats.hits, 2); assert_eq!(stats.misses, 1); assert!((stats.hit_rate() - 66.67).abs() < 1.0); } #[test] fn test_scoped_buffer() { let pool = Arc::new(BufferPool::with_config(1024, 10)); let mut buf = pool.get(); { let mut scoped = ScopedBuffer::new(&mut buf); scoped.extend_from_slice(b"scoped data"); assert_eq!(&scoped[..], b"scoped data"); } // After scoped is dropped, buffer is cleared assert!(buf.is_empty()); } #[test] fn test_concurrent_access() { use std::thread; let pool = Arc::new(BufferPool::with_config(1024, 100)); let mut handles = vec![]; for _ in 0..10 { let pool_clone = Arc::clone(&pool); handles.push(thread::spawn(move || { for _ in 0..100 { let mut buf = pool_clone.get(); buf.extend_from_slice(b"test"); // buf auto-returned on drop } })); } for handle in handles { handle.join().unwrap(); } let stats = pool.stats(); // All buffers should be returned assert!(stats.pooled > 0); } // When a buffer containing data is returned to the pool and then re-issued // to a new caller, its visible length must be zero. The caller must not be // able to read the previous contents through the BytesMut API. // (NOTE: the backing bytes are NOT zeroed — this is intentional. The pool // only resets the length. Actual plaintext data in the backing bytes is // not exploitable through safe Rust, because BytesMut only exposes [0..len].) #[test] fn pooled_buffer_length_is_zero_when_returned_and_re_issued() { let pool = Arc::new(BufferPool::with_config(1024, 10)); let mut buf = pool.get(); buf.extend_from_slice(b"sensitive plaintext that must not leak"); assert_eq!(buf.len(), 38); drop(buf); // returns to pool let reissued = pool.get(); assert_eq!(reissued.len(), 0, "re-issued buffer must have zero visible length"); assert!(reissued.is_empty(), "re-issued buffer.is_empty() must be true"); // Capacity may be non-zero (reserved), but len == 0 ensures no prior // bytes are accessible through the safe API. } // Preallocated buffers must be usable and their capacity must be at least // buffer_size so that callers do not immediately trigger a reallocation. #[test] fn preallocated_buffers_have_correct_capacity() { let pool = Arc::new(BufferPool::with_config(2048, 8)); pool.preallocate(4); let stats = pool.stats(); assert_eq!(stats.pooled, 4); let buf = pool.get(); assert!( buf.capacity() >= 2048, "preallocated buffer capacity ({}) must be >= buffer_size (2048)", buf.capacity() ); } // A buffer that grew moderately (within MAX_POOL_BUFFER_OVERSIZE_MULT of the canonical // size) must be returned to the pool, because the allocation is still reasonable and // reusing it is more efficient than allocating a new one. #[test] fn oversized_buffer_is_returned_to_pool() { let canonical = 64usize; let pool = Arc::new(BufferPool::with_config(canonical, 10)); let mut buf = pool.get(); // Grow to 2× the canonical size — within the 4× upper bound. buf.reserve(canonical); assert!(buf.capacity() >= canonical); assert!( buf.capacity() <= canonical * MAX_POOL_BUFFER_OVERSIZE_MULT, "pre-condition: test growth must stay within the acceptable bound" ); drop(buf); // The buffer must have been returned because capacity is within acceptable range. let stats = pool.stats(); assert_eq!(stats.pooled, 1, "moderately-oversized buffer must be returned to pool"); } // A buffer whose capacity fell below buffer_size (e.g. due to take() on // the internal BytesMut creating a sub-capacity view) is silently dropped // rather than pooled. The pool must not panic and stats must remain valid. #[test] fn undersized_take_does_not_panic_stats_remain_valid() { let pool = Arc::new(BufferPool::with_config(1024, 10)); let buf = pool.get(); // take() drains the buffer out of the pool wrapper; the wrapper then // calls return_buffer on a freshly allocated (zero-length) BytesMut. let _inner: bytes::BytesMut = buf.take(); // Pool should not have received back an undersized buffer — no panic. let stats = pool.stats(); assert!(stats.pooled <= 1, "undersized buffer must be silently dropped"); } // `allocated` is a high-water mark. After a miss-then-return cycle, the // counter stays at 1 even though the buffer is back in the pool and no // buffer is currently live. Callers must not interpret `allocated` as the // count of currently live buffers. #[test] fn allocated_is_high_water_mark_not_current_live_count() { let pool = Arc::new(BufferPool::with_config(64, 10)); // One miss: allocated = 1. let buf = pool.get(); { let stats = pool.stats(); assert_eq!(stats.allocated, 1); assert_eq!(stats.pooled, 0); } // Return to pool: allocated stays at 1 even though nothing is live. drop(buf); { let stats = pool.stats(); assert_eq!(stats.allocated, 1, "allocated must not decrease on buffer return"); assert_eq!(stats.pooled, 1); } // Hit (reuse): allocated stays 1; no new allocation occurs. let buf2 = pool.get(); drop(buf2); { let stats = pool.stats(); assert_eq!(stats.allocated, 1, "reusing a pooled buffer must not increase allocated"); // If allocated tracked current live count it would have dropped to 0 here — // which is the semantic mismatch this test guards against. } } // `allocated` must NOT drop below the total number of new allocations ever // made, even after all buffers have been returned and the pool is at full // capacity — meaning excess returns are silently dropped. #[test] fn allocated_never_decrements_even_when_pool_drops_excess_buffers() { let pool = Arc::new(BufferPool::with_config(64, 2)); // Allocate 5 buffers — 5 misses, allocated = 5. let bufs: Vec<_> = (0..5).map(|_| pool.get()).collect(); assert_eq!(pool.stats().allocated, 5); // Return all: pool can hold at most 2. The 3 excess buffers are dropped. drop(bufs); let stats = pool.stats(); assert_eq!(stats.pooled, 2, "pool should hold max 2"); assert_eq!( stats.allocated, 5, "allocated must stay at 5 even though 3 buffers were silently dropped" ); // If allocated were a live-count, it would now be 2 (only pooled). This // test documents that it is NOT: `allocated` is a high-water mark only. } // Repeated get/drop cycles must not inflate `allocated` beyond the true // number of distinct allocations. Each reuse must increment `hits` and // leave `allocated` unchanged. #[test] fn repeated_get_drop_does_not_inflate_allocated() { let pool = Arc::new(BufferPool::with_config(64, 10)); // Warm up pool with exactly 1 buffer. let buf = pool.get(); // miss: allocated = 1 drop(buf); let allocated_after_warmup = pool.stats().allocated; assert_eq!(allocated_after_warmup, 1); // 50 subsequent get/drop cycles — all hits, allocated must stay at 1. for _ in 0..50 { let b = pool.get(); drop(b); } let stats = pool.stats(); assert_eq!( stats.allocated, 1, "allocated must stay at 1 after 50 hit cycles; got {}", stats.allocated ); assert_eq!(stats.hits, 50); } // ── Security invariant: sensitive data must not leak between pool users ─── // A buffer containing "sensitive" bytes must be zeroed before being handed // to the next caller. An attacker who can trigger repeated pool cycles against // a shared buffer slot must not be able to read prior connection data. #[test] fn pooled_buffer_sensitive_data_is_cleared_before_reuse() { let pool = Arc::new(BufferPool::with_config(64, 2)); { let mut buf = pool.get(); buf.extend_from_slice(b"credentials:password123"); // Drop returns the buffer to the pool after clearing. } { let buf = pool.get(); // Buffer must be empty — no leftover bytes from the previous user. assert!(buf.is_empty(), "pool must clear buffer before handing it to the next caller"); assert_eq!(buf.len(), 0); } } // Verify that calling take() extracts the full content and the extracted // BytesMut does NOT get returned to the pool (no double-return). #[test] fn pooled_buffer_take_eliminates_pool_return() { let pool = Arc::new(BufferPool::with_config(64, 2)); let stats_before = pool.stats(); let mut buf = pool.get(); // miss buf.extend_from_slice(b"important"); let inner = buf.take(); // consumes PooledBuffer, should NOT return to pool assert_eq!(&inner[..], b"important"); let stats_after = pool.stats(); // pooled count must not increase — take() bypasses the pool assert_eq!( stats_after.pooled, stats_before.pooled, "take() must not return the buffer to the pool" ); } // Multiple concurrent get() calls must each get an independent empty buffer, // not aliased memory. An adversary who can cause aliased buffer access could // read or corrupt another connection's in-flight data. #[test] fn pooled_buffers_are_independent_no_aliasing() { let pool = Arc::new(BufferPool::with_config(64, 4)); let mut b1 = pool.get(); let mut b2 = pool.get(); b1.extend_from_slice(b"connection-A"); b2.extend_from_slice(b"connection-B"); assert_eq!(&b1[..], b"connection-A"); assert_eq!(&b2[..], b"connection-B"); // Verify no aliasing: modifying b2 does not affect b1. assert_ne!(&b1[..], &b2[..]); } // Oversized buffers (capacity grown beyond pool's canonical size) must NOT // be returned to the pool — this prevents the pool from holding oversized // buffers that could be handed to unrelated connections and leak large chunks // of heap across connection boundaries. #[test] fn oversized_buffer_is_dropped_not_pooled() { let canonical = 64usize; let pool = Arc::new(BufferPool::with_config(canonical, 4)); { let mut buf = pool.get(); // Grow well beyond the canonical size. buf.extend(std::iter::repeat(0u8).take(canonical * 8)); // Drop should abandon this oversized buffer rather than returning it. } let stats = pool.stats(); // Pool must be empty: the oversized buffer was not re-queued. assert_eq!( stats.pooled, 0, "oversized buffer must be dropped, not returned to pool (got {} pooled)", stats.pooled ); } // Deref on a PooledBuffer obtained normally must NOT panic. #[test] fn pooled_buffer_deref_on_live_buffer_does_not_panic() { let pool = Arc::new(BufferPool::new()); let mut buf = pool.get(); buf.extend_from_slice(b"hello"); assert_eq!(&buf[..], b"hello"); } }