use memf_core::object_reader::ObjectReader;
use memf_format::PhysicalMemoryProvider;
use crate::{Error, Result};
#[derive(Debug, Clone, serde::Serialize)]
pub struct BpfProgramInfo {
pub id: u32,
pub prog_type: String,
pub name: String,
pub tag: [u8; 8],
pub insn_count: u32,
pub jited_len: u32,
pub loaded_by_uid: u32,
pub is_suspicious: bool,
}
const BPF_PROG_TYPES: &[&str] = &[
"unspec",
"socket_filter",
"kprobe",
"sched_cls",
"sched_act",
"tracepoint",
"xdp",
"perf_event",
"cgroup_skb",
"cgroup_sock",
"lwt_in",
"lwt_out",
"lwt_xmit",
"sock_ops",
"sk_skb",
"cgroup_device",
"sk_msg",
"raw_tracepoint",
"cgroup_sock_addr",
"lwt_seg6local",
"lirc_mode2",
"sk_reuseport",
"flow_dissector",
"cgroup_sysctl",
"raw_tracepoint_writable",
"cgroup_sockopt",
"tracing",
"struct_ops",
"ext",
"lsm",
"sk_lookup",
"syscall",
];
fn prog_type_name(raw: u32) -> String {
BPF_PROG_TYPES
.get(raw as usize)
.map_or_else(|| format!("unknown({raw})"), |s| (*s).to_string())
}
pub fn walk_bpf_programs<P: PhysicalMemoryProvider>(
reader: &ObjectReader<P>,
) -> Result<Vec<BpfProgramInfo>> {
let Some(idr_addr) = reader.symbols().symbol_address("bpf_prog_idr") else {
return Ok(Vec::new());
};
let xa_head: u64 = reader
.read_field(idr_addr, "idr", "idr_rt")
.or_else(|_| {
reader.read_field::<u64>(idr_addr, "idr", "top")
})
.unwrap_or(0);
if xa_head == 0 {
return Ok(Vec::new());
}
let mut programs = Vec::new();
walk_idr_entries(reader, xa_head, &mut programs)?;
Ok(programs)
}
fn walk_idr_entries<P: PhysicalMemoryProvider>(
reader: &ObjectReader<P>,
node_ptr: u64,
programs: &mut Vec<BpfProgramInfo>,
) -> Result<()> {
const MAX_SLOTS: usize = 64;
const MAX_PROGRAMS: usize = 10_000;
let is_node = (node_ptr & 0x3) == 0x2;
if is_node {
let real_addr = node_ptr & !0x3;
let slots_offset = reader
.symbols()
.field_offset("xa_node", "slots")
.unwrap_or(16);
for i in 0..MAX_SLOTS {
if programs.len() >= MAX_PROGRAMS {
break;
}
let slot_addr = real_addr + slots_offset + (i as u64) * 8;
let slot_val = {
let mut buf = [0u8; 8];
match reader.vas().read_virt(slot_addr, &mut buf) {
Ok(()) => u64::from_le_bytes(buf),
Err(_) => 0,
}
};
if slot_val == 0 {
continue;
}
walk_idr_entries(reader, slot_val, programs)?;
}
} else if node_ptr.trailing_zeros() >= 2 && node_ptr > 0x1000 {
if let Ok(info) = read_bpf_prog(reader, node_ptr) {
programs.push(info);
}
}
Ok(())
}
fn read_bpf_prog<P: PhysicalMemoryProvider>(
reader: &ObjectReader<P>,
prog_addr: u64,
) -> Result<BpfProgramInfo> {
let raw_type: u32 = reader.read_field(prog_addr, "bpf_prog", "type")?;
let prog_type = prog_type_name(raw_type);
let insn_count: u32 = reader.read_field(prog_addr, "bpf_prog", "len")?;
let jited_len: u32 = reader
.read_field(prog_addr, "bpf_prog", "jited_len")
.unwrap_or(0);
let mut tag = [0u8; 8];
let tag_offset = reader
.symbols()
.field_offset("bpf_prog", "tag")
.ok_or_else(|| Error::MissingField {
struct_name: "bpf_prog".into(),
field_name: "tag".into(),
})?;
if let Ok(bytes) = reader.read_bytes(prog_addr + tag_offset, 8) {
tag.copy_from_slice(&bytes[..8]);
}
let aux_addr: u64 = reader.read_field(prog_addr, "bpf_prog", "aux")?;
let id: u32 = reader
.read_field(aux_addr, "bpf_prog_aux", "id")
.unwrap_or(0);
let name = reader
.read_field_string(aux_addr, "bpf_prog_aux", "name", 16)
.unwrap_or_default();
let loaded_by_uid: u32 = reader
.read_field(aux_addr, "bpf_prog_aux", "uid")
.unwrap_or(0);
let is_suspicious = classify_bpf_program(&prog_type, &name);
Ok(BpfProgramInfo {
id,
prog_type,
name,
tag,
insn_count,
jited_len,
loaded_by_uid,
is_suspicious,
})
}
pub use crate::heuristics::classify_bpf_program;
#[cfg(test)]
mod tests {
use super::*;
use memf_core::test_builders::PageTableBuilder;
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
fn make_reader_no_bpf_symbol() -> ObjectReader<memf_core::test_builders::SyntheticPhysMem> {
let isf = IsfBuilder::new().build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new().build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
ObjectReader::new(vas, Box::new(resolver))
}
#[test]
fn walk_bpf_no_symbol() {
let reader = make_reader_no_bpf_symbol();
let result = walk_bpf_programs(&reader).unwrap();
assert!(
result.is_empty(),
"expected empty vec when bpf_prog_idr symbol missing"
);
}
#[test]
fn classify_bpf_suspicious_kprobe() {
assert!(
classify_bpf_program("kprobe", "my_kprobe"),
"kprobe programs should always be flagged as suspicious"
);
}
#[test]
fn classify_bpf_benign_socket_filter() {
assert!(
!classify_bpf_program("socket_filter", "tcpdump"),
"socket_filter with a name should not be flagged as suspicious"
);
}
#[test]
fn classify_bpf_suspicious_unnamed_tracing() {
assert!(
classify_bpf_program("tracing", ""),
"unnamed tracing programs should be flagged as suspicious"
);
}
#[test]
fn classify_bpf_benign_named_tracing() {
assert!(
!classify_bpf_program("tracing", "my_tracer"),
"named tracing programs should not be flagged as suspicious"
);
}
#[test]
fn classify_bpf_raw_tracepoint_unnamed_suspicious() {
assert!(
classify_bpf_program("raw_tracepoint", ""),
"unnamed raw_tracepoint must be suspicious"
);
}
#[test]
fn classify_bpf_raw_tracepoint_named_benign() {
assert!(
!classify_bpf_program("raw_tracepoint", "my_hook"),
"named raw_tracepoint must not be suspicious"
);
}
#[test]
fn classify_bpf_raw_tracepoint_writable_always_suspicious() {
assert!(
classify_bpf_program("raw_tracepoint_writable", ""),
"raw_tracepoint_writable with no name must be suspicious"
);
assert!(
classify_bpf_program("raw_tracepoint_writable", "named"),
"raw_tracepoint_writable with a name must also be suspicious"
);
}
#[test]
fn classify_bpf_lsm_always_suspicious() {
assert!(
classify_bpf_program("lsm", ""),
"lsm with no name must be suspicious"
);
assert!(
classify_bpf_program("lsm", "some_lsm_prog"),
"lsm with a name must also be suspicious"
);
}
#[test]
fn classify_bpf_xdp_not_suspicious() {
assert!(
!classify_bpf_program("xdp", "my_xdp"),
"xdp program must not be suspicious by default"
);
}
#[test]
fn classify_bpf_tracepoint_not_suspicious() {
assert!(
!classify_bpf_program("tracepoint", ""),
"plain tracepoint must not be suspicious"
);
}
#[test]
fn classify_bpf_sched_cls_not_suspicious() {
assert!(
!classify_bpf_program("sched_cls", "tc_prog"),
"sched_cls must not be suspicious"
);
}
#[test]
fn classify_bpf_unknown_type_not_suspicious() {
assert!(
!classify_bpf_program("unknown_type_xyz", ""),
"unknown program type must not be suspicious"
);
}
#[test]
fn walk_bpf_programs_empty_idr_returns_empty() {
let isf = IsfBuilder::new()
.add_symbol("bpf_prog_idr", 0xDEAD_0000_0000_0000)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new().build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = walk_bpf_programs(&reader).unwrap();
assert!(
result.is_empty(),
"bpf_prog_idr with unreadable/zero xa_head → empty vec expected"
);
}
#[test]
fn walk_bpf_programs_tagged_xa_head_skipped_returns_empty() {
use memf_core::test_builders::{flags as ptf, SyntheticPhysMem};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
let idr_vaddr: u64 = 0xFFFF_8800_0050_0000;
let idr_paddr: u64 = 0x0050_0000;
let isf = IsfBuilder::new()
.add_symbol("bpf_prog_idr", idr_vaddr)
.add_struct("idr", 0x20)
.add_field("idr", "idr_rt", 0x00, "pointer")
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let xa_head: u64 = 0x0001u64; let mut page = [0u8; 4096];
page[0..8].copy_from_slice(&xa_head.to_le_bytes());
let (cr3, mem) = PageTableBuilder::new()
.map_4k(idr_vaddr, idr_paddr, ptf::WRITABLE)
.write_phys(idr_paddr, &page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
let result = walk_bpf_programs(&reader).unwrap();
assert!(
result.is_empty(),
"tagged xa_head (retry entry) must be skipped → empty vec"
);
}
#[test]
fn walk_bpf_programs_xa_node_all_zero_slots_returns_empty() {
use memf_core::test_builders::{flags as ptf, SyntheticPhysMem};
let idr_vaddr: u64 = 0xFFFF_8800_0055_0000;
let idr_paddr: u64 = 0x0055_0000;
let xa_node_paddr: u64 = 0x0056_0000;
let xa_node_vaddr: u64 = 0xFFFF_8800_0056_0000;
let xa_head_tagged: u64 = xa_node_vaddr | 0x2;
let isf = IsfBuilder::new()
.add_symbol("bpf_prog_idr", idr_vaddr)
.add_struct("idr", 0x20)
.add_field("idr", "idr_rt", 0x00, "pointer")
.add_struct("xa_node", 0x400)
.add_field("xa_node", "slots", 0x10, "pointer")
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let mut idr_page = [0u8; 4096];
idr_page[0..8].copy_from_slice(&xa_head_tagged.to_le_bytes());
let xa_node_page = [0u8; 4096];
let (cr3, mem) = PageTableBuilder::new()
.map_4k(idr_vaddr, idr_paddr, ptf::WRITABLE)
.write_phys(idr_paddr, &idr_page)
.map_4k(xa_node_vaddr, xa_node_paddr, ptf::WRITABLE)
.write_phys(xa_node_paddr, &xa_node_page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
let result = walk_bpf_programs(&reader).unwrap();
assert!(
result.is_empty(),
"xa_node with all-zero slots → no bpf_prog entries"
);
}
#[test]
fn walk_bpf_programs_leaf_ptr_read_fails_returns_empty() {
use memf_core::test_builders::{flags as ptf, SyntheticPhysMem};
let idr_vaddr: u64 = 0xFFFF_8800_0057_0000;
let idr_paddr: u64 = 0x0057_0000;
let leaf_ptr: u64 = 0xFFFF_8800_DEAD_0000;
let isf = IsfBuilder::new()
.add_symbol("bpf_prog_idr", idr_vaddr)
.add_struct("idr", 0x20)
.add_field("idr", "idr_rt", 0x00, "pointer")
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let mut idr_page = [0u8; 4096];
idr_page[0..8].copy_from_slice(&leaf_ptr.to_le_bytes());
let (cr3, mem) = PageTableBuilder::new()
.map_4k(idr_vaddr, idr_paddr, ptf::WRITABLE)
.write_phys(idr_paddr, &idr_page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
let result = walk_bpf_programs(&reader).unwrap();
assert!(
result.is_empty(),
"leaf ptr pointing to unreadable addr → read_bpf_prog fails → empty vec"
);
}
#[test]
fn classify_bpf_unknown_indexed_type_not_suspicious() {
assert!(
!classify_bpf_program("unknown(99)", ""),
"unknown prog type string must not be suspicious"
);
}
#[test]
fn walk_bpf_programs_leaf_ptr_success_returns_program() {
use memf_core::test_builders::{flags as ptf, SyntheticPhysMem};
let idr_vaddr: u64 = 0xFFFF_8800_0060_0000;
let prog_vaddr: u64 = 0xFFFF_8800_0061_0000;
let aux_vaddr: u64 = 0xFFFF_8800_0062_0000;
let idr_paddr: u64 = 0x060_000;
let prog_paddr: u64 = 0x061_000;
let aux_paddr: u64 = 0x062_000;
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()
.add_symbol("bpf_prog_idr", idr_vaddr)
.add_struct("idr", 0x20)
.add_field("idr", "idr_rt", 0x00u64, "pointer")
.add_struct("bpf_prog", 0x100)
.add_field("bpf_prog", "type", prog_type_off, "unsigned int")
.add_field("bpf_prog", "len", prog_len_off, "unsigned int")
.add_field("bpf_prog", "jited_len", prog_jited_len_off, "unsigned int")
.add_field("bpf_prog", "tag", prog_tag_off, "array")
.add_field("bpf_prog", "aux", prog_aux_off, "pointer")
.add_struct("bpf_prog_aux", 0x100)
.add_field("bpf_prog_aux", "id", aux_id_off, "unsigned int")
.add_field("bpf_prog_aux", "name", aux_name_off, "char")
.add_field("bpf_prog_aux", "uid", aux_uid_off, "unsigned int")
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let mut idr_page = [0u8; 4096];
idr_page[0..8].copy_from_slice(&prog_vaddr.to_le_bytes());
let prog_type_val: u32 = 2; let mut prog_page = [0u8; 4096];
prog_page[prog_type_off as usize..prog_type_off as usize + 4]
.copy_from_slice(&prog_type_val.to_le_bytes());
prog_page[prog_len_off as usize..prog_len_off as usize + 4]
.copy_from_slice(&10u32.to_le_bytes());
prog_page[prog_jited_len_off as usize..prog_jited_len_off as usize + 4]
.copy_from_slice(&80u32.to_le_bytes());
prog_page[prog_tag_off as usize..prog_tag_off as usize + 8]
.copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, 0x00]);
prog_page[prog_aux_off as usize..prog_aux_off as usize + 8]
.copy_from_slice(&aux_vaddr.to_le_bytes());
let mut aux_page = [0u8; 4096];
aux_page[aux_id_off as usize..aux_id_off as usize + 4]
.copy_from_slice(&42u32.to_le_bytes());
aux_page[aux_name_off as usize..aux_name_off as usize + 12]
.copy_from_slice(b"evil_kprobe\0");
aux_page[aux_uid_off as usize..aux_uid_off as usize + 4]
.copy_from_slice(&1000u32.to_le_bytes());
let (cr3, mem) = PageTableBuilder::new()
.map_4k(idr_vaddr, idr_paddr, ptf::WRITABLE)
.write_phys(idr_paddr, &idr_page)
.map_4k(prog_vaddr, prog_paddr, ptf::WRITABLE)
.write_phys(prog_paddr, &prog_page)
.map_4k(aux_vaddr, aux_paddr, ptf::WRITABLE)
.write_phys(aux_paddr, &aux_page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
let result = walk_bpf_programs(&reader).unwrap();
assert_eq!(result.len(), 1, "should detect exactly one BPF program");
let prog = &result[0];
assert_eq!(prog.id, 42);
assert_eq!(prog.prog_type, "kprobe");
assert_eq!(prog.insn_count, 10);
assert_eq!(prog.jited_len, 80);
assert_eq!(prog.loaded_by_uid, 1000);
assert!(prog.is_suspicious, "kprobe must be suspicious");
assert!(
prog.name.contains("evil_kprobe"),
"name should be read from aux"
);
}
#[test]
fn walk_bpf_programs_xa_node_retry_slot_skipped() {
use memf_core::test_builders::{flags as ptf, SyntheticPhysMem};
let idr_vaddr: u64 = 0xFFFF_8800_0063_0000;
let idr_paddr: u64 = 0x0063_0000;
let xa_node_paddr: u64 = 0x0064_0000;
let xa_node_vaddr: u64 = 0xFFFF_8800_0064_0000;
let xa_head_tagged: u64 = xa_node_vaddr | 0x2;
let isf = IsfBuilder::new()
.add_symbol("bpf_prog_idr", idr_vaddr)
.add_struct("idr", 0x20)
.add_field("idr", "idr_rt", 0x00u64, "pointer")
.add_struct("xa_node", 0x400)
.add_field("xa_node", "slots", 0x10u64, "pointer")
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let mut idr_page = [0u8; 4096];
idr_page[0..8].copy_from_slice(&xa_head_tagged.to_le_bytes());
let mut xa_node_page = [0u8; 4096];
let retry_val: u64 = 0x0001u64; xa_node_page[0x10..0x18].copy_from_slice(&retry_val.to_le_bytes());
let (cr3, mem) = PageTableBuilder::new()
.map_4k(idr_vaddr, idr_paddr, ptf::WRITABLE)
.write_phys(idr_paddr, &idr_page)
.map_4k(xa_node_vaddr, xa_node_paddr, ptf::WRITABLE)
.write_phys(xa_node_paddr, &xa_node_page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
let result = walk_bpf_programs(&reader).unwrap();
assert!(
result.is_empty(),
"retry-tagged slot in xa_node must be skipped → empty result"
);
}
#[test]
fn walk_bpf_programs_idr_top_fallback_zero_returns_empty() {
use memf_core::test_builders::{flags as ptf, SyntheticPhysMem};
let idr_vaddr: u64 = 0xFFFF_8800_0065_0000;
let idr_paddr: u64 = 0x0065_0000;
let isf = IsfBuilder::new()
.add_symbol("bpf_prog_idr", idr_vaddr)
.add_struct("idr", 0x20)
.add_field("idr", "top", 0x00u64, "pointer")
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let idr_page = [0u8; 4096];
let (cr3, mem) = PageTableBuilder::new()
.map_4k(idr_vaddr, idr_paddr, ptf::WRITABLE)
.write_phys(idr_paddr, &idr_page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
let result = walk_bpf_programs(&reader).unwrap();
assert!(
result.is_empty(),
"idr.top fallback with xa_head=0 → empty result"
);
}
#[test]
fn bpf_program_info_serializes() {
let info = BpfProgramInfo {
id: 7,
prog_type: "kprobe".to_string(),
name: "hook".to_string(),
tag: [1, 2, 3, 4, 5, 6, 7, 8],
insn_count: 20,
jited_len: 120,
loaded_by_uid: 0,
is_suspicious: true,
};
let json = serde_json::to_string(&info).unwrap();
assert!(json.contains("\"id\":7"));
assert!(json.contains("kprobe"));
assert!(json.contains("\"is_suspicious\":true"));
}
}