use memf_core::object_reader::ObjectReader;
use memf_format::PhysicalMemoryProvider;
use crate::Result;
#[derive(Debug, Clone, serde::Serialize)]
pub struct PerfEventInfo {
pub pid: u32,
pub comm: String,
pub event_type: u32,
pub event_type_name: String,
pub config: u64,
pub sample_period: u64,
pub is_suspicious: bool,
}
pub fn perf_type_name(t: u32) -> &'static str {
match t {
0 => "HARDWARE",
1 => "SOFTWARE",
2 => "TRACEPOINT",
3 => "HW_CACHE",
4 => "RAW",
5 => "BREAKPOINT",
_ => "UNKNOWN",
}
}
pub use crate::heuristics::classify_perf_event;
pub fn walk_perf_events<P: PhysicalMemoryProvider>(
reader: &ObjectReader<P>,
) -> Result<Vec<PerfEventInfo>> {
let init_task_addr = match reader.symbols().symbol_address("init_task") {
Some(addr) => addr,
None => return Ok(Vec::new()),
};
let tasks_offset = match reader.symbols().field_offset("task_struct", "tasks") {
Some(off) => off,
None => return Ok(Vec::new()),
};
let ctxp_offset = match reader
.symbols()
.field_offset("task_struct", "perf_event_ctxp")
{
Some(off) => off,
None => return Ok(Vec::new()),
};
let mut results = Vec::new();
let mut task_addrs: Vec<u64> = Vec::new();
{
let first_next: u64 = match reader.read_field(init_task_addr, "task_struct", "tasks") {
Ok(v) => v,
Err(_) => return Ok(Vec::new()),
};
let mut cursor = first_next;
let mut guard = 0usize;
loop {
if cursor == 0 || guard > 65536 {
break;
}
let task_addr = cursor.saturating_sub(tasks_offset);
if task_addr == init_task_addr {
break;
}
task_addrs.push(task_addr);
cursor = match reader.read_field(cursor, "list_head", "next") {
Ok(v) => v,
Err(_) => break,
};
guard += 1;
}
}
let all_tasks = std::iter::once(init_task_addr).chain(task_addrs);
for task_addr in all_tasks {
let pid: u32 = reader
.read_field::<u32>(task_addr, "task_struct", "pid")
.unwrap_or(0);
let comm_bytes: [u8; 16] = reader
.read_field(task_addr, "task_struct", "comm")
.unwrap_or([0u8; 16]);
let comm = std::str::from_utf8(&comm_bytes)
.unwrap_or("")
.trim_end_matches('\0')
.to_string();
let ctx_ptr_addr = task_addr + ctxp_offset;
let ctx_ptr: u64 = match reader.read_bytes(ctx_ptr_addr, 8) {
Ok(bytes) => u64::from_le_bytes(bytes.try_into().unwrap_or([0u8; 8])),
Err(_) => continue,
};
if ctx_ptr == 0 {
continue;
}
for group_field in &["pinned_groups", "flexible_groups"] {
let head_addr = match reader
.symbols()
.field_offset("perf_event_context", group_field)
{
Some(off) => ctx_ptr + off,
None => continue,
};
let first_event_list: u64 = match reader.read_bytes(head_addr, 8) {
Ok(b) => u64::from_le_bytes(b.try_into().unwrap_or([0u8; 8])),
Err(_) => continue,
};
let event_group_node_offset =
match reader.symbols().field_offset("perf_event", "group_entry") {
Some(off) => off,
None => continue,
};
let mut cursor = first_event_list;
let mut guard = 0usize;
loop {
if cursor == 0 || cursor == head_addr || guard > 4096 {
break;
}
let event_addr = cursor.saturating_sub(event_group_node_offset);
let attr_offset: u64 = reader
.symbols()
.field_offset("perf_event", "attr")
.map_or(0x20, |o| o);
let event_type: u32 = if let Ok(b) = reader.read_bytes(event_addr + attr_offset, 4)
{
u32::from_le_bytes(b.try_into().unwrap_or([0u8; 4]))
} else {
cursor = match reader.read_bytes(cursor, 8) {
Ok(b) => u64::from_le_bytes(b.try_into().unwrap_or([0u8; 8])),
Err(_) => break,
};
guard += 1;
continue;
};
let config: u64 = reader
.read_bytes(event_addr + attr_offset + 8, 8)
.ok()
.and_then(|b| b.try_into().ok())
.map_or(0, u64::from_le_bytes);
let sample_period: u64 = reader
.read_bytes(event_addr + attr_offset + 16, 8)
.ok()
.and_then(|b| b.try_into().ok())
.map_or(0, u64::from_le_bytes);
let is_suspicious = classify_perf_event(event_type, config);
results.push(PerfEventInfo {
pid,
comm: comm.clone(),
event_type,
event_type_name: perf_type_name(event_type).to_string(),
config,
sample_period,
is_suspicious,
});
cursor = match reader.read_bytes(cursor, 8) {
Ok(b) => u64::from_le_bytes(b.try_into().unwrap_or([0u8; 8])),
Err(_) => break,
};
guard += 1;
}
}
}
Ok(results)
}
#[cfg(test)]
mod tests {
use super::*;
use memf_core::object_reader::ObjectReader;
use memf_core::test_builders::{PageTableBuilder, SyntheticPhysMem};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
fn make_no_symbol_reader() -> ObjectReader<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 perf_type_name_hardware() {
assert_eq!(perf_type_name(0), "HARDWARE");
}
#[test]
fn perf_type_name_unknown() {
assert_eq!(perf_type_name(99), "UNKNOWN");
}
#[test]
fn classify_ll_cache_event_suspicious() {
assert!(
classify_perf_event(3, 2),
"LL cache event must be suspicious"
);
}
#[test]
fn classify_l1d_cache_event_suspicious() {
assert!(
classify_perf_event(3, 0),
"L1D cache event must be suspicious"
);
}
#[test]
fn classify_software_event_not_suspicious() {
assert!(
!classify_perf_event(1, 0),
"SOFTWARE event must not be suspicious"
);
}
#[test]
fn classify_raw_pmu_event_suspicious() {
assert!(
classify_perf_event(4, 0xDEAD),
"RAW PMU event must be suspicious"
);
}
#[test]
fn classify_hardware_event_not_suspicious() {
assert!(
!classify_perf_event(0, 1),
"plain HARDWARE event must not be suspicious"
);
}
#[test]
fn walk_perf_events_no_symbol_returns_empty() {
let reader = make_no_symbol_reader();
let result = walk_perf_events(&reader).unwrap();
assert!(
result.is_empty(),
"no init_task symbol → empty vec expected"
);
}
#[test]
fn perf_type_name_software() {
assert_eq!(perf_type_name(1), "SOFTWARE");
}
#[test]
fn perf_type_name_tracepoint() {
assert_eq!(perf_type_name(2), "TRACEPOINT");
}
#[test]
fn perf_type_name_hw_cache() {
assert_eq!(perf_type_name(3), "HW_CACHE");
}
#[test]
fn perf_type_name_raw() {
assert_eq!(perf_type_name(4), "RAW");
}
#[test]
fn perf_type_name_breakpoint() {
assert_eq!(perf_type_name(5), "BREAKPOINT");
}
#[test]
fn classify_hw_cache_config_byte_1_suspicious() {
assert!(
classify_perf_event(3, 1),
"HW_CACHE with config=1 must be suspicious"
);
}
#[test]
fn classify_hw_cache_config_byte_3_not_suspicious() {
assert!(
!classify_perf_event(3, 3),
"HW_CACHE with config byte = 3 must not be suspicious"
);
}
#[test]
fn classify_hw_cache_config_high_byte_not_suspicious() {
assert!(
!classify_perf_event(3, 0xFF03),
"HW_CACHE with low byte > 2 must not be suspicious"
);
}
#[test]
fn classify_tracepoint_not_suspicious() {
assert!(
!classify_perf_event(2, 0),
"TRACEPOINT event must not be suspicious"
);
}
#[test]
fn classify_breakpoint_not_suspicious() {
assert!(
!classify_perf_event(5, 0),
"BREAKPOINT event must not be suspicious"
);
}
#[test]
fn classify_unknown_type_not_suspicious() {
assert!(
!classify_perf_event(99, 0),
"unknown event type must not be suspicious"
);
}
#[test]
fn walk_perf_events_missing_tasks_offset_returns_empty() {
let isf = IsfBuilder::new()
.add_symbol("init_task", 0xFFFF_8888_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_perf_events(&reader).unwrap();
assert!(
result.is_empty(),
"missing task_struct.tasks offset → empty vec expected"
);
}
#[test]
fn walk_perf_events_missing_ctxp_offset_returns_empty() {
let isf = IsfBuilder::new()
.add_symbol("init_task", 0xFFFF_8888_0000_0000)
.add_struct("task_struct", 512)
.add_field("task_struct", "tasks", 8, "pointer")
.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_perf_events(&reader).unwrap();
assert!(
result.is_empty(),
"missing perf_event_ctxp offset → empty vec expected"
);
}
#[test]
fn walk_perf_events_symbol_present_self_pointing_list_returns_empty() {
use memf_core::test_builders::{flags as ptf, SyntheticPhysMem};
let tasks_offset: u64 = 0x10;
let ctxp_offset: u64 = 0x20;
let sym_vaddr: u64 = 0xFFFF_8800_0020_0000;
let sym_paddr: u64 = 0x0040_0000;
let isf = IsfBuilder::new()
.add_symbol("init_task", sym_vaddr)
.add_struct("task_struct", 0x400)
.add_field("task_struct", "tasks", tasks_offset, "pointer")
.add_field("task_struct", "perf_event_ctxp", ctxp_offset, "pointer")
.add_field("task_struct", "pid", 0x30, "unsigned int")
.add_field("task_struct", "comm", 0x38, "char")
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let mut page = [0u8; 4096];
let self_ptr = sym_vaddr + tasks_offset;
page[tasks_offset as usize..tasks_offset as usize + 8]
.copy_from_slice(&self_ptr.to_le_bytes());
let (cr3, mem) = PageTableBuilder::new()
.map_4k(sym_vaddr, sym_paddr, ptf::WRITABLE)
.write_phys(sym_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_perf_events(&reader).unwrap();
assert!(
result.is_empty(),
"self-pointing tasks list with null ctx_ptr → no perf events"
);
}
#[test]
fn walk_perf_events_missing_group_field_offsets_returns_empty() {
use memf_core::test_builders::{flags as ptf, SyntheticPhysMem};
let task_vaddr: u64 = 0xFFFF_8800_0030_0000;
let ctx_vaddr: u64 = 0xFFFF_8800_0031_0000;
let task_paddr: u64 = 0x041_000;
let ctx_paddr: u64 = 0x042_000;
let tasks_offset: u64 = 0x10;
let ctxp_offset: u64 = 0x20;
let pid_offset: u64 = 0x30;
let comm_offset: u64 = 0x38;
let isf = IsfBuilder::new()
.add_symbol("init_task", task_vaddr)
.add_struct("task_struct", 0x400)
.add_field("task_struct", "tasks", tasks_offset, "pointer")
.add_field("task_struct", "perf_event_ctxp", ctxp_offset, "pointer")
.add_field("task_struct", "pid", pid_offset, "unsigned int")
.add_field("task_struct", "comm", comm_offset, "char")
.add_struct("perf_event_context", 0x200)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let mut task_page = [0u8; 4096];
let self_ptr = task_vaddr + tasks_offset;
task_page[tasks_offset as usize..tasks_offset as usize + 8]
.copy_from_slice(&self_ptr.to_le_bytes());
task_page[ctxp_offset as usize..ctxp_offset as usize + 8]
.copy_from_slice(&ctx_vaddr.to_le_bytes());
task_page[pid_offset as usize..pid_offset as usize + 4]
.copy_from_slice(&777u32.to_le_bytes());
let ctx_page = [0u8; 4096];
let (cr3, mem) = PageTableBuilder::new()
.map_4k(task_vaddr, task_paddr, ptf::WRITABLE)
.write_phys(task_paddr, &task_page)
.map_4k(ctx_vaddr, ctx_paddr, ptf::WRITABLE)
.write_phys(ctx_paddr, &ctx_page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
let result = walk_perf_events(&reader).unwrap();
assert!(
result.is_empty(),
"missing group field offsets → inner loop continues → no events"
);
}
#[test]
fn walk_perf_events_empty_group_list_returns_empty() {
use memf_core::test_builders::{flags as ptf, SyntheticPhysMem};
let task_vaddr: u64 = 0xFFFF_8800_0032_0000;
let ctx_vaddr: u64 = 0xFFFF_8800_0033_0000;
let task_paddr: u64 = 0x043_000;
let ctx_paddr: u64 = 0x044_000;
let tasks_offset: u64 = 0x10;
let ctxp_offset: u64 = 0x20;
let pid_offset: u64 = 0x30;
let comm_offset: u64 = 0x38;
let pinned_offset: u64 = 0x10;
let flexible_offset: u64 = 0x18;
let isf = IsfBuilder::new()
.add_symbol("init_task", task_vaddr)
.add_struct("task_struct", 0x400)
.add_field("task_struct", "tasks", tasks_offset, "pointer")
.add_field("task_struct", "perf_event_ctxp", ctxp_offset, "pointer")
.add_field("task_struct", "pid", pid_offset, "unsigned int")
.add_field("task_struct", "comm", comm_offset, "char")
.add_struct("perf_event_context", 0x200)
.add_field(
"perf_event_context",
"pinned_groups",
pinned_offset,
"list_head",
)
.add_field(
"perf_event_context",
"flexible_groups",
flexible_offset,
"list_head",
)
.add_struct("perf_event", 0x200)
.add_field("perf_event", "group_entry", 0u64, "list_head")
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let mut task_page = [0u8; 4096];
let self_ptr = task_vaddr + tasks_offset;
task_page[tasks_offset as usize..tasks_offset as usize + 8]
.copy_from_slice(&self_ptr.to_le_bytes());
task_page[ctxp_offset as usize..ctxp_offset as usize + 8]
.copy_from_slice(&ctx_vaddr.to_le_bytes());
task_page[pid_offset as usize..pid_offset as usize + 4]
.copy_from_slice(&888u32.to_le_bytes());
let pinned_head = ctx_vaddr + pinned_offset;
let flexible_head = ctx_vaddr + flexible_offset;
let mut ctx_page = [0u8; 4096];
ctx_page[pinned_offset as usize..pinned_offset as usize + 8]
.copy_from_slice(&pinned_head.to_le_bytes());
ctx_page[flexible_offset as usize..flexible_offset as usize + 8]
.copy_from_slice(&flexible_head.to_le_bytes());
let (cr3, mem) = PageTableBuilder::new()
.map_4k(task_vaddr, task_paddr, ptf::WRITABLE)
.write_phys(task_paddr, &task_page)
.map_4k(ctx_vaddr, ctx_paddr, ptf::WRITABLE)
.write_phys(ctx_paddr, &ctx_page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
let result = walk_perf_events(&reader).unwrap();
assert!(
result.is_empty(),
"self-pointing group list (empty) → no perf events enumerated"
);
}
#[test]
fn walk_perf_events_tasks_read_fails_returns_empty() {
let isf = IsfBuilder::new()
.add_symbol("init_task", 0xFFFF_DEAD_CAFE_0000) .add_struct("task_struct", 0x400)
.add_field("task_struct", "tasks", 0x10, "pointer")
.add_field("task_struct", "perf_event_ctxp", 0x20, "pointer")
.add_field("task_struct", "pid", 0x30, "unsigned int")
.add_field("task_struct", "comm", 0x38, "char")
.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_perf_events(&reader).unwrap();
assert!(
result.is_empty(),
"unreadable init_task → early return empty"
);
}
#[test]
fn walk_perf_events_one_task_with_one_event_in_pinned_groups() {
use memf_core::test_builders::{flags as ptf, SyntheticPhysMem};
let tasks_offset: u64 = 0x10;
let ctxp_offset: u64 = 0x20;
let pid_offset: u64 = 0x30;
let comm_offset: u64 = 0x38;
let pinned_offset: u64 = 0x00;
let flexible_offset: u64 = 0x08;
let group_entry_offset: u64 = 0x00;
let attr_offset: u64 = 0x20;
let init_vaddr: u64 = 0xFFFF_8800_0080_0000;
let init_paddr: u64 = 0x0080_0000;
let t2_vaddr: u64 = 0xFFFF_8800_0081_0000;
let t2_paddr: u64 = 0x0081_0000;
let ctx_vaddr: u64 = 0xFFFF_8800_0082_0000;
let ctx_paddr: u64 = 0x0082_0000;
let ev_vaddr: u64 = 0xFFFF_8800_0083_0000;
let ev_paddr: u64 = 0x0083_0000;
let mut init_page = [0u8; 4096];
let t2_list_node = t2_vaddr + tasks_offset;
init_page[tasks_offset as usize..tasks_offset as usize + 8]
.copy_from_slice(&t2_list_node.to_le_bytes());
let mut t2_page = [0u8; 4096];
let init_list_node = init_vaddr + tasks_offset;
t2_page[tasks_offset as usize..tasks_offset as usize + 8]
.copy_from_slice(&init_list_node.to_le_bytes());
t2_page[ctxp_offset as usize..ctxp_offset as usize + 8]
.copy_from_slice(&ctx_vaddr.to_le_bytes());
t2_page[pid_offset as usize..pid_offset as usize + 4].copy_from_slice(&42u32.to_le_bytes());
t2_page[comm_offset as usize..comm_offset as usize + 3].copy_from_slice(b"spy");
let mut ctx_page = [0u8; 4096];
let ev_list_node = ev_vaddr + group_entry_offset;
ctx_page[pinned_offset as usize..pinned_offset as usize + 8]
.copy_from_slice(&ev_list_node.to_le_bytes());
let pinned_head = ctx_vaddr + pinned_offset;
let flex_head = ctx_vaddr + flexible_offset;
ctx_page[flexible_offset as usize..flexible_offset as usize + 8]
.copy_from_slice(&flex_head.to_le_bytes());
let mut ev_page = [0u8; 4096];
ev_page[group_entry_offset as usize..group_entry_offset as usize + 8]
.copy_from_slice(&pinned_head.to_le_bytes());
ev_page[attr_offset as usize..attr_offset as usize + 4]
.copy_from_slice(&4u32.to_le_bytes()); ev_page[attr_offset as usize + 8..attr_offset as usize + 16]
.copy_from_slice(&0xDEADu64.to_le_bytes());
ev_page[attr_offset as usize + 16..attr_offset as usize + 24]
.copy_from_slice(&1000u64.to_le_bytes());
let isf = IsfBuilder::new()
.add_symbol("init_task", init_vaddr)
.add_struct("list_head", 0x10)
.add_field("list_head", "next", 0x00u64, "pointer")
.add_struct("task_struct", 0x400)
.add_field("task_struct", "tasks", tasks_offset, "pointer")
.add_field("task_struct", "perf_event_ctxp", ctxp_offset, "pointer")
.add_field("task_struct", "pid", pid_offset, "unsigned int")
.add_field("task_struct", "comm", comm_offset, "char")
.add_struct("perf_event_context", 0x200)
.add_field(
"perf_event_context",
"pinned_groups",
pinned_offset,
"list_head",
)
.add_field(
"perf_event_context",
"flexible_groups",
flexible_offset,
"list_head",
)
.add_struct("perf_event", 0x200)
.add_field("perf_event", "group_entry", group_entry_offset, "list_head")
.add_field("perf_event", "attr", attr_offset, "pointer")
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(init_vaddr, init_paddr, ptf::WRITABLE)
.write_phys(init_paddr, &init_page)
.map_4k(t2_vaddr, t2_paddr, ptf::WRITABLE)
.write_phys(t2_paddr, &t2_page)
.map_4k(ctx_vaddr, ctx_paddr, ptf::WRITABLE)
.write_phys(ctx_paddr, &ctx_page)
.map_4k(ev_vaddr, ev_paddr, ptf::WRITABLE)
.write_phys(ev_paddr, &ev_page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
let result = walk_perf_events(&reader).unwrap();
assert_eq!(
result.len(),
1,
"expected one perf_event from task2's pinned_groups"
);
assert_eq!(result[0].pid, 42);
assert_eq!(result[0].comm, "spy");
assert_eq!(result[0].event_type, 4); assert_eq!(result[0].config, 0xDEAD);
assert_eq!(result[0].sample_period, 1000);
assert!(
result[0].is_suspicious,
"RAW PMU event must be flagged suspicious"
);
}
#[test]
fn perf_event_info_serializes() {
let info = PerfEventInfo {
pid: 12,
comm: "spy".to_string(),
event_type: 4,
event_type_name: "RAW".to_string(),
config: 0xDEAD,
sample_period: 1000,
is_suspicious: true,
};
let json = serde_json::to_string(&info).unwrap();
assert!(json.contains("\"pid\":12"));
assert!(json.contains("RAW"));
assert!(json.contains("\"is_suspicious\":true"));
}
}