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