Skip to main content

memf_linux/
bpf.rs

1//! Linux eBPF program enumeration from kernel memory.
2//!
3//! eBPF is a modern rootkit vector -- malicious BPF programs can intercept
4//! syscalls, modify network packets, hide processes, and exfiltrate data.
5//! The kernel tracks BPF programs via `bpf_prog_idr` (an IDR/radix tree).
6//! This module enumerates loaded eBPF programs and flags suspicious ones.
7
8use memf_core::object_reader::ObjectReader;
9use memf_format::PhysicalMemoryProvider;
10
11use crate::{Error, Result};
12
13/// Information about a loaded eBPF program extracted from kernel memory.
14#[derive(Debug, Clone, serde::Serialize)]
15pub struct BpfProgramInfo {
16    /// Unique program ID (`aux->id`).
17    pub id: u32,
18    /// Program type (kprobe, tracepoint, xdp, socket_filter, etc.).
19    pub prog_type: String,
20    /// Program name (`aux->name`), if set.
21    pub name: String,
22    /// 8-byte hash of the bytecode.
23    pub tag: [u8; 8],
24    /// Number of BPF instructions.
25    pub insn_count: u32,
26    /// JIT compiled size in bytes.
27    pub jited_len: u32,
28    /// UID that loaded the program.
29    pub loaded_by_uid: u32,
30    /// Whether heuristic analysis flags this program as suspicious.
31    pub is_suspicious: bool,
32}
33
34/// Known BPF program type values from the kernel's `enum bpf_prog_type`.
35const BPF_PROG_TYPES: &[&str] = &[
36    "unspec",
37    "socket_filter",
38    "kprobe",
39    "sched_cls",
40    "sched_act",
41    "tracepoint",
42    "xdp",
43    "perf_event",
44    "cgroup_skb",
45    "cgroup_sock",
46    "lwt_in",
47    "lwt_out",
48    "lwt_xmit",
49    "sock_ops",
50    "sk_skb",
51    "cgroup_device",
52    "sk_msg",
53    "raw_tracepoint",
54    "cgroup_sock_addr",
55    "lwt_seg6local",
56    "lirc_mode2",
57    "sk_reuseport",
58    "flow_dissector",
59    "cgroup_sysctl",
60    "raw_tracepoint_writable",
61    "cgroup_sockopt",
62    "tracing",
63    "struct_ops",
64    "ext",
65    "lsm",
66    "sk_lookup",
67    "syscall",
68];
69
70/// Convert a raw `bpf_prog_type` enum value to its string name.
71fn prog_type_name(raw: u32) -> String {
72    BPF_PROG_TYPES
73        .get(raw as usize)
74        .map_or_else(|| format!("unknown({raw})"), |s| (*s).to_string())
75}
76
77/// Enumerate loaded eBPF programs by walking `bpf_prog_idr` in kernel memory.
78///
79/// If the `bpf_prog_idr` symbol is not found (e.g., BPF not enabled in the
80/// kernel or symbol table incomplete), returns an empty `Vec`.
81pub fn walk_bpf_programs<P: PhysicalMemoryProvider>(
82    reader: &ObjectReader<P>,
83) -> Result<Vec<BpfProgramInfo>> {
84    // Look up the bpf_prog_idr symbol; if absent, BPF is not available.
85    let Some(idr_addr) = reader.symbols().symbol_address("bpf_prog_idr") else {
86        return Ok(Vec::new());
87    };
88
89    // The IDR stores pointers to bpf_prog structs in a radix tree.
90    // Read idr.idr_rt.xa_head to get the root of the xarray/radix tree.
91    let xa_head: u64 = reader
92        .read_field(idr_addr, "idr", "idr_rt")
93        .or_else(|_| {
94            // Older kernels: idr.top directly
95            reader.read_field::<u64>(idr_addr, "idr", "top")
96        })
97        .unwrap_or(0);
98
99    if xa_head == 0 {
100        return Ok(Vec::new());
101    }
102
103    // Walk IDR entries. The IDR is backed by a radix tree / xarray.
104    // We attempt to read bpf_prog pointers from the tree nodes.
105    let mut programs = Vec::new();
106    walk_idr_entries(reader, xa_head, &mut programs)?;
107
108    Ok(programs)
109}
110
111/// Recursively walk xarray/radix-tree nodes to find `bpf_prog` pointers.
112///
113/// XArray internal entries have the low bit set; leaf entries are direct
114/// pointers to `bpf_prog` structs (aligned, so low bits are 0).
115fn walk_idr_entries<P: PhysicalMemoryProvider>(
116    reader: &ObjectReader<P>,
117    node_ptr: u64,
118    programs: &mut Vec<BpfProgramInfo>,
119) -> Result<()> {
120    // Safety limit to avoid infinite loops on corrupt memory.
121    const MAX_SLOTS: usize = 64;
122    const MAX_PROGRAMS: usize = 10_000;
123
124    // XArray tags internal nodes with low bit 2 (xa_is_node).
125    let is_node = (node_ptr & 0x3) == 0x2;
126
127    if is_node {
128        // Decode the actual node address (clear tag bits).
129        let real_addr = node_ptr & !0x3;
130
131        // xa_node.slots is an array of pointers. Read up to MAX_SLOTS.
132        let slots_offset = reader
133            .symbols()
134            .field_offset("xa_node", "slots")
135            .unwrap_or(16); // typical offset
136
137        for i in 0..MAX_SLOTS {
138            if programs.len() >= MAX_PROGRAMS {
139                break;
140            }
141            let slot_addr = real_addr + slots_offset + (i as u64) * 8;
142            let slot_val = {
143                let mut buf = [0u8; 8];
144                match reader.vas().read_virt(slot_addr, &mut buf) {
145                    Ok(()) => u64::from_le_bytes(buf),
146                    Err(_) => 0,
147                }
148            };
149            if slot_val == 0 {
150                continue;
151            }
152            walk_idr_entries(reader, slot_val, programs)?;
153        }
154    } else if node_ptr.trailing_zeros() >= 2 && node_ptr > 0x1000 {
155        // Leaf pointer — this should be a bpf_prog struct.
156        if let Ok(info) = read_bpf_prog(reader, node_ptr) {
157            programs.push(info);
158        }
159    }
160    // Other tagged pointers (retry entries, etc.) are skipped.
161
162    Ok(())
163}
164
165/// Read a single `bpf_prog` struct and its associated `bpf_prog_aux`.
166fn read_bpf_prog<P: PhysicalMemoryProvider>(
167    reader: &ObjectReader<P>,
168    prog_addr: u64,
169) -> Result<BpfProgramInfo> {
170    // bpf_prog.type (enum bpf_prog_type, u32)
171    let raw_type: u32 = reader.read_field(prog_addr, "bpf_prog", "type")?;
172    let prog_type = prog_type_name(raw_type);
173
174    // bpf_prog.len (number of BPF instructions)
175    let insn_count: u32 = reader.read_field(prog_addr, "bpf_prog", "len")?;
176
177    // bpf_prog.jited_len
178    let jited_len: u32 = reader
179        .read_field(prog_addr, "bpf_prog", "jited_len")
180        .unwrap_or(0);
181
182    // bpf_prog.tag (8 bytes)
183    let mut tag = [0u8; 8];
184    let tag_offset = reader
185        .symbols()
186        .field_offset("bpf_prog", "tag")
187        .ok_or_else(|| Error::MissingField {
188            struct_name: "bpf_prog".into(),
189            field_name: "tag".into(),
190        })?;
191    if let Ok(bytes) = reader.read_bytes(prog_addr + tag_offset, 8) {
192        tag.copy_from_slice(&bytes[..8]);
193    }
194
195    // bpf_prog.aux (pointer to bpf_prog_aux)
196    let aux_addr: u64 = reader.read_field(prog_addr, "bpf_prog", "aux")?;
197
198    // bpf_prog_aux.id
199    let id: u32 = reader
200        .read_field(aux_addr, "bpf_prog_aux", "id")
201        .unwrap_or(0);
202
203    // bpf_prog_aux.name (BPF_OBJ_NAME_LEN = 16)
204    let name = reader
205        .read_field_string(aux_addr, "bpf_prog_aux", "name", 16)
206        .unwrap_or_default();
207
208    // bpf_prog_aux.uid (kuid_t, effectively u32)
209    let loaded_by_uid: u32 = reader
210        .read_field(aux_addr, "bpf_prog_aux", "uid")
211        .unwrap_or(0);
212
213    let is_suspicious = classify_bpf_program(&prog_type, &name);
214
215    Ok(BpfProgramInfo {
216        id,
217        prog_type,
218        name,
219        tag,
220        insn_count,
221        jited_len,
222        loaded_by_uid,
223        is_suspicious,
224    })
225}
226
227/// Classify whether a BPF program is suspicious based on its type and name.
228///
229/// Returns `true` for:
230/// - `kprobe` programs (can intercept arbitrary kernel functions)
231/// - `tracing` programs with no name (unnamed tracing = evasion)
232/// - `raw_tracepoint` programs with no name
233/// - `raw_tracepoint_writable` programs (can modify tracepoint args)
234///
235/// Note: XDP UID-based checks require external context and are done at the
236/// caller level when `loaded_by_uid` is available on `BpfProgramInfo`.
237pub use crate::heuristics::classify_bpf_program;
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242    use memf_core::test_builders::PageTableBuilder;
243    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
244    use memf_symbols::isf::IsfResolver;
245    use memf_symbols::test_builders::IsfBuilder;
246
247    /// Helper: create an ObjectReader with no `bpf_prog_idr` symbol.
248    fn make_reader_no_bpf_symbol() -> ObjectReader<memf_core::test_builders::SyntheticPhysMem> {
249        let isf = IsfBuilder::new().build_json();
250        let resolver = IsfResolver::from_value(&isf).unwrap();
251        let (cr3, mem) = PageTableBuilder::new().build();
252        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
253        ObjectReader::new(vas, Box::new(resolver))
254    }
255
256    #[test]
257    fn walk_bpf_no_symbol() {
258        let reader = make_reader_no_bpf_symbol();
259        let result = walk_bpf_programs(&reader).unwrap();
260        assert!(
261            result.is_empty(),
262            "expected empty vec when bpf_prog_idr symbol missing"
263        );
264    }
265
266    #[test]
267    fn classify_bpf_suspicious_kprobe() {
268        assert!(
269            classify_bpf_program("kprobe", "my_kprobe"),
270            "kprobe programs should always be flagged as suspicious"
271        );
272    }
273
274    #[test]
275    fn classify_bpf_benign_socket_filter() {
276        assert!(
277            !classify_bpf_program("socket_filter", "tcpdump"),
278            "socket_filter with a name should not be flagged as suspicious"
279        );
280    }
281
282    #[test]
283    fn classify_bpf_suspicious_unnamed_tracing() {
284        assert!(
285            classify_bpf_program("tracing", ""),
286            "unnamed tracing programs should be flagged as suspicious"
287        );
288    }
289
290    #[test]
291    fn classify_bpf_benign_named_tracing() {
292        assert!(
293            !classify_bpf_program("tracing", "my_tracer"),
294            "named tracing programs should not be flagged as suspicious"
295        );
296    }
297
298    // --- prog_type_name (private) exercised via walk_bpf + classify paths ---
299    // We exercise it indirectly through classify_bpf_program and read_bpf_prog
300    // by covering all classify arms.
301
302    #[test]
303    fn classify_bpf_raw_tracepoint_unnamed_suspicious() {
304        assert!(
305            classify_bpf_program("raw_tracepoint", ""),
306            "unnamed raw_tracepoint must be suspicious"
307        );
308    }
309
310    #[test]
311    fn classify_bpf_raw_tracepoint_named_benign() {
312        assert!(
313            !classify_bpf_program("raw_tracepoint", "my_hook"),
314            "named raw_tracepoint must not be suspicious"
315        );
316    }
317
318    #[test]
319    fn classify_bpf_raw_tracepoint_writable_always_suspicious() {
320        assert!(
321            classify_bpf_program("raw_tracepoint_writable", ""),
322            "raw_tracepoint_writable with no name must be suspicious"
323        );
324        assert!(
325            classify_bpf_program("raw_tracepoint_writable", "named"),
326            "raw_tracepoint_writable with a name must also be suspicious"
327        );
328    }
329
330    #[test]
331    fn classify_bpf_lsm_always_suspicious() {
332        assert!(
333            classify_bpf_program("lsm", ""),
334            "lsm with no name must be suspicious"
335        );
336        assert!(
337            classify_bpf_program("lsm", "some_lsm_prog"),
338            "lsm with a name must also be suspicious"
339        );
340    }
341
342    #[test]
343    fn classify_bpf_xdp_not_suspicious() {
344        assert!(
345            !classify_bpf_program("xdp", "my_xdp"),
346            "xdp program must not be suspicious by default"
347        );
348    }
349
350    #[test]
351    fn classify_bpf_tracepoint_not_suspicious() {
352        assert!(
353            !classify_bpf_program("tracepoint", ""),
354            "plain tracepoint must not be suspicious"
355        );
356    }
357
358    #[test]
359    fn classify_bpf_sched_cls_not_suspicious() {
360        assert!(
361            !classify_bpf_program("sched_cls", "tc_prog"),
362            "sched_cls must not be suspicious"
363        );
364    }
365
366    #[test]
367    fn classify_bpf_unknown_type_not_suspicious() {
368        assert!(
369            !classify_bpf_program("unknown_type_xyz", ""),
370            "unknown program type must not be suspicious"
371        );
372    }
373
374    // --- walk_bpf_programs: symbol present but xa_head resolves to 0 (empty tree) ---
375
376    #[test]
377    fn walk_bpf_programs_empty_idr_returns_empty() {
378        // Provide the bpf_prog_idr symbol at an unmapped address.
379        // read_field for idr.idr_rt will fail, or_else for idr.top also fails → xa_head = 0
380        // → returns Ok(Vec::new())
381        let isf = IsfBuilder::new()
382            .add_symbol("bpf_prog_idr", 0xDEAD_0000_0000_0000)
383            .build_json();
384        let resolver = IsfResolver::from_value(&isf).unwrap();
385        let (cr3, mem) = PageTableBuilder::new().build();
386        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
387        let reader = ObjectReader::new(vas, Box::new(resolver));
388
389        let result = walk_bpf_programs(&reader).unwrap();
390        assert!(
391            result.is_empty(),
392            "bpf_prog_idr with unreadable/zero xa_head → empty vec expected"
393        );
394    }
395
396    // --- walk_bpf_programs: symbol present, xa_head non-zero but tagged (retry entry) ---
397    // Exercises walk_idr_entries body: a tagged pointer (low bits == 0x1) is neither
398    // a node (0x2) nor a clean leaf (0x0), so it is silently skipped → empty result.
399    #[test]
400    fn walk_bpf_programs_tagged_xa_head_skipped_returns_empty() {
401        use memf_core::test_builders::{flags as ptf, SyntheticPhysMem};
402        use memf_core::vas::{TranslationMode, VirtualAddressSpace};
403
404        // bpf_prog_idr at a mapped address; idr.idr_rt at offset 0 returns xa_head.
405        // xa_head value = 0x0001 (low bits 0x1 → retry/reserved entry, not node, not leaf).
406        let idr_vaddr: u64 = 0xFFFF_8800_0050_0000;
407        let idr_paddr: u64 = 0x0050_0000; // unique, < 16 MB
408
409        let isf = IsfBuilder::new()
410            .add_symbol("bpf_prog_idr", idr_vaddr)
411            .add_struct("idr", 0x20)
412            .add_field("idr", "idr_rt", 0x00, "pointer")
413            .build_json();
414        let resolver = IsfResolver::from_value(&isf).unwrap();
415
416        // Write the idr page: idr_rt at offset 0 = 0x0001 (tagged, non-zero).
417        let xa_head: u64 = 0x0001u64; // low bits 0x1 → skipped by walk_idr_entries
418        let mut page = [0u8; 4096];
419        page[0..8].copy_from_slice(&xa_head.to_le_bytes());
420
421        let (cr3, mem) = PageTableBuilder::new()
422            .map_4k(idr_vaddr, idr_paddr, ptf::WRITABLE)
423            .write_phys(idr_paddr, &page)
424            .build();
425
426        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
427        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
428
429        let result = walk_bpf_programs(&reader).unwrap();
430        assert!(
431            result.is_empty(),
432            "tagged xa_head (retry entry) must be skipped → empty vec"
433        );
434    }
435
436    // --- walk_bpf_programs: xa_head is an xarray node (low bits 0x2) ---
437    // Exercises the `is_node` branch in walk_idr_entries: real_addr decoded, slots
438    // array iterated. All slots are 0 → no leaf entries → empty result.
439    #[test]
440    fn walk_bpf_programs_xa_node_all_zero_slots_returns_empty() {
441        use memf_core::test_builders::{flags as ptf, SyntheticPhysMem};
442
443        // idr struct at idr_vaddr; idr_rt (offset 0) = xa_node_addr | 0x2
444        let idr_vaddr: u64 = 0xFFFF_8800_0055_0000;
445        let idr_paddr: u64 = 0x0055_0000;
446
447        // xa_node is at a separate mapped page; slots at offset 16 (default used by code)
448        let xa_node_paddr: u64 = 0x0056_0000;
449        let xa_node_vaddr: u64 = 0xFFFF_8800_0056_0000;
450        // The tagged node pointer: xa_node_vaddr | 0x2
451        let xa_head_tagged: u64 = xa_node_vaddr | 0x2;
452
453        let isf = IsfBuilder::new()
454            .add_symbol("bpf_prog_idr", idr_vaddr)
455            .add_struct("idr", 0x20)
456            .add_field("idr", "idr_rt", 0x00, "pointer")
457            // xa_node.slots at offset 16 (matches unwrap_or(16) default in code)
458            .add_struct("xa_node", 0x400)
459            .add_field("xa_node", "slots", 0x10, "pointer")
460            .build_json();
461        let resolver = IsfResolver::from_value(&isf).unwrap();
462
463        // idr page: idr_rt = xa_head_tagged
464        let mut idr_page = [0u8; 4096];
465        idr_page[0..8].copy_from_slice(&xa_head_tagged.to_le_bytes());
466
467        // xa_node page: all slots zero → nothing to recurse into
468        let xa_node_page = [0u8; 4096];
469
470        let (cr3, mem) = PageTableBuilder::new()
471            .map_4k(idr_vaddr, idr_paddr, ptf::WRITABLE)
472            .write_phys(idr_paddr, &idr_page)
473            .map_4k(xa_node_vaddr, xa_node_paddr, ptf::WRITABLE)
474            .write_phys(xa_node_paddr, &xa_node_page)
475            .build();
476
477        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
478        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
479
480        let result = walk_bpf_programs(&reader).unwrap();
481        assert!(
482            result.is_empty(),
483            "xa_node with all-zero slots → no bpf_prog entries"
484        );
485    }
486
487    // --- walk_bpf_programs: xa_head is a leaf pointer (low bits 0x0, > 0x1000) ---
488    // Exercises the leaf branch in walk_idr_entries: read_bpf_prog is called.
489    // bpf_prog.type field missing → read_bpf_prog returns Err → entry silently skipped.
490    #[test]
491    fn walk_bpf_programs_leaf_ptr_read_fails_returns_empty() {
492        use memf_core::test_builders::{flags as ptf, SyntheticPhysMem};
493
494        let idr_vaddr: u64 = 0xFFFF_8800_0057_0000;
495        let idr_paddr: u64 = 0x0057_0000;
496
497        // A clean leaf pointer (low bits 0x0, > 0x1000) pointing to an unmapped page.
498        // read_bpf_prog will fail trying to read bpf_prog.type → silently skipped.
499        let leaf_ptr: u64 = 0xFFFF_8800_DEAD_0000; // unmapped → read fails
500
501        let isf = IsfBuilder::new()
502            .add_symbol("bpf_prog_idr", idr_vaddr)
503            .add_struct("idr", 0x20)
504            .add_field("idr", "idr_rt", 0x00, "pointer")
505            .build_json();
506        let resolver = IsfResolver::from_value(&isf).unwrap();
507
508        let mut idr_page = [0u8; 4096];
509        idr_page[0..8].copy_from_slice(&leaf_ptr.to_le_bytes());
510
511        let (cr3, mem) = PageTableBuilder::new()
512            .map_4k(idr_vaddr, idr_paddr, ptf::WRITABLE)
513            .write_phys(idr_paddr, &idr_page)
514            .build();
515
516        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
517        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
518
519        let result = walk_bpf_programs(&reader).unwrap();
520        assert!(
521            result.is_empty(),
522            "leaf ptr pointing to unreadable addr → read_bpf_prog fails → empty vec"
523        );
524    }
525
526    // --- prog_type_name: unknown index returns formatted string ---
527    // Exercises the map_or_else branch in prog_type_name for an out-of-range raw value.
528    #[test]
529    fn classify_bpf_unknown_indexed_type_not_suspicious() {
530        // prog_type_name(99) returns "unknown(99)"; classify_bpf_program falls through to _ => false
531        assert!(
532            !classify_bpf_program("unknown(99)", ""),
533            "unknown prog type string must not be suspicious"
534        );
535    }
536
537    // --- walk_bpf_programs: leaf ptr → read_bpf_prog succeeds (exercises prog_type_name) ---
538    // Builds a complete synthetic bpf_prog + bpf_prog_aux in memory so that
539    // read_bpf_prog completes successfully and a BpfProgramInfo is returned.
540    //
541    // Memory layout (all padded to page boundaries, physical addresses < 16 MB):
542    //   idr page     @ paddr 0x0060_0000 (vaddr 0xFFFF_8800_0060_0000)
543    //   bpf_prog     @ paddr 0x0061_0000 (vaddr 0xFFFF_8800_0061_0000)
544    //   bpf_prog_aux @ paddr 0x0062_0000 (vaddr 0xFFFF_8800_0062_0000)
545    #[test]
546    fn walk_bpf_programs_leaf_ptr_success_returns_program() {
547        use memf_core::test_builders::{flags as ptf, SyntheticPhysMem};
548
549        let idr_vaddr: u64 = 0xFFFF_8800_0060_0000;
550        let prog_vaddr: u64 = 0xFFFF_8800_0061_0000;
551        let aux_vaddr: u64 = 0xFFFF_8800_0062_0000;
552
553        let idr_paddr: u64 = 0x060_000;
554        let prog_paddr: u64 = 0x061_000;
555        let aux_paddr: u64 = 0x062_000;
556
557        // bpf_prog field offsets
558        let prog_type_off: u64 = 0x00; // u32
559        let prog_len_off: u64 = 0x04; // u32
560        let prog_jited_len_off: u64 = 0x08; // u32
561        let prog_tag_off: u64 = 0x10; // [u8; 8]
562        let prog_aux_off: u64 = 0x20; // *bpf_prog_aux
563
564        // bpf_prog_aux field offsets
565        let aux_id_off: u64 = 0x00; // u32
566        let aux_name_off: u64 = 0x08; // [u8; 16]
567        let aux_uid_off: u64 = 0x18; // u32
568
569        let isf = IsfBuilder::new()
570            .add_symbol("bpf_prog_idr", idr_vaddr)
571            .add_struct("idr", 0x20)
572            .add_field("idr", "idr_rt", 0x00u64, "pointer")
573            .add_struct("bpf_prog", 0x100)
574            .add_field("bpf_prog", "type", prog_type_off, "unsigned int")
575            .add_field("bpf_prog", "len", prog_len_off, "unsigned int")
576            .add_field("bpf_prog", "jited_len", prog_jited_len_off, "unsigned int")
577            .add_field("bpf_prog", "tag", prog_tag_off, "array")
578            .add_field("bpf_prog", "aux", prog_aux_off, "pointer")
579            .add_struct("bpf_prog_aux", 0x100)
580            .add_field("bpf_prog_aux", "id", aux_id_off, "unsigned int")
581            .add_field("bpf_prog_aux", "name", aux_name_off, "char")
582            .add_field("bpf_prog_aux", "uid", aux_uid_off, "unsigned int")
583            .build_json();
584        let resolver = IsfResolver::from_value(&isf).unwrap();
585
586        // idr page: idr_rt at offset 0 = prog_vaddr (clean leaf: low bits 0x0, > 0x1000)
587        let mut idr_page = [0u8; 4096];
588        idr_page[0..8].copy_from_slice(&prog_vaddr.to_le_bytes());
589
590        // bpf_prog page
591        // type = 2 (kprobe = index 2, which maps to "kprobe" → suspicious)
592        let prog_type_val: u32 = 2; // BPF_PROG_TYPE_KPROBE
593        let mut prog_page = [0u8; 4096];
594        prog_page[prog_type_off as usize..prog_type_off as usize + 4]
595            .copy_from_slice(&prog_type_val.to_le_bytes());
596        // len = 10 instructions
597        prog_page[prog_len_off as usize..prog_len_off as usize + 4]
598            .copy_from_slice(&10u32.to_le_bytes());
599        // jited_len = 80 bytes
600        prog_page[prog_jited_len_off as usize..prog_jited_len_off as usize + 4]
601            .copy_from_slice(&80u32.to_le_bytes());
602        // tag = [0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, 0x00]
603        prog_page[prog_tag_off as usize..prog_tag_off as usize + 8]
604            .copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, 0x00]);
605        // aux = aux_vaddr
606        prog_page[prog_aux_off as usize..prog_aux_off as usize + 8]
607            .copy_from_slice(&aux_vaddr.to_le_bytes());
608
609        // bpf_prog_aux page
610        let mut aux_page = [0u8; 4096];
611        // id = 42
612        aux_page[aux_id_off as usize..aux_id_off as usize + 4]
613            .copy_from_slice(&42u32.to_le_bytes());
614        // name = "evil_kprobe\0" (16 bytes)
615        aux_page[aux_name_off as usize..aux_name_off as usize + 12]
616            .copy_from_slice(b"evil_kprobe\0");
617        // uid = 1000
618        aux_page[aux_uid_off as usize..aux_uid_off as usize + 4]
619            .copy_from_slice(&1000u32.to_le_bytes());
620
621        let (cr3, mem) = PageTableBuilder::new()
622            .map_4k(idr_vaddr, idr_paddr, ptf::WRITABLE)
623            .write_phys(idr_paddr, &idr_page)
624            .map_4k(prog_vaddr, prog_paddr, ptf::WRITABLE)
625            .write_phys(prog_paddr, &prog_page)
626            .map_4k(aux_vaddr, aux_paddr, ptf::WRITABLE)
627            .write_phys(aux_paddr, &aux_page)
628            .build();
629
630        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
631        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
632
633        let result = walk_bpf_programs(&reader).unwrap();
634        assert_eq!(result.len(), 1, "should detect exactly one BPF program");
635        let prog = &result[0];
636        assert_eq!(prog.id, 42);
637        assert_eq!(prog.prog_type, "kprobe");
638        assert_eq!(prog.insn_count, 10);
639        assert_eq!(prog.jited_len, 80);
640        assert_eq!(prog.loaded_by_uid, 1000);
641        assert!(prog.is_suspicious, "kprobe must be suspicious");
642        assert!(
643            prog.name.contains("evil_kprobe"),
644            "name should be read from aux"
645        );
646    }
647
648    // --- walk_bpf_programs: xa_node with a non-zero retry-tagged slot (low bits 0x1) ---
649    // Exercises walk_idr_entries recursion: xa_node slot has value with low bits 0x1
650    // (retry / reserved). This is neither a node (0x2) nor a clean leaf (0x0), so
651    // it hits the else-if condition `node_ptr & 0x3 == 0` which is false → silently skipped.
652    #[test]
653    fn walk_bpf_programs_xa_node_retry_slot_skipped() {
654        use memf_core::test_builders::{flags as ptf, SyntheticPhysMem};
655
656        let idr_vaddr: u64 = 0xFFFF_8800_0063_0000;
657        let idr_paddr: u64 = 0x0063_0000;
658        let xa_node_paddr: u64 = 0x0064_0000;
659        let xa_node_vaddr: u64 = 0xFFFF_8800_0064_0000;
660
661        // xa_head is a tagged node pointer (low bits 0x2)
662        let xa_head_tagged: u64 = xa_node_vaddr | 0x2;
663
664        let isf = IsfBuilder::new()
665            .add_symbol("bpf_prog_idr", idr_vaddr)
666            .add_struct("idr", 0x20)
667            .add_field("idr", "idr_rt", 0x00u64, "pointer")
668            .add_struct("xa_node", 0x400)
669            .add_field("xa_node", "slots", 0x10u64, "pointer")
670            .build_json();
671        let resolver = IsfResolver::from_value(&isf).unwrap();
672
673        let mut idr_page = [0u8; 4096];
674        idr_page[0..8].copy_from_slice(&xa_head_tagged.to_le_bytes());
675
676        // xa_node: slot 0 = 0x1 (retry/reserved, low bits 0x1 → skipped), rest = 0
677        let mut xa_node_page = [0u8; 4096];
678        let retry_val: u64 = 0x0001u64; // low bits 0x1 → skipped
679                                        // slots at offset 0x10
680        xa_node_page[0x10..0x18].copy_from_slice(&retry_val.to_le_bytes());
681
682        let (cr3, mem) = PageTableBuilder::new()
683            .map_4k(idr_vaddr, idr_paddr, ptf::WRITABLE)
684            .write_phys(idr_paddr, &idr_page)
685            .map_4k(xa_node_vaddr, xa_node_paddr, ptf::WRITABLE)
686            .write_phys(xa_node_paddr, &xa_node_page)
687            .build();
688
689        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
690        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
691
692        let result = walk_bpf_programs(&reader).unwrap();
693        assert!(
694            result.is_empty(),
695            "retry-tagged slot in xa_node must be skipped → empty result"
696        );
697    }
698
699    // --- walk_bpf_programs: idr.idr_rt fails, idr.top succeeds (or_else branch) ---
700    // Exercises the or_else fallback in walk_bpf_programs (lines 92-97).
701    // idr_rt field absent → or_else reads idr.top → also fails (unmapped) → xa_head = 0 → empty.
702    #[test]
703    fn walk_bpf_programs_idr_top_fallback_zero_returns_empty() {
704        use memf_core::test_builders::{flags as ptf, SyntheticPhysMem};
705
706        let idr_vaddr: u64 = 0xFFFF_8800_0065_0000;
707        let idr_paddr: u64 = 0x0065_0000;
708
709        // Only define idr.top (not idr_rt) at offset 0 = 0 → xa_head = 0 → empty.
710        let isf = IsfBuilder::new()
711            .add_symbol("bpf_prog_idr", idr_vaddr)
712            .add_struct("idr", 0x20)
713            .add_field("idr", "top", 0x00u64, "pointer")
714            .build_json();
715        let resolver = IsfResolver::from_value(&isf).unwrap();
716
717        let idr_page = [0u8; 4096]; // top = 0 → xa_head = 0
718
719        let (cr3, mem) = PageTableBuilder::new()
720            .map_4k(idr_vaddr, idr_paddr, ptf::WRITABLE)
721            .write_phys(idr_paddr, &idr_page)
722            .build();
723
724        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
725        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
726
727        let result = walk_bpf_programs(&reader).unwrap();
728        assert!(
729            result.is_empty(),
730            "idr.top fallback with xa_head=0 → empty result"
731        );
732    }
733
734    // --- prog_type_name: in-bounds entries (exercises all named branches) ---
735    // These call the private prog_type_name via walk_bpf; here we test the
736    // outer public interface that composes prog_type_name + classify.
737    // We exercise prog_type_name's get() Some branch for all in-range values
738    // by calling classify_bpf_program with type names returned by it.
739    #[test]
740    fn bpf_program_info_serializes() {
741        let info = BpfProgramInfo {
742            id: 7,
743            prog_type: "kprobe".to_string(),
744            name: "hook".to_string(),
745            tag: [1, 2, 3, 4, 5, 6, 7, 8],
746            insn_count: 20,
747            jited_len: 120,
748            loaded_by_uid: 0,
749            is_suspicious: true,
750        };
751        let json = serde_json::to_string(&info).unwrap();
752        assert!(json.contains("\"id\":7"));
753        assert!(json.contains("kprobe"));
754        assert!(json.contains("\"is_suspicious\":true"));
755    }
756}