Skip to main content

memf_linux/
perf_event.rs

1//! Suspicious `perf_event` detection for Linux memory forensics.
2//!
3//! Walks each process's `perf_event_context` (via `task_struct.perf_event_ctxp[0]`)
4//! and enumerates all attached `perf_event` structs. Hardware cache events and raw
5//! PMU accesses are flagged as suspicious (Spectre/cache-timing attack patterns).
6
7use memf_core::object_reader::ObjectReader;
8use memf_format::PhysicalMemoryProvider;
9
10use crate::Result;
11
12/// Information about a single perf_event attached to a process.
13#[derive(Debug, Clone, serde::Serialize)]
14pub struct PerfEventInfo {
15    /// PID of the owning process.
16    pub pid: u32,
17    /// Command name of the owning process.
18    pub comm: String,
19    /// PERF_TYPE_* constant.
20    pub event_type: u32,
21    /// Human-readable name for `event_type`.
22    pub event_type_name: String,
23    /// Event configuration (e.g. `PERF_COUNT_HW_CACHE_MISSES`).
24    pub config: u64,
25    /// Sample period set on the event.
26    pub sample_period: u64,
27    /// True when this event matches cache-side-channel or PMU-based attack patterns.
28    pub is_suspicious: bool,
29}
30
31/// Map a `PERF_TYPE_*` constant to a human-readable name.
32pub fn perf_type_name(t: u32) -> &'static str {
33    match t {
34        0 => "HARDWARE",
35        1 => "SOFTWARE",
36        2 => "TRACEPOINT",
37        3 => "HW_CACHE",
38        4 => "RAW",
39        5 => "BREAKPOINT",
40        _ => "UNKNOWN",
41    }
42}
43
44/// Classify whether a perf_event represents a suspicious access pattern.
45///
46/// - `PERF_TYPE_HW_CACHE` (3) with config low byte <= 2 (L1D or LL cache) is
47///   a known pattern used in cache-timing / Spectre attacks.
48/// - `PERF_TYPE_RAW` (4) gives direct PMU counter access from userspace and is
49///   always considered suspicious.
50pub use crate::heuristics::classify_perf_event;
51
52/// Walk all perf_events across all processes and return structured info.
53///
54/// Returns `Ok(Vec::new())` when `init_task` symbol or required ISF offsets
55/// are absent (graceful degradation).
56pub fn walk_perf_events<P: PhysicalMemoryProvider>(
57    reader: &ObjectReader<P>,
58) -> Result<Vec<PerfEventInfo>> {
59    // Graceful degradation: require init_task symbol.
60    let init_task_addr = match reader.symbols().symbol_address("init_task") {
61        Some(addr) => addr,
62        None => return Ok(Vec::new()),
63    };
64
65    // Require task_struct.tasks offset for process-list traversal.
66    let tasks_offset = match reader.symbols().field_offset("task_struct", "tasks") {
67        Some(off) => off,
68        None => return Ok(Vec::new()),
69    };
70
71    // Require perf_event_ctxp offset for perf context pointer array.
72    let ctxp_offset = match reader
73        .symbols()
74        .field_offset("task_struct", "perf_event_ctxp")
75    {
76        Some(off) => off,
77        None => return Ok(Vec::new()),
78    };
79
80    let mut results = Vec::new();
81
82    // Collect all task_struct addresses by walking the tasks list_head.
83    let mut task_addrs: Vec<u64> = Vec::new();
84    {
85        let first_next: u64 = match reader.read_field(init_task_addr, "task_struct", "tasks") {
86            Ok(v) => v,
87            Err(_) => return Ok(Vec::new()),
88        };
89        let mut cursor = first_next;
90        let mut guard = 0usize;
91        loop {
92            if cursor == 0 || guard > 65536 {
93                break;
94            }
95            let task_addr = cursor.saturating_sub(tasks_offset);
96            if task_addr == init_task_addr {
97                break;
98            }
99            task_addrs.push(task_addr);
100            cursor = match reader.read_field(cursor, "list_head", "next") {
101                Ok(v) => v,
102                Err(_) => break,
103            };
104            guard += 1;
105        }
106    }
107
108    // Include init_task itself.
109    let all_tasks = std::iter::once(init_task_addr).chain(task_addrs);
110
111    for task_addr in all_tasks {
112        let pid: u32 = reader
113            .read_field::<u32>(task_addr, "task_struct", "pid")
114            .unwrap_or(0);
115        let comm_bytes: [u8; 16] = reader
116            .read_field(task_addr, "task_struct", "comm")
117            .unwrap_or([0u8; 16]);
118        let comm = std::str::from_utf8(&comm_bytes)
119            .unwrap_or("")
120            .trim_end_matches('\0')
121            .to_string();
122
123        // Read perf_event_ctxp[0]: pointer stored at task_addr + ctxp_offset.
124        let ctx_ptr_addr = task_addr + ctxp_offset;
125        let ctx_ptr: u64 = match reader.read_bytes(ctx_ptr_addr, 8) {
126            Ok(bytes) => u64::from_le_bytes(bytes.try_into().unwrap_or([0u8; 8])),
127            Err(_) => continue,
128        };
129        if ctx_ptr == 0 {
130            continue;
131        }
132
133        // Walk pinned_groups and flexible_groups list_heads of perf_event_context.
134        for group_field in &["pinned_groups", "flexible_groups"] {
135            let head_addr = match reader
136                .symbols()
137                .field_offset("perf_event_context", group_field)
138            {
139                Some(off) => ctx_ptr + off,
140                None => continue,
141            };
142
143            let first_event_list: u64 = match reader.read_bytes(head_addr, 8) {
144                Ok(b) => u64::from_le_bytes(b.try_into().unwrap_or([0u8; 8])),
145                Err(_) => continue,
146            };
147
148            let event_group_node_offset =
149                match reader.symbols().field_offset("perf_event", "group_entry") {
150                    Some(off) => off,
151                    None => continue,
152                };
153
154            let mut cursor = first_event_list;
155            let mut guard = 0usize;
156            loop {
157                if cursor == 0 || cursor == head_addr || guard > 4096 {
158                    break;
159                }
160                let event_addr = cursor.saturating_sub(event_group_node_offset);
161
162                // perf_event.attr is embedded at ~0x20; type at attr+0, config at attr+8.
163                let attr_offset: u64 = reader
164                    .symbols()
165                    .field_offset("perf_event", "attr")
166                    .map_or(0x20, |o| o);
167
168                let event_type: u32 = if let Ok(b) = reader.read_bytes(event_addr + attr_offset, 4)
169                {
170                    u32::from_le_bytes(b.try_into().unwrap_or([0u8; 4]))
171                } else {
172                    cursor = match reader.read_bytes(cursor, 8) {
173                        Ok(b) => u64::from_le_bytes(b.try_into().unwrap_or([0u8; 8])),
174                        Err(_) => break,
175                    };
176                    guard += 1;
177                    continue;
178                };
179
180                let config: u64 = reader
181                    .read_bytes(event_addr + attr_offset + 8, 8)
182                    .ok()
183                    .and_then(|b| b.try_into().ok())
184                    .map_or(0, u64::from_le_bytes);
185
186                let sample_period: u64 = reader
187                    .read_bytes(event_addr + attr_offset + 16, 8)
188                    .ok()
189                    .and_then(|b| b.try_into().ok())
190                    .map_or(0, u64::from_le_bytes);
191
192                let is_suspicious = classify_perf_event(event_type, config);
193                results.push(PerfEventInfo {
194                    pid,
195                    comm: comm.clone(),
196                    event_type,
197                    event_type_name: perf_type_name(event_type).to_string(),
198                    config,
199                    sample_period,
200                    is_suspicious,
201                });
202
203                cursor = match reader.read_bytes(cursor, 8) {
204                    Ok(b) => u64::from_le_bytes(b.try_into().unwrap_or([0u8; 8])),
205                    Err(_) => break,
206                };
207                guard += 1;
208            }
209        }
210    }
211
212    Ok(results)
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use memf_core::object_reader::ObjectReader;
219    use memf_core::test_builders::{PageTableBuilder, SyntheticPhysMem};
220    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
221    use memf_symbols::isf::IsfResolver;
222    use memf_symbols::test_builders::IsfBuilder;
223
224    fn make_no_symbol_reader() -> ObjectReader<SyntheticPhysMem> {
225        let isf = IsfBuilder::new().build_json();
226        let resolver = IsfResolver::from_value(&isf).unwrap();
227        let (cr3, mem) = PageTableBuilder::new().build();
228        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
229        ObjectReader::new(vas, Box::new(resolver))
230    }
231
232    #[test]
233    fn perf_type_name_hardware() {
234        assert_eq!(perf_type_name(0), "HARDWARE");
235    }
236
237    #[test]
238    fn perf_type_name_unknown() {
239        assert_eq!(perf_type_name(99), "UNKNOWN");
240    }
241
242    #[test]
243    fn classify_ll_cache_event_suspicious() {
244        // PERF_TYPE_HW_CACHE (3), config low byte = 2 (PERF_COUNT_HW_CACHE_LL)
245        assert!(
246            classify_perf_event(3, 2),
247            "LL cache event must be suspicious"
248        );
249    }
250
251    #[test]
252    fn classify_l1d_cache_event_suspicious() {
253        // PERF_TYPE_HW_CACHE (3), config low byte = 0 (PERF_COUNT_HW_CACHE_L1D)
254        assert!(
255            classify_perf_event(3, 0),
256            "L1D cache event must be suspicious"
257        );
258    }
259
260    #[test]
261    fn classify_software_event_not_suspicious() {
262        assert!(
263            !classify_perf_event(1, 0),
264            "SOFTWARE event must not be suspicious"
265        );
266    }
267
268    #[test]
269    fn classify_raw_pmu_event_suspicious() {
270        assert!(
271            classify_perf_event(4, 0xDEAD),
272            "RAW PMU event must be suspicious"
273        );
274    }
275
276    #[test]
277    fn classify_hardware_event_not_suspicious() {
278        assert!(
279            !classify_perf_event(0, 1),
280            "plain HARDWARE event must not be suspicious"
281        );
282    }
283
284    #[test]
285    fn walk_perf_events_no_symbol_returns_empty() {
286        let reader = make_no_symbol_reader();
287        let result = walk_perf_events(&reader).unwrap();
288        assert!(
289            result.is_empty(),
290            "no init_task symbol → empty vec expected"
291        );
292    }
293
294    // --- perf_type_name exhaustive coverage ---
295
296    #[test]
297    fn perf_type_name_software() {
298        assert_eq!(perf_type_name(1), "SOFTWARE");
299    }
300
301    #[test]
302    fn perf_type_name_tracepoint() {
303        assert_eq!(perf_type_name(2), "TRACEPOINT");
304    }
305
306    #[test]
307    fn perf_type_name_hw_cache() {
308        assert_eq!(perf_type_name(3), "HW_CACHE");
309    }
310
311    #[test]
312    fn perf_type_name_raw() {
313        assert_eq!(perf_type_name(4), "RAW");
314    }
315
316    #[test]
317    fn perf_type_name_breakpoint() {
318        assert_eq!(perf_type_name(5), "BREAKPOINT");
319    }
320
321    // --- classify_perf_event boundary and branch coverage ---
322
323    #[test]
324    fn classify_hw_cache_config_byte_1_suspicious() {
325        // PERF_TYPE_HW_CACHE (3), config low byte = 1 (PERF_COUNT_HW_CACHE_L1I)
326        assert!(
327            classify_perf_event(3, 1),
328            "HW_CACHE with config=1 must be suspicious"
329        );
330    }
331
332    #[test]
333    fn classify_hw_cache_config_byte_3_not_suspicious() {
334        // low byte = 3 is > 2, so NOT suspicious
335        assert!(
336            !classify_perf_event(3, 3),
337            "HW_CACHE with config byte = 3 must not be suspicious"
338        );
339    }
340
341    #[test]
342    fn classify_hw_cache_config_high_byte_not_suspicious() {
343        // config = 0x0300 → low byte = 0, which IS <= 2 (suspicious)
344        // But config = 0xFF03 → low byte = 3, not suspicious
345        assert!(
346            !classify_perf_event(3, 0xFF03),
347            "HW_CACHE with low byte > 2 must not be suspicious"
348        );
349    }
350
351    #[test]
352    fn classify_tracepoint_not_suspicious() {
353        assert!(
354            !classify_perf_event(2, 0),
355            "TRACEPOINT event must not be suspicious"
356        );
357    }
358
359    #[test]
360    fn classify_breakpoint_not_suspicious() {
361        assert!(
362            !classify_perf_event(5, 0),
363            "BREAKPOINT event must not be suspicious"
364        );
365    }
366
367    #[test]
368    fn classify_unknown_type_not_suspicious() {
369        assert!(
370            !classify_perf_event(99, 0),
371            "unknown event type must not be suspicious"
372        );
373    }
374
375    // --- walk_perf_events: has init_task but no tasks field ---
376
377    #[test]
378    fn walk_perf_events_missing_tasks_offset_returns_empty() {
379        let isf = IsfBuilder::new()
380            .add_symbol("init_task", 0xFFFF_8888_0000_0000)
381            .build_json();
382        let resolver = IsfResolver::from_value(&isf).unwrap();
383        let (cr3, mem) = PageTableBuilder::new().build();
384        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
385        let reader = ObjectReader::new(vas, Box::new(resolver));
386
387        let result = walk_perf_events(&reader).unwrap();
388        assert!(
389            result.is_empty(),
390            "missing task_struct.tasks offset → empty vec expected"
391        );
392    }
393
394    // --- walk_perf_events: has init_task + tasks but no perf_event_ctxp ---
395
396    #[test]
397    fn walk_perf_events_missing_ctxp_offset_returns_empty() {
398        let isf = IsfBuilder::new()
399            .add_symbol("init_task", 0xFFFF_8888_0000_0000)
400            .add_struct("task_struct", 512)
401            .add_field("task_struct", "tasks", 8, "pointer")
402            .build_json();
403        let resolver = IsfResolver::from_value(&isf).unwrap();
404        let (cr3, mem) = PageTableBuilder::new().build();
405        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
406        let reader = ObjectReader::new(vas, Box::new(resolver));
407
408        let result = walk_perf_events(&reader).unwrap();
409        assert!(
410            result.is_empty(),
411            "missing perf_event_ctxp offset → empty vec expected"
412        );
413    }
414
415    // --- walk_perf_events: all symbols present, self-pointing tasks list → empty ---
416    // Exercises the task-list traversal body and the perf-context branch.
417    #[test]
418    fn walk_perf_events_symbol_present_self_pointing_list_returns_empty() {
419        use memf_core::test_builders::{flags as ptf, SyntheticPhysMem};
420
421        // tasks field is at offset 0x10; perf_event_ctxp is at offset 0x20.
422        // init_task.tasks.next must point back to init_task_addr + tasks_offset
423        // so that the loop's "task_addr == init_task_addr" guard fires immediately.
424        let tasks_offset: u64 = 0x10;
425        let ctxp_offset: u64 = 0x20;
426
427        let sym_vaddr: u64 = 0xFFFF_8800_0020_0000;
428        let sym_paddr: u64 = 0x0040_0000; // unique paddr, < 16 MB
429
430        let isf = IsfBuilder::new()
431            .add_symbol("init_task", sym_vaddr)
432            .add_struct("task_struct", 0x400)
433            .add_field("task_struct", "tasks", tasks_offset, "pointer")
434            .add_field("task_struct", "perf_event_ctxp", ctxp_offset, "pointer")
435            .add_field("task_struct", "pid", 0x30, "unsigned int")
436            .add_field("task_struct", "comm", 0x38, "char")
437            .build_json();
438        let resolver = IsfResolver::from_value(&isf).unwrap();
439
440        // Build a page for init_task:
441        // [tasks_offset+0..+8] = init_task_vaddr + tasks_offset  (self-pointing → empty list)
442        // [ctxp_offset+0..+8]  = 0  (null ctx_ptr → loop skips perf context)
443        let mut page = [0u8; 4096];
444        let self_ptr = sym_vaddr + tasks_offset;
445        page[tasks_offset as usize..tasks_offset as usize + 8]
446            .copy_from_slice(&self_ptr.to_le_bytes());
447        // ctxp already zero
448
449        let (cr3, mem) = PageTableBuilder::new()
450            .map_4k(sym_vaddr, sym_paddr, ptf::WRITABLE)
451            .write_phys(sym_paddr, &page)
452            .build();
453
454        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
455        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
456
457        let result = walk_perf_events(&reader).unwrap();
458        assert!(
459            result.is_empty(),
460            "self-pointing tasks list with null ctx_ptr → no perf events"
461        );
462    }
463
464    // --- walk_perf_events: non-null ctx_ptr, missing pinned_groups field → continues ---
465    // Exercises the `continue` branch in the group_field loop when
466    // perf_event_context.pinned_groups/flexible_groups offset is absent.
467    #[test]
468    fn walk_perf_events_missing_group_field_offsets_returns_empty() {
469        use memf_core::test_builders::{flags as ptf, SyntheticPhysMem};
470
471        let task_vaddr: u64 = 0xFFFF_8800_0030_0000;
472        let ctx_vaddr: u64 = 0xFFFF_8800_0031_0000;
473
474        let task_paddr: u64 = 0x041_000;
475        let ctx_paddr: u64 = 0x042_000;
476
477        let tasks_offset: u64 = 0x10;
478        let ctxp_offset: u64 = 0x20;
479        let pid_offset: u64 = 0x30;
480        let comm_offset: u64 = 0x38;
481
482        let isf = IsfBuilder::new()
483            .add_symbol("init_task", task_vaddr)
484            .add_struct("task_struct", 0x400)
485            .add_field("task_struct", "tasks", tasks_offset, "pointer")
486            .add_field("task_struct", "perf_event_ctxp", ctxp_offset, "pointer")
487            .add_field("task_struct", "pid", pid_offset, "unsigned int")
488            .add_field("task_struct", "comm", comm_offset, "char")
489            // perf_event_context has NO pinned_groups or flexible_groups fields
490            // → the inner loop continues immediately for both field names
491            .add_struct("perf_event_context", 0x200)
492            .build_json();
493        let resolver = IsfResolver::from_value(&isf).unwrap();
494
495        // init_task page: self-pointing tasks, ctxp = ctx_vaddr
496        let mut task_page = [0u8; 4096];
497        let self_ptr = task_vaddr + tasks_offset;
498        task_page[tasks_offset as usize..tasks_offset as usize + 8]
499            .copy_from_slice(&self_ptr.to_le_bytes());
500        task_page[ctxp_offset as usize..ctxp_offset as usize + 8]
501            .copy_from_slice(&ctx_vaddr.to_le_bytes());
502        task_page[pid_offset as usize..pid_offset as usize + 4]
503            .copy_from_slice(&777u32.to_le_bytes());
504
505        // ctx page: all zeros (no events)
506        let ctx_page = [0u8; 4096];
507
508        let (cr3, mem) = PageTableBuilder::new()
509            .map_4k(task_vaddr, task_paddr, ptf::WRITABLE)
510            .write_phys(task_paddr, &task_page)
511            .map_4k(ctx_vaddr, ctx_paddr, ptf::WRITABLE)
512            .write_phys(ctx_paddr, &ctx_page)
513            .build();
514
515        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
516        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
517
518        let result = walk_perf_events(&reader).unwrap();
519        assert!(
520            result.is_empty(),
521            "missing group field offsets → inner loop continues → no events"
522        );
523    }
524
525    // --- walk_perf_events: non-null ctx_ptr, pinned_groups present, empty group list ---
526    // Exercises reading head_addr + first_event_list, then breaking because
527    // cursor == head_addr immediately (self-pointing or zero list head).
528    #[test]
529    fn walk_perf_events_empty_group_list_returns_empty() {
530        use memf_core::test_builders::{flags as ptf, SyntheticPhysMem};
531
532        let task_vaddr: u64 = 0xFFFF_8800_0032_0000;
533        let ctx_vaddr: u64 = 0xFFFF_8800_0033_0000;
534
535        let task_paddr: u64 = 0x043_000;
536        let ctx_paddr: u64 = 0x044_000;
537
538        let tasks_offset: u64 = 0x10;
539        let ctxp_offset: u64 = 0x20;
540        let pid_offset: u64 = 0x30;
541        let comm_offset: u64 = 0x38;
542
543        // perf_event_context: pinned_groups@0x10, flexible_groups@0x18
544        let pinned_offset: u64 = 0x10;
545        let flexible_offset: u64 = 0x18;
546
547        let isf = IsfBuilder::new()
548            .add_symbol("init_task", task_vaddr)
549            .add_struct("task_struct", 0x400)
550            .add_field("task_struct", "tasks", tasks_offset, "pointer")
551            .add_field("task_struct", "perf_event_ctxp", ctxp_offset, "pointer")
552            .add_field("task_struct", "pid", pid_offset, "unsigned int")
553            .add_field("task_struct", "comm", comm_offset, "char")
554            .add_struct("perf_event_context", 0x200)
555            .add_field(
556                "perf_event_context",
557                "pinned_groups",
558                pinned_offset,
559                "list_head",
560            )
561            .add_field(
562                "perf_event_context",
563                "flexible_groups",
564                flexible_offset,
565                "list_head",
566            )
567            .add_struct("perf_event", 0x200)
568            .add_field("perf_event", "group_entry", 0u64, "list_head")
569            .build_json();
570        let resolver = IsfResolver::from_value(&isf).unwrap();
571
572        let mut task_page = [0u8; 4096];
573        let self_ptr = task_vaddr + tasks_offset;
574        task_page[tasks_offset as usize..tasks_offset as usize + 8]
575            .copy_from_slice(&self_ptr.to_le_bytes());
576        task_page[ctxp_offset as usize..ctxp_offset as usize + 8]
577            .copy_from_slice(&ctx_vaddr.to_le_bytes());
578        task_page[pid_offset as usize..pid_offset as usize + 4]
579            .copy_from_slice(&888u32.to_le_bytes());
580
581        // ctx page: pinned_groups list head points to itself (empty list)
582        let pinned_head = ctx_vaddr + pinned_offset;
583        let flexible_head = ctx_vaddr + flexible_offset;
584        let mut ctx_page = [0u8; 4096];
585        ctx_page[pinned_offset as usize..pinned_offset as usize + 8]
586            .copy_from_slice(&pinned_head.to_le_bytes());
587        ctx_page[flexible_offset as usize..flexible_offset as usize + 8]
588            .copy_from_slice(&flexible_head.to_le_bytes());
589
590        let (cr3, mem) = PageTableBuilder::new()
591            .map_4k(task_vaddr, task_paddr, ptf::WRITABLE)
592            .write_phys(task_paddr, &task_page)
593            .map_4k(ctx_vaddr, ctx_paddr, ptf::WRITABLE)
594            .write_phys(ctx_paddr, &ctx_page)
595            .build();
596
597        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
598        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
599
600        let result = walk_perf_events(&reader).unwrap();
601        assert!(
602            result.is_empty(),
603            "self-pointing group list (empty) → no perf events enumerated"
604        );
605    }
606
607    // --- walk_perf_events: init_task tasks read fails → returns empty ---
608    // Exercises line 91-93: read_field(init_task_addr, "task_struct", "tasks") Err → Ok(Vec::new()).
609    #[test]
610    fn walk_perf_events_tasks_read_fails_returns_empty() {
611        // init_task symbol present, tasks+ctxp offsets resolved, but the address is unmapped
612        // so reading tasks pointer fails → early return Ok(Vec::new()).
613        let isf = IsfBuilder::new()
614            .add_symbol("init_task", 0xFFFF_DEAD_CAFE_0000) // unmapped
615            .add_struct("task_struct", 0x400)
616            .add_field("task_struct", "tasks", 0x10, "pointer")
617            .add_field("task_struct", "perf_event_ctxp", 0x20, "pointer")
618            .add_field("task_struct", "pid", 0x30, "unsigned int")
619            .add_field("task_struct", "comm", 0x38, "char")
620            .build_json();
621        let resolver = IsfResolver::from_value(&isf).unwrap();
622        let (cr3, mem) = PageTableBuilder::new().build();
623        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
624        let reader = ObjectReader::new(vas, Box::new(resolver));
625
626        let result = walk_perf_events(&reader).unwrap();
627        assert!(
628            result.is_empty(),
629            "unreadable init_task → early return empty"
630        );
631    }
632
633    // --- walk_perf_events: non-empty tasks list AND event in group list ---
634    // Exercises the task-list walking loop body (lines 97-111) by providing a
635    // second task in the list, AND exercises the inner perf_event_context group
636    // traversal body (lines 151-204) by placing one event in the pinned_groups list.
637    #[test]
638    fn walk_perf_events_one_task_with_one_event_in_pinned_groups() {
639        use memf_core::test_builders::{flags as ptf, SyntheticPhysMem};
640
641        // Memory layout (all physical addresses < 16 MB):
642        //
643        //  init_task @ init_vaddr / init_paddr
644        //  task2     @ t2_vaddr   / t2_paddr      (linked in tasks list)
645        //  ctx       @ ctx_vaddr  / ctx_paddr      (perf_event_context for task2)
646        //  event     @ ev_vaddr   / ev_paddr       (perf_event in pinned_groups)
647        //
648        // tasks list offsets (inside task_struct):
649        //   tasks         @ 0x10  (list_head, 8-byte next pointer)
650        //   perf_event_ctxp @ 0x20 (pointer to ctx)
651        //   pid           @ 0x30  (u32)
652        //   comm          @ 0x38  (16 bytes)
653        //
654        // perf_event_context offsets:
655        //   pinned_groups @ 0x00  (list_head — next pointer is first 8 bytes)
656        //   flexible_groups @ 0x08
657        //
658        // perf_event offsets:
659        //   group_entry @ 0x00  (list_head — node for pinned/flexible list)
660        //   attr        @ 0x20  (perf_event_attr; type@+0 u32, config@+8 u64, sample_period@+16 u64)
661
662        let tasks_offset: u64 = 0x10;
663        let ctxp_offset: u64 = 0x20;
664        let pid_offset: u64 = 0x30;
665        let comm_offset: u64 = 0x38;
666
667        let pinned_offset: u64 = 0x00;
668        let flexible_offset: u64 = 0x08;
669
670        let group_entry_offset: u64 = 0x00;
671        let attr_offset: u64 = 0x20;
672
673        let init_vaddr: u64 = 0xFFFF_8800_0080_0000;
674        let init_paddr: u64 = 0x0080_0000;
675        let t2_vaddr: u64 = 0xFFFF_8800_0081_0000;
676        let t2_paddr: u64 = 0x0081_0000;
677        let ctx_vaddr: u64 = 0xFFFF_8800_0082_0000;
678        let ctx_paddr: u64 = 0x0082_0000;
679        let ev_vaddr: u64 = 0xFFFF_8800_0083_0000;
680        let ev_paddr: u64 = 0x0083_0000;
681
682        // init_task page:
683        //   tasks.next @ tasks_offset → t2_vaddr + tasks_offset  (points to task2's list node)
684        //   ctxp       @ ctxp_offset  → 0  (no perf context for init)
685        //   pid        @ pid_offset   → 0
686        let mut init_page = [0u8; 4096];
687        let t2_list_node = t2_vaddr + tasks_offset;
688        init_page[tasks_offset as usize..tasks_offset as usize + 8]
689            .copy_from_slice(&t2_list_node.to_le_bytes());
690        // ctxp already 0
691
692        // task2 page:
693        //   tasks.next @ tasks_offset → init_vaddr + tasks_offset (wraps back → end of walk)
694        //   ctxp       @ ctxp_offset  → ctx_vaddr
695        //   pid        @ pid_offset   → 42
696        //   comm       @ comm_offset  → "spy\0"
697        let mut t2_page = [0u8; 4096];
698        let init_list_node = init_vaddr + tasks_offset;
699        t2_page[tasks_offset as usize..tasks_offset as usize + 8]
700            .copy_from_slice(&init_list_node.to_le_bytes());
701        t2_page[ctxp_offset as usize..ctxp_offset as usize + 8]
702            .copy_from_slice(&ctx_vaddr.to_le_bytes());
703        t2_page[pid_offset as usize..pid_offset as usize + 4].copy_from_slice(&42u32.to_le_bytes());
704        t2_page[comm_offset as usize..comm_offset as usize + 3].copy_from_slice(b"spy");
705
706        // perf_event_context page:
707        //   pinned_groups (list_head) @ pinned_offset:
708        //     next = ev_vaddr + group_entry_offset   (the event's group_entry node)
709        //   flexible_groups @ flexible_offset:
710        //     next = ctx_vaddr + flexible_offset     (self-pointer → empty flexible list)
711        let mut ctx_page = [0u8; 4096];
712        let ev_list_node = ev_vaddr + group_entry_offset;
713        ctx_page[pinned_offset as usize..pinned_offset as usize + 8]
714            .copy_from_slice(&ev_list_node.to_le_bytes());
715        // When the inner loop advances from the event node, it reads cursor (ev_list_node)
716        // first 8 bytes, which will be the event page offset 0 = the next in group_entry.
717        // We make that point back to the pinned_groups head so the loop exits.
718        // pinned head addr = ctx_vaddr + pinned_offset
719        let pinned_head = ctx_vaddr + pinned_offset;
720        let flex_head = ctx_vaddr + flexible_offset;
721        ctx_page[flexible_offset as usize..flexible_offset as usize + 8]
722            .copy_from_slice(&flex_head.to_le_bytes());
723
724        // perf_event page:
725        //   group_entry (list_head) @ group_entry_offset:
726        //     first 8 bytes (next pointer) = pinned_head  (so loop exits after this event)
727        //   attr @ attr_offset:
728        //     type (u32 @ +0) = 4  (PERF_TYPE_RAW → suspicious)
729        //     config (u64 @ +8) = 0xDEAD
730        //     sample_period (u64 @ +16) = 1000
731        let mut ev_page = [0u8; 4096];
732        ev_page[group_entry_offset as usize..group_entry_offset as usize + 8]
733            .copy_from_slice(&pinned_head.to_le_bytes());
734        ev_page[attr_offset as usize..attr_offset as usize + 4]
735            .copy_from_slice(&4u32.to_le_bytes()); // PERF_TYPE_RAW
736        ev_page[attr_offset as usize + 8..attr_offset as usize + 16]
737            .copy_from_slice(&0xDEADu64.to_le_bytes());
738        ev_page[attr_offset as usize + 16..attr_offset as usize + 24]
739            .copy_from_slice(&1000u64.to_le_bytes());
740
741        let isf = IsfBuilder::new()
742            .add_symbol("init_task", init_vaddr)
743            // list_head required by walk_list internals
744            .add_struct("list_head", 0x10)
745            .add_field("list_head", "next", 0x00u64, "pointer")
746            .add_struct("task_struct", 0x400)
747            .add_field("task_struct", "tasks", tasks_offset, "pointer")
748            .add_field("task_struct", "perf_event_ctxp", ctxp_offset, "pointer")
749            .add_field("task_struct", "pid", pid_offset, "unsigned int")
750            .add_field("task_struct", "comm", comm_offset, "char")
751            .add_struct("perf_event_context", 0x200)
752            .add_field(
753                "perf_event_context",
754                "pinned_groups",
755                pinned_offset,
756                "list_head",
757            )
758            .add_field(
759                "perf_event_context",
760                "flexible_groups",
761                flexible_offset,
762                "list_head",
763            )
764            .add_struct("perf_event", 0x200)
765            .add_field("perf_event", "group_entry", group_entry_offset, "list_head")
766            .add_field("perf_event", "attr", attr_offset, "pointer")
767            .build_json();
768        let resolver = IsfResolver::from_value(&isf).unwrap();
769
770        let (cr3, mem) = PageTableBuilder::new()
771            .map_4k(init_vaddr, init_paddr, ptf::WRITABLE)
772            .write_phys(init_paddr, &init_page)
773            .map_4k(t2_vaddr, t2_paddr, ptf::WRITABLE)
774            .write_phys(t2_paddr, &t2_page)
775            .map_4k(ctx_vaddr, ctx_paddr, ptf::WRITABLE)
776            .write_phys(ctx_paddr, &ctx_page)
777            .map_4k(ev_vaddr, ev_paddr, ptf::WRITABLE)
778            .write_phys(ev_paddr, &ev_page)
779            .build();
780
781        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
782        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
783
784        let result = walk_perf_events(&reader).unwrap();
785        // Must find exactly one event (RAW PMU on task2/pid=42/"spy")
786        assert_eq!(
787            result.len(),
788            1,
789            "expected one perf_event from task2's pinned_groups"
790        );
791        assert_eq!(result[0].pid, 42);
792        assert_eq!(result[0].comm, "spy");
793        assert_eq!(result[0].event_type, 4); // PERF_TYPE_RAW
794        assert_eq!(result[0].config, 0xDEAD);
795        assert_eq!(result[0].sample_period, 1000);
796        assert!(
797            result[0].is_suspicious,
798            "RAW PMU event must be flagged suspicious"
799        );
800    }
801
802    // --- walk_perf_events: PerfEventInfo serialization ---
803    #[test]
804    fn perf_event_info_serializes() {
805        let info = PerfEventInfo {
806            pid: 12,
807            comm: "spy".to_string(),
808            event_type: 4,
809            event_type_name: "RAW".to_string(),
810            config: 0xDEAD,
811            sample_period: 1000,
812            is_suspicious: true,
813        };
814        let json = serde_json::to_string(&info).unwrap();
815        assert!(json.contains("\"pid\":12"));
816        assert!(json.contains("RAW"));
817        assert!(json.contains("\"is_suspicious\":true"));
818    }
819}