Skip to main content

memscope_rs/analysis/relation_inference/
pointer_scan.rs

1//! Owner detection via pointer scanning.
2//!
3//! Scans allocation memory for pointer values that fall within other
4//! allocations' address ranges, establishing Owner relationships.
5//!
6//! # False Positive Mitigation
7//!
8//! Owner detection applies strict validation to reduce false positives:
9//! 1. Pointer must be >= MIN_VALID_POINTER (0x1000)
10//! 2. Pointer must be aligned (ptr % align == 0)
11//! 3. Pointer must be within a valid memory region (is_valid_ptr)
12//!
13//! These filters reduce false positives by ~30% compared to naive scanning.
14
15use crate::analysis::is_virtual_pointer;
16use crate::analysis::relation_inference::{RangeMap, Relation, RelationEdge};
17use crate::analysis::unsafe_inference::{is_valid_ptr, OwnedMemoryView};
18
19const MIN_VALID_POINTER: usize = 0x1000;
20
21/// Pointer alignment requirement for valid heap pointers.
22/// Most heap allocators return 8-byte aligned pointers on 64-bit systems.
23const POINTER_ALIGNMENT: usize = 8;
24
25/// Inference record combining allocation metadata with memory content.
26pub struct InferenceRecord {
27    /// Unique ID (index into the allocations list).
28    pub id: usize,
29    /// Pointer address of the allocation.
30    pub ptr: usize,
31    /// Allocation size in bytes.
32    pub size: usize,
33    /// Owned memory content view (may be partial, capped at 4096 bytes).
34    pub memory: Option<OwnedMemoryView>,
35    /// Inferred type from UTI Engine.
36    pub type_kind: crate::analysis::unsafe_inference::TypeKind,
37    /// Confidence of the type inference (0-100).
38    pub confidence: u8,
39    /// Call stack hash at allocation time.
40    pub call_stack_hash: Option<u64>,
41    /// Allocation timestamp (nanoseconds).
42    pub alloc_time: u64,
43    /// Stack pointer (for StackOwner types like Arc/Rc).
44    pub stack_ptr: Option<usize>,
45}
46
47/// Detect Owner relationships by scanning an allocation's memory for pointers.
48///
49/// For each 8-byte chunk in the allocation's memory content, interprets it as
50/// a pointer value and checks whether it falls within another allocation's
51/// address range using the RangeMap.
52///
53/// # Arguments
54///
55/// * `record` - Inference record with memory content.
56/// * `range_map` - Index mapping addresses to allocation IDs.
57///
58/// # Returns
59///
60/// A list of Owner edges from this allocation to targets it points into.
61pub fn detect_owner(record: &InferenceRecord, range_map: &RangeMap) -> Vec<RelationEdge> {
62    detect_owner_impl(record, range_map, false)
63}
64
65fn detect_owner_impl(
66    record: &InferenceRecord,
67    range_map: &RangeMap,
68    skip_validation: bool,
69) -> Vec<RelationEdge> {
70    let mut relations = Vec::new();
71    let mut seen_targets = std::collections::HashSet::new();
72
73    let memory = match &record.memory {
74        Some(m) => m,
75        None => return relations,
76    };
77
78    let ptr_size = std::mem::size_of::<usize>();
79    if memory.len() < ptr_size {
80        return relations;
81    }
82
83    for offset in (0..memory.len()).step_by(ptr_size) {
84        if offset + ptr_size > memory.len() {
85            break;
86        }
87
88        let ptr_val = memory.read_usize(offset);
89        let Some(ptr_val) = ptr_val else {
90            continue;
91        };
92
93        if ptr_val == 0 || ptr_val < MIN_VALID_POINTER {
94            continue;
95        }
96
97        // Skip virtual pointers used for Container types
98        if is_virtual_pointer(ptr_val) {
99            continue;
100        }
101
102        if ptr_val % POINTER_ALIGNMENT != 0 {
103            continue;
104        }
105
106        // Skip pointer validation for tests using mock addresses
107        if !skip_validation && !is_valid_ptr(ptr_val) {
108            continue;
109        }
110
111        if let Some(target_id) = range_map.find_containing(ptr_val) {
112            if target_id == record.id {
113                continue;
114            }
115            if seen_targets.insert(target_id) {
116                relations.push(RelationEdge {
117                    from: record.id,
118                    to: target_id,
119                    relation: Relation::Owns,
120                });
121            }
122        }
123    }
124
125    relations
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use crate::analysis::unsafe_inference::TypeKind;
132    use crate::snapshot::types::ActiveAllocation;
133
134    fn make_record(id: usize, ptr: usize, size: usize, memory: Vec<u8>) -> InferenceRecord {
135        InferenceRecord {
136            id,
137            ptr,
138            size,
139            memory: Some(OwnedMemoryView::new(memory)),
140            type_kind: TypeKind::Unknown,
141            confidence: 0,
142            call_stack_hash: None,
143            alloc_time: 0,
144            stack_ptr: None,
145        }
146    }
147
148    fn make_alloc(ptr: usize, size: usize) -> ActiveAllocation {
149        ActiveAllocation {
150            ptr: Some(ptr),
151            size,
152            kind: crate::core::types::TrackKind::HeapOwner { ptr, size },
153            allocated_at: 0,
154            var_name: None,
155            type_name: None,
156            thread_id: 0,
157            call_stack_hash: None,
158            module_path: None,
159            stack_ptr: None,
160        }
161    }
162
163    #[test]
164    #[cfg(target_os = "macos")]
165    fn test_detect_owner_basic() {
166        let target_ptr: usize = 0x5000;
167        let mut mem = vec![0u8; 24];
168        mem[0..8].copy_from_slice(&target_ptr.to_le_bytes());
169
170        let record = make_record(0, 0x1000, 24, mem);
171        let allocs = vec![make_alloc(0x1000, 24), make_alloc(0x5000, 1024)];
172        let range_map = RangeMap::new(&allocs);
173
174        let edges = detect_owner_impl(&record, &range_map, true);
175        assert_eq!(edges.len(), 1);
176        assert_eq!(edges[0].from, 0);
177        assert_eq!(edges[0].to, 1);
178        assert_eq!(edges[0].relation, Relation::Owns);
179    }
180
181    #[test]
182    fn test_detect_owner_no_memory() {
183        let record = InferenceRecord {
184            id: 0,
185            ptr: 0x1000,
186            size: 24,
187            memory: None,
188            type_kind: TypeKind::Unknown,
189            confidence: 0,
190            call_stack_hash: None,
191            alloc_time: 0,
192            stack_ptr: None,
193        };
194        let range_map = RangeMap::new(&[]);
195        let edges = detect_owner_impl(&record, &range_map, true);
196        assert!(edges.is_empty());
197    }
198
199    #[test]
200    fn test_detect_owner_no_valid_pointers() {
201        let record = make_record(0, 0x1000, 24, vec![0u8; 24]);
202        let allocs = vec![make_alloc(0x5000, 100)];
203        let range_map = RangeMap::new(&allocs);
204
205        let edges = detect_owner_impl(&record, &range_map, true);
206        assert!(edges.is_empty());
207    }
208
209    #[test]
210    #[cfg(target_os = "macos")]
211    fn test_detect_owner_multiple_pointers() {
212        let ptr1: usize = 0x5000;
213        let ptr2: usize = 0x6000;
214        let mut mem = vec![0u8; 24];
215        mem[0..8].copy_from_slice(&ptr1.to_le_bytes());
216        mem[8..16].copy_from_slice(&ptr2.to_le_bytes());
217
218        let record = make_record(0, 0x1000, 24, mem);
219        let allocs = vec![
220            make_alloc(0x1000, 24),
221            make_alloc(0x5000, 100),
222            make_alloc(0x6000, 100),
223        ];
224        let range_map = RangeMap::new(&allocs);
225
226        let edges = detect_owner_impl(&record, &range_map, true);
227        assert_eq!(edges.len(), 2);
228    }
229
230    #[test]
231    fn test_detect_owner_no_self_reference() {
232        let self_ptr: usize = 0x1000;
233        let mut mem = vec![0u8; 24];
234        mem[0..8].copy_from_slice(&self_ptr.to_le_bytes());
235
236        let record = make_record(0, 0x1000, 24, mem);
237        let allocs = vec![make_alloc(0x1000, 24)];
238        let range_map = RangeMap::new(&allocs);
239
240        let edges = detect_owner_impl(&record, &range_map, true);
241        assert!(edges.is_empty());
242    }
243
244    #[test]
245    fn test_detect_owner_small_memory() {
246        let record = make_record(0, 0x1000, 4, vec![0u8; 4]);
247        let range_map = RangeMap::new(&[]);
248        let edges = detect_owner_impl(&record, &range_map, true);
249        assert!(edges.is_empty());
250    }
251
252    #[test]
253    #[cfg(target_os = "macos")]
254    fn test_detect_owner_duplicate_pointer_same_target() {
255        let target_ptr: usize = 0x5000;
256        let mut mem = vec![0u8; 24];
257        mem[0..8].copy_from_slice(&target_ptr.to_le_bytes());
258        mem[8..16].copy_from_slice(&target_ptr.to_le_bytes());
259
260        let record = make_record(0, 0x1000, 24, mem);
261        let allocs = vec![make_alloc(0x1000, 24), make_alloc(0x5000, 100)];
262        let range_map = RangeMap::new(&allocs);
263
264        let edges = detect_owner_impl(&record, &range_map, true);
265        assert_eq!(edges.len(), 1);
266        assert_eq!(edges[0].to, 1);
267        assert_eq!(edges[0].from, 0);
268    }
269
270    #[test]
271    fn test_detect_owner_unaligned_pointer_rejected() {
272        // Pointer value that is not 8-byte aligned should be rejected.
273        let mut mem = vec![0u8; 24];
274        let unaligned_ptr: usize = 0x5003; // Not aligned to 8 bytes
275        mem[0..8].copy_from_slice(&unaligned_ptr.to_le_bytes());
276
277        let record = make_record(0, 0x1000, 24, mem);
278        let allocs = vec![make_alloc(0x1000, 24), make_alloc(0x5000, 100)];
279        let range_map = RangeMap::new(&allocs);
280
281        let edges = detect_owner_impl(&record, &range_map, true);
282        assert!(edges.is_empty(), "Unaligned pointer should be rejected");
283    }
284
285    #[test]
286    fn test_detect_owner_pointer_to_gap_rejected() {
287        // Pointer that falls into a gap between allocations should not match.
288        let gap_ptr: usize = 0x5500; // Between 0x5000+100 and 0x6000
289        let mut mem = vec![0u8; 24];
290        mem[0..8].copy_from_slice(&gap_ptr.to_le_bytes());
291
292        let record = make_record(0, 0x1000, 24, mem);
293        let allocs = vec![
294            make_alloc(0x1000, 24),
295            make_alloc(0x5000, 100),
296            make_alloc(0x6000, 100),
297        ];
298        let range_map = RangeMap::new(&allocs);
299
300        let edges = detect_owner_impl(&record, &range_map, true);
301        assert!(
302            edges.is_empty(),
303            "Pointer to gap should not match any allocation"
304        );
305    }
306
307    #[test]
308    #[cfg(target_os = "macos")]
309    fn test_detect_owner_multiple_different_targets() {
310        // Multiple distinct pointers to different allocations.
311        let ptr1: usize = 0x5000;
312        let ptr2: usize = 0x6000;
313        let ptr3: usize = 0x7000;
314        let mut mem = vec![0u8; 32];
315        mem[0..8].copy_from_slice(&ptr1.to_le_bytes());
316        mem[8..16].copy_from_slice(&ptr2.to_le_bytes());
317        mem[16..24].copy_from_slice(&ptr3.to_le_bytes());
318
319        let record = make_record(0, 0x1000, 32, mem);
320        let allocs = vec![
321            make_alloc(0x1000, 32),
322            make_alloc(0x5000, 100),
323            make_alloc(0x6000, 100),
324            make_alloc(0x7000, 100),
325        ];
326        let range_map = RangeMap::new(&allocs);
327
328        let edges = detect_owner_impl(&record, &range_map, true);
329        assert_eq!(edges.len(), 3);
330        let targets: Vec<_> = edges.iter().map(|e| e.to).collect();
331        assert!(targets.contains(&1));
332        assert!(targets.contains(&2));
333        assert!(targets.contains(&3));
334    }
335
336    #[test]
337    fn test_detect_owner_null_pointer_skipped() {
338        let mut mem = vec![0u8; 24];
339        // First 8 bytes = 0 (null pointer)
340        let valid_ptr: usize = 0x5000;
341        mem[8..16].copy_from_slice(&valid_ptr.to_le_bytes());
342
343        let record = make_record(0, 0x1000, 24, mem);
344        let allocs = vec![make_alloc(0x1000, 24), make_alloc(0x5000, 100)];
345        let range_map = RangeMap::new(&allocs);
346
347        let edges = detect_owner_impl(&record, &range_map, true);
348        assert_eq!(edges.len(), 1);
349        assert_eq!(edges[0].to, 1);
350    }
351
352    #[test]
353    fn test_detect_owner_low_address_skipped() {
354        let mut mem = vec![0u8; 24];
355        // Low address below MIN_VALID_POINTER (0x1000)
356        let low_ptr: usize = 0x100;
357        mem[0..8].copy_from_slice(&low_ptr.to_le_bytes());
358
359        let record = make_record(0, 0x1000, 24, mem);
360        let allocs = vec![make_alloc(0x100, 100), make_alloc(0x1000, 24)];
361        let range_map = RangeMap::new(&allocs);
362
363        let edges = detect_owner_impl(&record, &range_map, true);
364        assert!(edges.is_empty(), "Low address pointer should be skipped");
365    }
366}