Skip to main content

memscope_rs/core/
allocator.rs

1//! Custom global allocator for tracking memory allocations.
2
3use std::alloc::{GlobalAlloc, Layout, System};
4
5pub(crate) mod capture_gate;
6
7/// A custom allocator that tracks memory allocations and deallocations.
8///
9/// This allocator wraps the system allocator and records all allocation
10/// and deallocation events through the global memory tracker.
11pub struct TrackingAllocator;
12
13impl TrackingAllocator {
14    /// Create a new tracking allocator instance.
15    pub const fn new() -> Self {
16        Self
17    }
18
19    // Removed: `_infer_type_from_allocation_context` and friends mapped a request
20    // size to "String"/"Vec<T>" style names. A byte count cannot identify a type,
21    // so those helpers were dead weight that invited false conclusions. The
22    // allocator now records only what it actually observes: address and size.
23}
24
25// Thread-local flag to prevent recursive tracking
26thread_local! {
27    static TRACKING_DISABLED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
28}
29
30/// Scope guard that suppresses tracking while the allocator works.
31///
32/// The previous code toggled the thread-local flag manually, so any early return (or a
33/// panic inside the tracker) left tracking disabled for the rest of the thread's life.
34/// Restoring on `Drop` makes the suppression impossible to leak.
35pub(crate) struct TrackingGuard;
36
37impl TrackingGuard {
38    /// Take the suppression slot, or `None` when tracking is already suppressed
39    /// (recursive allocation from inside the tracker itself).
40    pub(crate) fn acquire() -> Option<Self> {
41        TRACKING_DISABLED
42            .try_with(|disabled| {
43                if disabled.get() {
44                    None
45                } else {
46                    disabled.set(true);
47                    Some(Self)
48                }
49            })
50            .ok()
51            .flatten()
52    }
53}
54
55impl Drop for TrackingGuard {
56    fn drop(&mut self) {
57        let _ = TRACKING_DISABLED.try_with(|disabled| disabled.set(false));
58    }
59}
60
61unsafe impl GlobalAlloc for TrackingAllocator {
62    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
63        // Allocate memory first
64        let ptr = System.alloc(layout);
65
66        // Track the allocation if it succeeded and tracking is not disabled
67        if !ptr.is_null() {
68            if let Some(_suppression) = TrackingGuard::acquire() {
69                // `catch_unwind` keeps a tracker failure from breaking user allocation.
70                let _ = std::panic::catch_unwind(|| {
71                    let _gate = capture_gate::acquire(ptr as usize);
72                    crate::tracker::allocator_events::global_tracker()
73                        .record_allocator_allocation(ptr as usize, layout.size());
74                });
75            }
76        }
77
78        ptr
79    }
80
81    /// Reallocate through the system allocator and record the transition once.
82    ///
83    /// The default `GlobalAlloc::realloc` implementation is alloc + copy + dealloc,
84    /// which turns one logical operation into two unrelated events and loses the
85    /// fact that the block kept its identity. Recording a single `Reallocate` event
86    /// with both addresses keeps the ledger consistent for moved blocks.
87    unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
88        let Some(_suppression) = TrackingGuard::acquire() else {
89            return System.realloc(ptr, layout, new_size);
90        };
91
92        // Save the actual allocator result even if event publication panics after
93        // the system call. Repeating realloc would touch an already-freed pointer.
94        let mut result = None;
95        let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
96            let _gate = capture_gate::acquire(ptr as usize);
97            crate::tracker::allocator_events::global_tracker().allocator_reallocation(
98                ptr as usize,
99                layout.size(),
100                new_size,
101                || {
102                    let new_ptr = System.realloc(ptr, layout, new_size);
103                    result = Some(new_ptr);
104                    new_ptr
105                },
106            );
107        }));
108        result.unwrap_or_else(|| System.realloc(ptr, layout, new_size))
109    }
110
111    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
112        // Track the deallocation first, then hand the block back to the system.
113        if let Some(_suppression) = TrackingGuard::acquire() {
114            let _ = std::panic::catch_unwind(|| {
115                let _gate = capture_gate::acquire(ptr as usize);
116                let _ = crate::tracker::allocator_events::global_tracker()
117                    .track_deallocation(ptr as usize);
118            });
119        }
120
121        System.dealloc(ptr, layout);
122    }
123}
124
125impl Default for TrackingAllocator {
126    fn default() -> Self {
127        Self::new()
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use std::alloc::{GlobalAlloc, Layout};
135    use std::sync::atomic::{AtomicBool, Ordering};
136    use std::sync::Once;
137
138    // Helper to reset thread-local state between tests
139    fn reset_thread_local_state() {
140        TRACKING_DISABLED.with(|disabled| disabled.set(false));
141    }
142
143    #[test]
144    fn test_allocation_tracking() {
145        let allocator = TrackingAllocator::new();
146        let layout = Layout::from_size_align(1024, 8).unwrap();
147
148        unsafe {
149            let ptr = allocator.alloc(layout);
150            assert!(!ptr.is_null());
151
152            // Test deallocation
153            allocator.dealloc(ptr, layout);
154        }
155    }
156
157    /// Objective: Verify that zero-sized requests never reach the allocator.
158    /// Invariants: `GlobalAlloc` requires `layout.size() > 0` (the standard
159    /// library aborts otherwise), so ZST containers must use the dangling
160    /// sentinel pointer and produce no tracked allocation at all.
161    #[test]
162    fn test_zero_sized_allocation() {
163        let layout = Layout::from_size_align(0, 1).expect("zero-size layout must be constructible");
164        assert_eq!(
165            layout.size(),
166            0,
167            "the zero-size layout must report a zero length, otherwise the test is meaningless"
168        );
169
170        // ZST vectors share the non-null dangling sentinel: `with_capacity` on a
171        // zero-sized element never asks the global allocator for memory.
172        let with_capacity: Vec<()> = Vec::with_capacity(1024);
173        let empty: Vec<()> = Vec::new();
174        assert_eq!(
175            with_capacity.as_ptr(),
176            empty.as_ptr(),
177            "a ZST vector must reuse the dangling sentinel instead of allocating"
178        );
179        assert!(
180            with_capacity.capacity() >= 1024,
181            "a ZST vector must satisfy its requested capacity without allocating, got {}",
182            with_capacity.capacity()
183        );
184    }
185
186    #[test]
187    fn test_large_allocation() {
188        let allocator = TrackingAllocator::new();
189        let layout = Layout::from_size_align(1024 * 1024, 8).unwrap(); // 1MB
190
191        unsafe {
192            let ptr = allocator.alloc(layout);
193            if !ptr.is_null() {
194                // Only test deallocation if allocation succeeded
195                allocator.dealloc(ptr, layout);
196            }
197        }
198    }
199
200    #[test]
201    fn test_multiple_allocations() {
202        let allocator = TrackingAllocator::new();
203        let mut ptrs = Vec::new();
204
205        // Allocate multiple blocks
206        for i in 1..=10 {
207            let layout = Layout::from_size_align(i * 64, 8).unwrap();
208            unsafe {
209                let ptr = allocator.alloc(layout);
210                if !ptr.is_null() {
211                    ptrs.push((ptr, layout));
212                }
213            }
214        }
215
216        // Deallocate all blocks
217        for (ptr, layout) in ptrs {
218            unsafe {
219                allocator.dealloc(ptr, layout);
220            }
221        }
222    }
223
224    #[test]
225    fn test_default_implementation() {
226        let allocator = TrackingAllocator::new();
227        assert_eq!(
228            std::mem::size_of_val(&allocator),
229            std::mem::size_of::<TrackingAllocator>()
230        );
231    }
232
233    #[test]
234    fn test_thread_local_tracking() {
235        reset_thread_local_state();
236
237        // Test that tracking is enabled by default
238        TRACKING_DISABLED.with(|disabled| {
239            assert!(!disabled.get());
240        });
241
242        // Test disabling tracking
243        TRACKING_DISABLED.with(|disabled| {
244            disabled.set(true);
245            assert!(disabled.get());
246            disabled.set(false);
247        });
248    }
249
250    #[test]
251    fn test_allocation_edge_cases() {
252        let allocator = TrackingAllocator::new();
253
254        // Test with maximum alignment
255        let max_align = std::mem::size_of::<usize>() * 2;
256        let layout = Layout::from_size_align(16, max_align).unwrap();
257
258        unsafe {
259            let ptr = allocator.alloc(layout);
260            if !ptr.is_null() {
261                // Test that the pointer is properly aligned
262                assert_eq!((ptr as usize) % max_align, 0);
263                allocator.dealloc(ptr, layout);
264            }
265        }
266
267        // Test with minimal size but non-zero
268        let layout = Layout::from_size_align(1, 1).unwrap();
269        unsafe {
270            let ptr = allocator.alloc(layout);
271            if !ptr.is_null() {
272                allocator.dealloc(ptr, layout);
273            }
274        }
275    }
276
277    #[test]
278    fn test_recursive_allocation_handling() {
279        // This test verifies that recursive allocations don't cause infinite loops
280        let allocator = TrackingAllocator::new();
281        let layout = Layout::from_size_align(64, 8).unwrap();
282
283        // Set up a flag to detect if we're in a recursive call
284        static RECURSION_DETECTED: AtomicBool = AtomicBool::new(false);
285        static INIT: Once = Once::new();
286
287        INIT.call_once(|| {
288            // Install a panic hook to detect if we hit a stack overflow
289            let original_hook = std::panic::take_hook();
290            std::panic::set_hook(Box::new(move |panic_info| {
291                if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
292                    if s.contains("stack overflow") {
293                        RECURSION_DETECTED.store(true, Ordering::SeqCst);
294                    }
295                }
296                original_hook(panic_info);
297            }));
298        });
299
300        // This allocation will trigger tracking, but the thread-local flag should prevent recursion
301        unsafe {
302            let ptr = allocator.alloc(layout);
303            if !ptr.is_null() {
304                allocator.dealloc(ptr, layout);
305            }
306        }
307
308        // Verify we didn't hit a stack overflow
309        assert!(
310            !RECURSION_DETECTED.load(Ordering::SeqCst),
311            "Recursive allocation detected - thread-local tracking failed"
312        );
313    }
314
315    /// Objective: Verify the suppression guard restores tracking on drop, including
316    /// when a nested acquisition is refused.
317    /// Invariants: exactly one guard owns the slot; the nested one does not restore
318    /// early; dropping the owner leaves tracking enabled.
319    #[test]
320    fn suppression_guard_restores_state() {
321        reset_thread_local_state();
322
323        {
324            let outer = TrackingGuard::acquire();
325            assert!(outer.is_some(), "the first guard must take the slot");
326            assert!(
327                TRACKING_DISABLED.with(|disabled| disabled.get()),
328                "tracking must be suppressed while the guard is alive"
329            );
330
331            {
332                let inner = TrackingGuard::acquire();
333                assert!(
334                    inner.is_none(),
335                    "a nested acquisition must be refused instead of double-owning the slot"
336                );
337            }
338
339            assert!(
340                TRACKING_DISABLED.with(|disabled| disabled.get()),
341                "the refused nested guard must not restore tracking early"
342            );
343        }
344
345        assert!(
346            !TRACKING_DISABLED.with(|disabled| disabled.get()),
347            "dropping the owning guard must restore tracking"
348        );
349    }
350}