memscope_rs/analysis/heap_scanner/
reader.rs1use crate::analysis::is_virtual_pointer;
8use crate::analysis::unsafe_inference::is_valid_ptr;
9use crate::snapshot::types::ActiveAllocation;
10
11mod process_memory;
12
13const MAX_READ_BYTES: usize = 4096;
16
17const PAGE_SIZE: usize = 4096;
19
20#[derive(Debug)]
22pub struct ScanResult {
23 pub ptr: usize,
25 pub size: usize,
27 pub memory: Option<Vec<u8>>,
29}
30
31pub struct HeapScanner;
36
37impl HeapScanner {
38 pub fn scan(allocations: &[ActiveAllocation]) -> Vec<ScanResult> {
62 let regions = Self::dedup_heap_regions(allocations);
64
65 regions
67 .iter()
68 .map(|&(ptr, size)| {
69 let memory = safe_read_memory(ptr, size);
70 ScanResult { ptr, size, memory }
71 })
72 .collect()
73 }
74
75 fn dedup_heap_regions(allocs: &[ActiveAllocation]) -> Vec<(usize, usize)> {
84 use std::collections::HashSet;
85
86 let mut seen = HashSet::new();
87 let mut regions = Vec::new();
88
89 for alloc in allocs {
90 if let crate::core::types::TrackKind::HeapOwner { ptr, size } = alloc.kind {
91 if is_virtual_pointer(ptr) {
92 continue;
93 }
94
95 let key = (ptr, size);
96
97 if seen.insert(key) {
98 regions.push(key);
99 }
100 }
101 }
102
103 regions
104 }
105}
106
107fn safe_read_memory(ptr: usize, size: usize) -> Option<Vec<u8>> {
115 if size == 0 || ptr == 0 {
116 return None;
117 }
118
119 if !is_valid_ptr(ptr) {
120 return None;
121 }
122
123 let read_size = size.min(MAX_READ_BYTES);
124 if !are_pages_valid(ptr, read_size) {
125 return None;
126 }
127
128 let mut buf = crate::analysis::unsafe_inference::scan_buffer(read_size);
129 buf.resize(read_size, 0);
130
131 process_memory::read(ptr, &mut buf).then_some(buf)
132}
133
134fn are_pages_valid(ptr: usize, size: usize) -> bool {
136 let page_start = ptr & !(PAGE_SIZE - 1);
137 let Some(end) = ptr
138 .checked_add(size)
139 .and_then(|end| end.checked_add(PAGE_SIZE - 1))
140 else {
141 return false;
142 };
143 let page_end = end & !(PAGE_SIZE - 1);
144
145 let mut p = page_start;
146 while p < page_end {
147 if !is_valid_ptr(p) {
148 return false;
149 }
150 p += PAGE_SIZE;
151 }
152 true
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use crate::core::types::TrackKind;
159
160 #[test]
161 fn test_safe_read_memory_zero_size() {
162 assert!(safe_read_memory(0x10000, 0).is_none());
163 }
164
165 #[test]
166 fn test_safe_read_memory_null_ptr() {
167 assert!(safe_read_memory(0, 100).is_none());
168 }
169
170 #[test]
171 #[cfg(target_os = "macos")]
172 fn test_are_pages_valid_single_page() {
173 assert!(are_pages_valid(0x10000, 100));
174 }
175
176 #[test]
177 #[cfg(target_os = "macos")]
178 fn test_are_pages_valid_cross_page() {
179 let ptr = 0x10000;
181 let size = 200;
182 assert!(are_pages_valid(ptr, size));
183 }
184
185 #[test]
186 fn test_scan_result_creation() {
187 let result = ScanResult {
188 ptr: 0x1000,
189 size: 64,
190 memory: None,
191 };
192 assert_eq!(result.ptr, 0x1000);
193 assert_eq!(result.size, 64);
194 assert!(result.memory.is_none());
195 }
196
197 #[test]
198 #[cfg(not(target_os = "linux"))]
199 fn test_heap_scanner_scan_real_allocations() {
200 let data1 = vec![42u8; 64];
201 let data2 = vec![99u8; 128];
202 let ptr1 = data1.as_ptr() as usize;
203 let ptr2 = data2.as_ptr() as usize;
204
205 let allocations = vec![
206 ActiveAllocation {
207 ptr: Some(ptr1),
208 size: 64,
209 kind: TrackKind::HeapOwner {
210 ptr: ptr1,
211 size: 64,
212 },
213 allocated_at: 1000,
214 var_name: None,
215 type_name: None,
216 thread_id: 0,
217 call_stack_hash: None,
218 module_path: None,
219 stack_ptr: None,
220 },
221 ActiveAllocation {
222 ptr: Some(ptr2),
223 size: 128,
224 kind: TrackKind::HeapOwner {
225 ptr: ptr2,
226 size: 128,
227 },
228 allocated_at: 2000,
229 var_name: None,
230 type_name: None,
231 thread_id: 0,
232 call_stack_hash: None,
233 module_path: None,
234 stack_ptr: None,
235 },
236 ];
237
238 let results = HeapScanner::scan(&allocations);
239 assert_eq!(results.len(), 2);
240
241 assert!(results[0].memory.is_some(), "Should read memory at ptr1");
242 assert!(results[1].memory.is_some(), "Should read memory at ptr2");
243
244 drop(data1);
245 drop(data2);
246 }
247
248 #[test]
249 fn test_heap_scanner_scan_empty_allocations() {
250 let allocations: Vec<ActiveAllocation> = vec![];
251 let results = HeapScanner::scan(&allocations);
252 assert!(results.is_empty());
253 }
254
255 #[test]
256 fn test_heap_scanner_scan_zero_size_allocation() {
257 let allocations = vec![ActiveAllocation {
258 ptr: Some(0x10000),
259 size: 0,
260 kind: TrackKind::HeapOwner {
261 ptr: 0x10000,
262 size: 0,
263 },
264 allocated_at: 1000,
265 var_name: None,
266 type_name: None,
267 thread_id: 0,
268 call_stack_hash: None,
269 module_path: None,
270 stack_ptr: None,
271 }];
272
273 let results = HeapScanner::scan(&allocations);
274 assert_eq!(results.len(), 1);
275 assert!(results[0].memory.is_none());
277 }
278
279 #[test]
280 #[cfg(not(target_os = "linux"))]
281 #[ignore = "Heap pointer addresses may exceed VIRTUAL_PTR_BASE in some CI environments"]
282 fn test_heap_scanner_content_preserved_after_scan() {
283 let data = vec![0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE];
284 let ptr = data.as_ptr() as usize;
285 let size = data.len();
286
287 let alloc = ActiveAllocation {
288 ptr: Some(ptr),
289 size,
290 kind: TrackKind::HeapOwner { ptr, size },
291 allocated_at: 1000,
292 var_name: None,
293 type_name: None,
294 thread_id: 0,
295 call_stack_hash: None,
296 module_path: None,
297 stack_ptr: None,
298 };
299
300 let results = HeapScanner::scan(&[alloc]);
301 assert_eq!(results.len(), 1);
302
303 let mem = results[0]
304 .memory
305 .as_ref()
306 .expect("Should read memory at allocated address");
307 assert_eq!(mem.len(), size, "Should read expected number of bytes");
308
309 drop(data);
310 }
311}