use std::alloc::{GlobalAlloc, Layout, System};
pub(crate) mod capture_gate;
pub struct TrackingAllocator;
impl TrackingAllocator {
pub const fn new() -> Self {
Self
}
}
thread_local! {
static TRACKING_DISABLED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
pub(crate) struct TrackingGuard;
impl TrackingGuard {
pub(crate) fn acquire() -> Option<Self> {
TRACKING_DISABLED
.try_with(|disabled| {
if disabled.get() {
None
} else {
disabled.set(true);
Some(Self)
}
})
.ok()
.flatten()
}
}
impl Drop for TrackingGuard {
fn drop(&mut self) {
let _ = TRACKING_DISABLED.try_with(|disabled| disabled.set(false));
}
}
unsafe impl GlobalAlloc for TrackingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let ptr = System.alloc(layout);
if !ptr.is_null() {
if let Some(_suppression) = TrackingGuard::acquire() {
let _ = std::panic::catch_unwind(|| {
let _gate = capture_gate::acquire(ptr as usize);
crate::tracker::allocator_events::global_tracker()
.record_allocator_allocation(ptr as usize, layout.size());
});
}
}
ptr
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
let Some(_suppression) = TrackingGuard::acquire() else {
return System.realloc(ptr, layout, new_size);
};
let mut result = None;
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _gate = capture_gate::acquire(ptr as usize);
crate::tracker::allocator_events::global_tracker().allocator_reallocation(
ptr as usize,
layout.size(),
new_size,
|| {
let new_ptr = System.realloc(ptr, layout, new_size);
result = Some(new_ptr);
new_ptr
},
);
}));
result.unwrap_or_else(|| System.realloc(ptr, layout, new_size))
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
if let Some(_suppression) = TrackingGuard::acquire() {
let _ = std::panic::catch_unwind(|| {
let _gate = capture_gate::acquire(ptr as usize);
let _ = crate::tracker::allocator_events::global_tracker()
.track_deallocation(ptr as usize);
});
}
System.dealloc(ptr, layout);
}
}
impl Default for TrackingAllocator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::alloc::{GlobalAlloc, Layout};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Once;
fn reset_thread_local_state() {
TRACKING_DISABLED.with(|disabled| disabled.set(false));
}
#[test]
fn test_allocation_tracking() {
let allocator = TrackingAllocator::new();
let layout = Layout::from_size_align(1024, 8).unwrap();
unsafe {
let ptr = allocator.alloc(layout);
assert!(!ptr.is_null());
allocator.dealloc(ptr, layout);
}
}
#[test]
fn test_zero_sized_allocation() {
let layout = Layout::from_size_align(0, 1).expect("zero-size layout must be constructible");
assert_eq!(
layout.size(),
0,
"the zero-size layout must report a zero length, otherwise the test is meaningless"
);
let with_capacity: Vec<()> = Vec::with_capacity(1024);
let empty: Vec<()> = Vec::new();
assert_eq!(
with_capacity.as_ptr(),
empty.as_ptr(),
"a ZST vector must reuse the dangling sentinel instead of allocating"
);
assert!(
with_capacity.capacity() >= 1024,
"a ZST vector must satisfy its requested capacity without allocating, got {}",
with_capacity.capacity()
);
}
#[test]
fn test_large_allocation() {
let allocator = TrackingAllocator::new();
let layout = Layout::from_size_align(1024 * 1024, 8).unwrap();
unsafe {
let ptr = allocator.alloc(layout);
if !ptr.is_null() {
allocator.dealloc(ptr, layout);
}
}
}
#[test]
fn test_multiple_allocations() {
let allocator = TrackingAllocator::new();
let mut ptrs = Vec::new();
for i in 1..=10 {
let layout = Layout::from_size_align(i * 64, 8).unwrap();
unsafe {
let ptr = allocator.alloc(layout);
if !ptr.is_null() {
ptrs.push((ptr, layout));
}
}
}
for (ptr, layout) in ptrs {
unsafe {
allocator.dealloc(ptr, layout);
}
}
}
#[test]
fn test_default_implementation() {
let allocator = TrackingAllocator::new();
assert_eq!(
std::mem::size_of_val(&allocator),
std::mem::size_of::<TrackingAllocator>()
);
}
#[test]
fn test_thread_local_tracking() {
reset_thread_local_state();
TRACKING_DISABLED.with(|disabled| {
assert!(!disabled.get());
});
TRACKING_DISABLED.with(|disabled| {
disabled.set(true);
assert!(disabled.get());
disabled.set(false);
});
}
#[test]
fn test_allocation_edge_cases() {
let allocator = TrackingAllocator::new();
let max_align = std::mem::size_of::<usize>() * 2;
let layout = Layout::from_size_align(16, max_align).unwrap();
unsafe {
let ptr = allocator.alloc(layout);
if !ptr.is_null() {
assert_eq!((ptr as usize) % max_align, 0);
allocator.dealloc(ptr, layout);
}
}
let layout = Layout::from_size_align(1, 1).unwrap();
unsafe {
let ptr = allocator.alloc(layout);
if !ptr.is_null() {
allocator.dealloc(ptr, layout);
}
}
}
#[test]
fn test_recursive_allocation_handling() {
let allocator = TrackingAllocator::new();
let layout = Layout::from_size_align(64, 8).unwrap();
static RECURSION_DETECTED: AtomicBool = AtomicBool::new(false);
static INIT: Once = Once::new();
INIT.call_once(|| {
let original_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
if s.contains("stack overflow") {
RECURSION_DETECTED.store(true, Ordering::SeqCst);
}
}
original_hook(panic_info);
}));
});
unsafe {
let ptr = allocator.alloc(layout);
if !ptr.is_null() {
allocator.dealloc(ptr, layout);
}
}
assert!(
!RECURSION_DETECTED.load(Ordering::SeqCst),
"Recursive allocation detected - thread-local tracking failed"
);
}
#[test]
fn suppression_guard_restores_state() {
reset_thread_local_state();
{
let outer = TrackingGuard::acquire();
assert!(outer.is_some(), "the first guard must take the slot");
assert!(
TRACKING_DISABLED.with(|disabled| disabled.get()),
"tracking must be suppressed while the guard is alive"
);
{
let inner = TrackingGuard::acquire();
assert!(
inner.is_none(),
"a nested acquisition must be refused instead of double-owning the slot"
);
}
assert!(
TRACKING_DISABLED.with(|disabled| disabled.get()),
"the refused nested guard must not restore tracking early"
);
}
assert!(
!TRACKING_DISABLED.with(|disabled| disabled.get()),
"dropping the owning guard must restore tracking"
);
}
}