memscope_rs/analyzer/detect.rs
1//! Detection analysis module.
2
3use crate::analyzer::report::{LeakReport, SafetyReport, UafReport};
4use crate::snapshot::ActiveAllocation;
5use crate::view::MemoryView;
6use tracing::{debug, warn};
7
8/// Detection analysis module.
9///
10/// Provides leak, UAF, and safety detection.
11pub struct DetectionAnalysis {
12 view: MemoryView,
13}
14
15impl DetectionAnalysis {
16 /// Create from view.
17 pub fn from_view(view: &MemoryView) -> Self {
18 debug!("Creating DetectionAnalysis with {} allocations", view.len());
19 Self { view: view.clone() }
20 }
21
22 /// Allocations that are still live at the cut.
23 ///
24 /// This is a **fact**, not a leak verdict: an allocation that was not released
25 /// inside the capture window is simply still live there.
26 pub fn unreleased(&self) -> Vec<&ActiveAllocation> {
27 self.view
28 .allocations()
29 .into_iter()
30 .filter(|a| a.ptr.is_some())
31 .collect()
32 }
33
34 /// Detect memory leaks.
35 ///
36 /// A leak is a **conclusion** that needs an assessment rule plus evidence about
37 /// capture completeness (plan ยง4.5). The view only carries the active set, so no
38 /// allocation can be assessed here and this returns an empty report.
39 ///
40 /// The historical implementation reported *every* live allocation as a leak
41 /// (`filter(|a| a.ptr.is_some())`), which made any normal program look like it was
42 /// leaking everything it held (plan A02). Use [`Self::unreleased`] when the caller
43 /// needs the "still live, no release observed" list.
44 pub fn leaks(&self) -> LeakReport {
45 let unreleased = self.unreleased();
46 if !unreleased.is_empty() {
47 debug!(
48 "Leak assessment not performed: {} allocations are still live at the cut",
49 unreleased.len()
50 );
51 }
52
53 LeakReport {
54 leak_count: 0,
55 total_leaked_bytes: 0,
56 leaked_allocations: Vec::new(),
57 }
58 }
59
60 /// Detect use-after-free.
61 ///
62 /// Analyzes event stream for potential UAF patterns by tracking
63 /// deallocations and checking for subsequent accesses to the same address.
64 ///
65 /// Note:
66 /// - Reallocations are NOT flagged as UAF because realloc on a
67 /// deallocated pointer is valid behavior (the allocator may reuse the address).
68 /// - Metadata events are NOT flagged as UAF because they represent
69 /// type/container information, not actual memory access.
70 /// - Only actual memory access events (read/write operations) on freed
71 /// memory would indicate UAF, but these are not currently tracked.
72 ///
73 /// Returns an empty report as UAF detection requires runtime memory
74 /// access tracking which is not implemented in the current architecture.
75 pub fn uaf(&self) -> UafReport {
76 // UAF detection requires tracking actual memory accesses (read/write)
77 // on freed pointers. The current event system does not track these
78 // operations. Realloc and Metadata events are not UAF indicators:
79 // - Realloc: valid allocator behavior, may reuse addresses
80 // - Metadata: type information only, no memory access
81 //
82 // For proper UAF detection, consider using sanitizers like ASAN
83 // or instrumenting memory access at runtime.
84 debug!("UAF detection: not implemented, returning empty report");
85 UafReport::empty()
86 }
87
88 /// Analyze memory safety.
89 ///
90 /// Checks for common safety issues.
91 pub fn safety(&self) -> SafetyReport {
92 let allocations = self.view.allocations();
93 let report = SafetyReport::from_allocations(&allocations);
94 if report.issue_count > 0 {
95 warn!(
96 "Safety analysis: found {} potential safety issues",
97 report.issue_count
98 );
99 }
100 report
101 }
102
103 /// Get detection summary.
104 pub fn summary(&self) -> DetectionSummary {
105 let leaks = self.leaks();
106 let safety = self.safety();
107 DetectionSummary {
108 leak_count: leaks.leak_count,
109 leaked_bytes: leaks.total_leaked_bytes,
110 uaf_count: 0, // UAF detection not implemented
111 safety_issues: safety.issue_count,
112 }
113 }
114}
115
116/// Detection summary.
117#[derive(Debug, Clone)]
118pub struct DetectionSummary {
119 /// Number of leaks
120 pub leak_count: usize,
121 /// Total leaked bytes
122 pub leaked_bytes: usize,
123 /// Number of UAF issues
124 pub uaf_count: usize,
125 /// Number of safety issues
126 pub safety_issues: usize,
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::event_store::MemoryEvent;
133
134 /// Objective: Verify that a still-live allocation is reported as "not released"
135 /// and *not* as a leak (plan A02).
136 /// Invariants: one unreleased allocation of 128 bytes, zero leak verdicts.
137 #[test]
138 fn test_live_allocation_is_not_a_leak() {
139 let events = vec![
140 MemoryEvent::allocate(0x1000, 64, 1),
141 MemoryEvent::allocate(0x2000, 128, 2),
142 MemoryEvent::deallocate(0x1000, 64, 1),
143 ];
144 let view = MemoryView::from_events(events);
145 let analysis = DetectionAnalysis::from_view(&view);
146
147 let unreleased = analysis.unreleased();
148 assert_eq!(
149 unreleased.len(),
150 1,
151 "one allocation is still live at the cut"
152 );
153 assert_eq!(
154 unreleased[0].size, 128,
155 "the unreleased allocation keeps its recorded size"
156 );
157
158 let leaks = analysis.leaks();
159 assert_eq!(
160 leaks.leak_count, 0,
161 "being alive at the cut is not evidence of a leak"
162 );
163 assert_eq!(
164 leaks.total_leaked_bytes, 0,
165 "no leaked bytes may be reported without an assessment"
166 );
167 }
168
169 #[test]
170 fn test_no_leaks() {
171 let events = vec![
172 MemoryEvent::allocate(0x1000, 64, 1),
173 MemoryEvent::deallocate(0x1000, 64, 1),
174 ];
175 let view = MemoryView::from_events(events);
176 let analysis = DetectionAnalysis::from_view(&view);
177 let leaks = analysis.leaks();
178 assert_eq!(leaks.leak_count, 0);
179 }
180
181 #[test]
182 fn test_uaf_returns_empty() {
183 // UAF detection is not implemented, should return empty report
184 let events = vec![
185 MemoryEvent::allocate(0x1000, 64, 1),
186 MemoryEvent::deallocate(0x1000, 64, 1),
187 ];
188 let view = MemoryView::from_events(events);
189 let analysis = DetectionAnalysis::from_view(&view);
190 let uaf = analysis.uaf();
191 assert_eq!(uaf.uaf_count, 0);
192 }
193}