use memf_core::object_reader::ObjectReader;
use memf_format::PhysicalMemoryProvider;
use crate::Result;
#[derive(Debug, Clone, serde::Serialize)]
pub struct KernelTimerInfo {
pub address: u64,
pub expires: u64,
pub function: u64,
pub is_periodic: bool,
pub is_suspicious: bool,
}
pub use crate::heuristics::classify_kernel_timer;
const TIMER_WHEEL_GROUPS: usize = 9;
const MAX_TIMERS_PER_VECTOR: usize = 4096;
pub fn walk_kernel_timers<P: PhysicalMemoryProvider>(
reader: &ObjectReader<P>,
) -> Result<Vec<KernelTimerInfo>> {
let timer_bases = reader
.symbols()
.symbol_address("timer_bases")
.or_else(|| reader.symbols().symbol_address("tvec_bases"));
let Some(bases_addr) = timer_bases else {
return Ok(Vec::new());
};
let Some(kernel_start) = reader.symbols().symbol_address("_stext") else {
return Ok(Vec::new());
};
let Some(kernel_end) = reader.symbols().symbol_address("_etext") else {
return Ok(Vec::new());
};
let mut results = Vec::new();
for group in 0..TIMER_WHEEL_GROUPS {
let vector_head =
match reader.read_pointer(bases_addr, "timer_base", &format!("vectors.{group}")) {
Ok(addr) => addr,
Err(_) => continue,
};
if vector_head == 0 {
continue;
}
let timer_addrs = match reader.walk_list(vector_head, "timer_list", "entry") {
Ok(addrs) => addrs,
Err(_) => continue,
};
for (i, &timer_addr) in timer_addrs.iter().enumerate() {
if i >= MAX_TIMERS_PER_VECTOR {
break;
}
let expires = reader
.read_field::<u64>(timer_addr, "timer_list", "expires")
.unwrap_or(0);
let function = reader
.read_pointer(timer_addr, "timer_list", "function")
.unwrap_or(0);
let flags = reader
.read_field::<u32>(timer_addr, "timer_list", "flags")
.unwrap_or(0);
let is_periodic = flags & 1 != 0;
let is_suspicious = classify_kernel_timer(function, kernel_start, kernel_end);
results.push(KernelTimerInfo {
address: timer_addr,
expires,
function,
is_periodic,
is_suspicious,
});
}
}
Ok(results)
}
#[cfg(test)]
mod tests {
use super::*;
use memf_core::test_builders::{flags, PageTableBuilder};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
#[test]
fn classify_kernel_timer_in_kernel_text_is_benign() {
let kernel_start = 0xFFFF_8000_0000_0000u64;
let kernel_end = 0xFFFF_8000_00FF_FFFFu64;
let function = kernel_start + 0x1000;
assert!(
!classify_kernel_timer(function, kernel_start, kernel_end),
"function inside kernel text should not be suspicious"
);
}
#[test]
fn classify_kernel_timer_outside_kernel_text_is_suspicious() {
let kernel_start = 0xFFFF_8000_0000_0000u64;
let kernel_end = 0xFFFF_8000_00FF_FFFFu64;
let function = 0xFFFF_C900_DEAD_BEEFu64;
assert!(
classify_kernel_timer(function, kernel_start, kernel_end),
"function outside kernel text should be suspicious"
);
}
#[test]
fn classify_kernel_timer_zero_is_not_suspicious() {
let kernel_start = 0xFFFF_8000_0000_0000u64;
let kernel_end = 0xFFFF_8000_00FF_FFFFu64;
assert!(
!classify_kernel_timer(0, kernel_start, kernel_end),
"function == 0 (unset timer) should not be suspicious"
);
}
#[test]
fn classify_kernel_timer_module_space_is_suspicious() {
let kernel_start = 0xFFFF_8000_0000_0000u64;
let kernel_end = 0xFFFF_8000_00FF_FFFFu64;
let function = 0xFFFF_FFFF_C000_0000u64;
assert!(
classify_kernel_timer(function, kernel_start, kernel_end),
"function in module space should be suspicious"
);
}
#[test]
fn classify_kernel_timer_at_kernel_boundary_is_benign() {
let kernel_start = 0xFFFF_8000_0000_0000u64;
let kernel_end = 0xFFFF_8000_00FF_FFFFu64;
assert!(
!classify_kernel_timer(kernel_start, kernel_start, kernel_end),
"function at kernel_start should be benign"
);
assert!(
!classify_kernel_timer(kernel_end, kernel_start, kernel_end),
"function at kernel_end should be benign"
);
}
#[test]
fn walk_kernel_timers_no_symbol_returns_empty() {
let isf = IsfBuilder::new()
.add_struct("timer_list", 64)
.add_field("timer_list", "entry", 0, "list_head")
.add_field("timer_list", "expires", 16, "unsigned long")
.add_field("timer_list", "function", 24, "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 results = walk_kernel_timers(&reader).unwrap();
assert!(results.is_empty(), "missing symbol should yield empty vec");
}
#[test]
fn walk_kernel_timers_missing_stext_returns_empty() {
let isf = IsfBuilder::new()
.add_struct("timer_list", 64)
.add_field("timer_list", "entry", 0, "list_head")
.add_field("timer_list", "expires", 16, "unsigned long")
.add_field("timer_list", "function", 24, "pointer")
.add_symbol("timer_bases", 0xFFFF_8000_0010_0000)
.add_symbol("_etext", 0xFFFF_8000_00FF_FFFF)
.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 results = walk_kernel_timers(&reader).unwrap();
assert!(results.is_empty(), "missing _stext should yield empty vec");
}
#[test]
fn walk_kernel_timers_missing_etext_returns_empty() {
let isf = IsfBuilder::new()
.add_struct("timer_list", 64)
.add_field("timer_list", "entry", 0, "list_head")
.add_field("timer_list", "expires", 16, "unsigned long")
.add_field("timer_list", "function", 24, "pointer")
.add_symbol("timer_bases", 0xFFFF_8000_0010_0000)
.add_symbol("_stext", 0xFFFF_8000_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 results = walk_kernel_timers(&reader).unwrap();
assert!(results.is_empty(), "missing _etext should yield empty vec");
}
#[test]
fn walk_kernel_timers_symbol_present_all_vectors_zero() {
let bases_vaddr: u64 = 0xFFFF_8800_0040_0000;
let bases_paddr: u64 = 0x0050_0000;
let page = [0u8; 4096];
let mut isf_builder = IsfBuilder::new()
.add_struct("timer_base", 512)
.add_struct("timer_list", 64)
.add_field("timer_list", "entry", 0, "pointer")
.add_field("timer_list", "expires", 16, "unsigned long")
.add_field("timer_list", "function", 24, "pointer")
.add_symbol("timer_bases", bases_vaddr)
.add_symbol("_stext", 0xFFFF_8000_0000_0000u64)
.add_symbol("_etext", 0xFFFF_8000_00FF_FFFFu64);
for i in 0..TIMER_WHEEL_GROUPS {
isf_builder =
isf_builder.add_field("timer_base", &format!("vectors.{i}"), 0, "pointer");
}
let isf = isf_builder.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(bases_vaddr, bases_paddr, flags::WRITABLE)
.write_phys(bases_paddr, &page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = walk_kernel_timers(&reader).unwrap_or_default();
assert!(
result.is_empty(),
"all-zero vector heads should produce no timer entries"
);
}
#[test]
fn walk_kernel_timers_uses_tvec_bases_fallback() {
let isf = IsfBuilder::new()
.add_struct("timer_list", 64)
.add_field("timer_list", "entry", 0, "list_head")
.add_field("timer_list", "expires", 16, "unsigned long")
.add_field("timer_list", "function", 24, "pointer")
.add_symbol("tvec_bases", 0xFFFF_8000_0020_0000u64)
.add_symbol("_stext", 0xFFFF_8000_0000_0000u64)
.add_symbol("_etext", 0xFFFF_8000_00FF_FFFFu64)
.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 results = walk_kernel_timers(&reader).unwrap_or_default();
assert!(
results.is_empty(),
"tvec_bases fallback with no vectors → empty"
);
}
#[test]
fn walk_kernel_timers_vector_nonzero_but_walk_list_fails() {
let bases_vaddr: u64 = 0xFFFF_8800_0060_0000;
let bases_paddr: u64 = 0x0060_0000;
let mut page = [0u8; 4096];
let fake_list_addr: u64 = 0xFFFF_DEAD_0000_0000; page[0..8].copy_from_slice(&fake_list_addr.to_le_bytes());
let mut isf_builder = IsfBuilder::new()
.add_struct("timer_base", 512)
.add_struct("timer_list", 64)
.add_field("timer_list", "entry", 0, "pointer")
.add_field("timer_list", "expires", 16, "unsigned long")
.add_field("timer_list", "function", 24, "pointer")
.add_symbol("timer_bases", bases_vaddr)
.add_symbol("_stext", 0xFFFF_8000_0000_0000u64)
.add_symbol("_etext", 0xFFFF_8000_00FF_FFFFu64);
for i in 0..TIMER_WHEEL_GROUPS {
isf_builder =
isf_builder.add_field("timer_base", &format!("vectors.{i}"), 0u64, "pointer");
}
let isf = isf_builder.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(bases_vaddr, bases_paddr, flags::WRITABLE)
.write_phys(bases_paddr, &page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = walk_kernel_timers(&reader).unwrap_or_default();
assert!(
result.is_empty(),
"failed walk_list → Err → continue → empty result"
);
}
#[test]
fn walk_kernel_timers_with_one_timer_in_vector() {
let bases_vaddr: u64 = 0xFFFF_8800_00D0_0000;
let bases_paddr: u64 = 0x00D0_0000;
let listhead_vaddr: u64 = 0xFFFF_8800_00D1_0000;
let listhead_paddr: u64 = 0x00D1_0000;
let timer_vaddr: u64 = 0xFFFF_8800_00D2_0000;
let timer_paddr: u64 = 0x00D2_0000;
let entry_offset: u64 = 0x00; let expires_offset: u64 = 0x10;
let function_offset: u64 = 0x18;
let kernel_start: u64 = 0xFFFF_8000_0000_0000;
let kernel_end: u64 = 0xFFFF_8000_00FF_FFFF;
let suspicious_fn: u64 = 0xFFFF_C900_DEAD_BEEFu64;
let mut bases_page = [0u8; 4096];
bases_page[0..8].copy_from_slice(&listhead_vaddr.to_le_bytes());
let timer_entry_node = timer_vaddr + entry_offset;
let mut listhead_page = [0u8; 4096];
listhead_page[0..8].copy_from_slice(&timer_entry_node.to_le_bytes());
let mut timer_page = [0u8; 4096];
timer_page[entry_offset as usize..entry_offset as usize + 8]
.copy_from_slice(&listhead_vaddr.to_le_bytes());
timer_page[expires_offset as usize..expires_offset as usize + 8]
.copy_from_slice(&9999u64.to_le_bytes());
timer_page[function_offset as usize..function_offset as usize + 8]
.copy_from_slice(&suspicious_fn.to_le_bytes());
let mut isf_builder = IsfBuilder::new()
.add_struct("list_head", 0x10)
.add_field("list_head", "next", 0x00u64, "pointer")
.add_struct("timer_base", 512)
.add_struct("timer_list", 64)
.add_field("timer_list", "entry", entry_offset, "pointer")
.add_field("timer_list", "expires", expires_offset, "unsigned long")
.add_field("timer_list", "function", function_offset, "pointer")
.add_symbol("timer_bases", bases_vaddr)
.add_symbol("_stext", kernel_start)
.add_symbol("_etext", kernel_end);
for i in 0..TIMER_WHEEL_GROUPS {
let field_offset: u64 = if i == 0 { 0 } else { 8 + i as u64 * 8 };
isf_builder = isf_builder.add_field(
"timer_base",
&format!("vectors.{i}"),
field_offset,
"pointer",
);
}
let isf = isf_builder.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(bases_vaddr, bases_paddr, flags::WRITABLE)
.write_phys(bases_paddr, &bases_page)
.map_4k(listhead_vaddr, listhead_paddr, flags::WRITABLE)
.write_phys(listhead_paddr, &listhead_page)
.map_4k(timer_vaddr, timer_paddr, flags::WRITABLE)
.write_phys(timer_paddr, &timer_page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = walk_kernel_timers(&reader).unwrap();
assert!(!result.is_empty(), "should find at least one timer");
let timer = &result[0];
assert_eq!(timer.expires, 9999);
assert_eq!(timer.function, suspicious_fn);
assert!(
timer.is_suspicious,
"function outside kernel text must be suspicious"
);
}
fn walk_one_timer_with_flags(flags_value: u32) -> KernelTimerInfo {
let bases_vaddr: u64 = 0xFFFF_8800_00E0_0000;
let bases_paddr: u64 = 0x00E0_0000;
let listhead_vaddr: u64 = 0xFFFF_8800_00E1_0000;
let listhead_paddr: u64 = 0x00E1_0000;
let timer_vaddr: u64 = 0xFFFF_8800_00E2_0000;
let timer_paddr: u64 = 0x00E2_0000;
let entry_offset: u64 = 0x00;
let expires_offset: u64 = 0x10;
let function_offset: u64 = 0x18;
let flags_offset: u64 = 0x20;
let kernel_start: u64 = 0xFFFF_8000_0000_0000;
let kernel_end: u64 = 0xFFFF_8000_00FF_FFFF;
let benign_fn: u64 = kernel_start + 0x1000;
let mut bases_page = [0u8; 4096];
bases_page[0..8].copy_from_slice(&listhead_vaddr.to_le_bytes());
let timer_entry_node = timer_vaddr + entry_offset;
let mut listhead_page = [0u8; 4096];
listhead_page[0..8].copy_from_slice(&timer_entry_node.to_le_bytes());
let mut timer_page = [0u8; 4096];
timer_page[entry_offset as usize..entry_offset as usize + 8]
.copy_from_slice(&listhead_vaddr.to_le_bytes());
timer_page[expires_offset as usize..expires_offset as usize + 8]
.copy_from_slice(&1234u64.to_le_bytes());
timer_page[function_offset as usize..function_offset as usize + 8]
.copy_from_slice(&benign_fn.to_le_bytes());
timer_page[flags_offset as usize..flags_offset as usize + 4]
.copy_from_slice(&flags_value.to_le_bytes());
let mut isf_builder = IsfBuilder::new()
.add_struct("list_head", 0x10)
.add_field("list_head", "next", 0x00u64, "pointer")
.add_struct("timer_base", 512)
.add_struct("timer_list", 64)
.add_field("timer_list", "entry", entry_offset, "pointer")
.add_field("timer_list", "expires", expires_offset, "unsigned long")
.add_field("timer_list", "function", function_offset, "pointer")
.add_field("timer_list", "flags", flags_offset, "unsigned int")
.add_symbol("timer_bases", bases_vaddr)
.add_symbol("_stext", kernel_start)
.add_symbol("_etext", kernel_end);
for i in 0..TIMER_WHEEL_GROUPS {
let field_offset: u64 = if i == 0 { 0 } else { 8 + i as u64 * 8 };
isf_builder = isf_builder.add_field(
"timer_base",
&format!("vectors.{i}"),
field_offset,
"pointer",
);
}
let isf = isf_builder.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(bases_vaddr, bases_paddr, flags::WRITABLE)
.write_phys(bases_paddr, &bases_page)
.map_4k(listhead_vaddr, listhead_paddr, flags::WRITABLE)
.write_phys(listhead_paddr, &listhead_page)
.map_4k(timer_vaddr, timer_paddr, flags::WRITABLE)
.write_phys(timer_paddr, &timer_page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let mut result = walk_kernel_timers(&reader).unwrap();
assert!(!result.is_empty(), "expected at least one timer");
result.remove(0)
}
#[test]
fn walk_kernel_timers_is_periodic_true_when_deferrable_bit_set() {
let timer = walk_one_timer_with_flags(0x1);
assert!(
timer.is_periodic,
"flags & 1 != 0 (TIMER_DEFERRABLE) must set is_periodic = true"
);
}
#[test]
fn walk_kernel_timers_is_periodic_false_when_flags_zero() {
let timer = walk_one_timer_with_flags(0x0);
assert!(
!timer.is_periodic,
"flags == 0 must leave is_periodic = false"
);
}
#[test]
fn walk_kernel_timers_is_periodic_true_when_other_bits_plus_deferrable() {
let timer = walk_one_timer_with_flags(0x5);
assert!(
timer.is_periodic,
"flags = 0x5 has bit 0 set → is_periodic = true"
);
}
#[test]
fn walk_kernel_timers_is_periodic_false_when_only_non_deferrable_bits_set() {
let timer = walk_one_timer_with_flags(0x4);
assert!(
!timer.is_periodic,
"flags = 0x4 (bit 0 clear) → is_periodic = false"
);
}
#[test]
fn classify_kernel_timer_just_below_kernel_start_is_suspicious() {
let kernel_start = 0xFFFF_8000_0000_0000u64;
let kernel_end = 0xFFFF_8000_00FF_FFFFu64;
let function = kernel_start - 1;
assert!(
classify_kernel_timer(function, kernel_start, kernel_end),
"function just below kernel_start should be suspicious"
);
}
#[test]
fn classify_kernel_timer_just_above_kernel_end_is_suspicious() {
let kernel_start = 0xFFFF_8000_0000_0000u64;
let kernel_end = 0xFFFF_8000_00FF_FFFFu64;
let function = kernel_end + 1;
assert!(
classify_kernel_timer(function, kernel_start, kernel_end),
"function just above kernel_end should be suspicious"
);
}
}