memscope_rs/core/
allocator.rs1use std::alloc::{GlobalAlloc, Layout, System};
4
5pub(crate) mod capture_gate;
6
7pub struct TrackingAllocator;
12
13impl TrackingAllocator {
14 pub const fn new() -> Self {
16 Self
17 }
18
19 }
24
25thread_local! {
27 static TRACKING_DISABLED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
28}
29
30pub(crate) struct TrackingGuard;
36
37impl TrackingGuard {
38 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 let ptr = System.alloc(layout);
65
66 if !ptr.is_null() {
68 if let Some(_suppression) = TrackingGuard::acquire() {
69 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 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 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 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 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 allocator.dealloc(ptr, layout);
154 }
155 }
156
157 #[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 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(); unsafe {
192 let ptr = allocator.alloc(layout);
193 if !ptr.is_null() {
194 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 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 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 TRACKING_DISABLED.with(|disabled| {
239 assert!(!disabled.get());
240 });
241
242 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 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 assert_eq!((ptr as usize) % max_align, 0);
263 allocator.dealloc(ptr, layout);
264 }
265 }
266
267 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 let allocator = TrackingAllocator::new();
281 let layout = Layout::from_size_align(64, 8).unwrap();
282
283 static RECURSION_DETECTED: AtomicBool = AtomicBool::new(false);
285 static INIT: Once = Once::new();
286
287 INIT.call_once(|| {
288 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 unsafe {
302 let ptr = allocator.alloc(layout);
303 if !ptr.is_null() {
304 allocator.dealloc(ptr, layout);
305 }
306 }
307
308 assert!(
310 !RECURSION_DETECTED.load(Ordering::SeqCst),
311 "Recursive allocation detected - thread-local tracking failed"
312 );
313 }
314
315 #[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}