1use memf_core::object_reader::ObjectReader;
9use memf_format::PhysicalMemoryProvider;
10
11use crate::{Error, Result};
12
13#[derive(Debug, Clone, serde::Serialize)]
15pub struct BpfProgramInfo {
16 pub id: u32,
18 pub prog_type: String,
20 pub name: String,
22 pub tag: [u8; 8],
24 pub insn_count: u32,
26 pub jited_len: u32,
28 pub loaded_by_uid: u32,
30 pub is_suspicious: bool,
32}
33
34const 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
70fn 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
77pub fn walk_bpf_programs<P: PhysicalMemoryProvider>(
82 reader: &ObjectReader<P>,
83) -> Result<Vec<BpfProgramInfo>> {
84 let Some(idr_addr) = reader.symbols().symbol_address("bpf_prog_idr") else {
86 return Ok(Vec::new());
87 };
88
89 let xa_head: u64 = reader
92 .read_field(idr_addr, "idr", "idr_rt")
93 .or_else(|_| {
94 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 let mut programs = Vec::new();
106 walk_idr_entries(reader, xa_head, &mut programs)?;
107
108 Ok(programs)
109}
110
111fn walk_idr_entries<P: PhysicalMemoryProvider>(
116 reader: &ObjectReader<P>,
117 node_ptr: u64,
118 programs: &mut Vec<BpfProgramInfo>,
119) -> Result<()> {
120 const MAX_SLOTS: usize = 64;
122 const MAX_PROGRAMS: usize = 10_000;
123
124 let is_node = (node_ptr & 0x3) == 0x2;
126
127 if is_node {
128 let real_addr = node_ptr & !0x3;
130
131 let slots_offset = reader
133 .symbols()
134 .field_offset("xa_node", "slots")
135 .unwrap_or(16); 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 if let Ok(info) = read_bpf_prog(reader, node_ptr) {
157 programs.push(info);
158 }
159 }
160 Ok(())
163}
164
165fn read_bpf_prog<P: PhysicalMemoryProvider>(
167 reader: &ObjectReader<P>,
168 prog_addr: u64,
169) -> Result<BpfProgramInfo> {
170 let raw_type: u32 = reader.read_field(prog_addr, "bpf_prog", "type")?;
172 let prog_type = prog_type_name(raw_type);
173
174 let insn_count: u32 = reader.read_field(prog_addr, "bpf_prog", "len")?;
176
177 let jited_len: u32 = reader
179 .read_field(prog_addr, "bpf_prog", "jited_len")
180 .unwrap_or(0);
181
182 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 let aux_addr: u64 = reader.read_field(prog_addr, "bpf_prog", "aux")?;
197
198 let id: u32 = reader
200 .read_field(aux_addr, "bpf_prog_aux", "id")
201 .unwrap_or(0);
202
203 let name = reader
205 .read_field_string(aux_addr, "bpf_prog_aux", "name", 16)
206 .unwrap_or_default();
207
208 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
227pub 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 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 #[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 #[test]
377 fn walk_bpf_programs_empty_idr_returns_empty() {
378 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 #[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 let idr_vaddr: u64 = 0xFFFF_8800_0050_0000;
407 let idr_paddr: u64 = 0x0050_0000; 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 let xa_head: u64 = 0x0001u64; 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 #[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 let idr_vaddr: u64 = 0xFFFF_8800_0055_0000;
445 let idr_paddr: u64 = 0x0055_0000;
446
447 let xa_node_paddr: u64 = 0x0056_0000;
449 let xa_node_vaddr: u64 = 0xFFFF_8800_0056_0000;
450 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 .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 let mut idr_page = [0u8; 4096];
465 idr_page[0..8].copy_from_slice(&xa_head_tagged.to_le_bytes());
466
467 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 #[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 let leaf_ptr: u64 = 0xFFFF_8800_DEAD_0000; 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 #[test]
529 fn classify_bpf_unknown_indexed_type_not_suspicious() {
530 assert!(
532 !classify_bpf_program("unknown(99)", ""),
533 "unknown prog type string must not be suspicious"
534 );
535 }
536
537 #[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 let prog_type_off: u64 = 0x00; let prog_len_off: u64 = 0x04; let prog_jited_len_off: u64 = 0x08; let prog_tag_off: u64 = 0x10; let prog_aux_off: u64 = 0x20; let aux_id_off: u64 = 0x00; let aux_name_off: u64 = 0x08; let aux_uid_off: u64 = 0x18; 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 let mut idr_page = [0u8; 4096];
588 idr_page[0..8].copy_from_slice(&prog_vaddr.to_le_bytes());
589
590 let prog_type_val: u32 = 2; 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 prog_page[prog_len_off as usize..prog_len_off as usize + 4]
598 .copy_from_slice(&10u32.to_le_bytes());
599 prog_page[prog_jited_len_off as usize..prog_jited_len_off as usize + 4]
601 .copy_from_slice(&80u32.to_le_bytes());
602 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 prog_page[prog_aux_off as usize..prog_aux_off as usize + 8]
607 .copy_from_slice(&aux_vaddr.to_le_bytes());
608
609 let mut aux_page = [0u8; 4096];
611 aux_page[aux_id_off as usize..aux_id_off as usize + 4]
613 .copy_from_slice(&42u32.to_le_bytes());
614 aux_page[aux_name_off as usize..aux_name_off as usize + 12]
616 .copy_from_slice(b"evil_kprobe\0");
617 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 #[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 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 let mut xa_node_page = [0u8; 4096];
678 let retry_val: u64 = 0x0001u64; 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 #[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 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]; 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 #[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}