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