use memf_core::object_reader::ObjectReader;
use memf_format::PhysicalMemoryProvider;
use crate::Result;
const MAX_RESOURCES: usize = 10_000;
#[derive(Debug, Clone, serde::Serialize)]
pub struct IoMemRegion {
pub start: u64,
pub end: u64,
pub name: String,
pub flags: u64,
pub depth: u32,
pub is_suspicious: bool,
}
pub use crate::heuristics::classify_iomem;
pub fn walk_iomem_regions<P: PhysicalMemoryProvider>(
reader: &ObjectReader<P>,
) -> Result<Vec<IoMemRegion>> {
let root_addr = match reader.symbols().symbol_address("iomem_resource") {
Some(addr) => addr,
None => return Ok(Vec::new()),
};
let start_offset = reader
.symbols()
.field_offset("resource", "start")
.unwrap_or(0x00);
let end_offset = reader
.symbols()
.field_offset("resource", "end")
.unwrap_or(0x08);
let flags_offset = reader
.symbols()
.field_offset("resource", "flags")
.unwrap_or(0x10);
let name_offset = reader
.symbols()
.field_offset("resource", "name")
.unwrap_or(0x18);
let child_offset = reader
.symbols()
.field_offset("resource", "child")
.unwrap_or(0x28);
let sibling_offset = reader
.symbols()
.field_offset("resource", "sibling")
.unwrap_or(0x20);
let mut regions = Vec::new();
let first_child = read_ptr(reader, root_addr + child_offset);
if first_child == 0 {
return Ok(Vec::new());
}
let mut stack: Vec<(u64, u32)> = vec![(first_child, 0)];
let mut seen = std::collections::HashSet::new();
while let Some((addr, depth)) = stack.pop() {
if addr == 0 || regions.len() >= MAX_RESOURCES {
continue;
}
if !seen.insert(addr) {
continue;
}
let start = read_u64(reader, addr + start_offset);
let end = read_u64(reader, addr + end_offset);
let flags = read_u64(reader, addr + flags_offset);
let name_ptr = read_ptr(reader, addr + name_offset);
let name = if name_ptr != 0 {
match reader.read_bytes(name_ptr, 256) {
Ok(bytes) => {
let nul = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
String::from_utf8_lossy(&bytes[..nul]).into_owned()
}
Err(_) => String::new(),
}
} else {
String::new()
};
let is_suspicious = classify_iomem(&name, start, end);
regions.push(IoMemRegion {
start,
end,
name,
flags,
depth,
is_suspicious,
});
let sibling = read_ptr(reader, addr + sibling_offset);
if sibling != 0 {
stack.push((sibling, depth));
}
let child = read_ptr(reader, addr + child_offset);
if child != 0 {
stack.push((child, depth + 1));
}
}
Ok(regions)
}
fn read_u64<P: PhysicalMemoryProvider>(reader: &ObjectReader<P>, addr: u64) -> u64 {
match reader.read_bytes(addr, 8) {
Ok(b) if b.len() == 8 => b[..8].try_into().map_or(0, u64::from_le_bytes),
_ => 0,
}
}
fn read_ptr<P: PhysicalMemoryProvider>(reader: &ObjectReader<P>, addr: u64) -> u64 {
read_u64(reader, addr)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classify_normal_system_ram_benign() {
assert!(!classify_iomem("System RAM", 0x0010_0000, 0x7FFF_FFFF));
}
#[test]
fn classify_empty_name_large_region_suspicious() {
assert!(classify_iomem("", 0x0, 0x0020_0000)); }
#[test]
fn classify_empty_name_small_region_benign() {
assert!(!classify_iomem("", 0x0, 0x100)); }
#[test]
fn classify_control_chars_in_name_suspicious() {
assert!(classify_iomem("System\x00RAM", 0x0, 0x1000));
}
#[test]
fn classify_non_ascii_name_suspicious() {
assert!(classify_iomem("Syst\u{00e9}m RAM", 0x0, 0x1000));
}
#[test]
fn classify_kernel_text_overlap_not_named_kernel_code_suspicious() {
assert!(classify_iomem(
"Evil Region",
0xffff_ffff_8100_0000,
0xffff_ffff_8180_0000,
));
}
#[test]
fn classify_kernel_code_region_benign() {
assert!(!classify_iomem(
"Kernel code",
0xffff_ffff_8100_0000,
0xffff_ffff_8180_0000,
));
}
#[test]
fn classify_acpi_tables_benign() {
assert!(!classify_iomem("ACPI Tables", 0xBFFE_0000, 0xBFFF_FFFF));
}
#[test]
fn classify_pci_mmio_benign() {
assert!(!classify_iomem("PCI Bus 0000:00", 0xE000_0000, 0xEFFF_FFFF));
}
#[test]
fn walk_iomem_no_symbol_returns_empty() {
use memf_core::test_builders::{flags, PageTableBuilder};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
let isf = IsfBuilder::new().build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let vaddr: u64 = 0xFFFF_8000_0010_0000;
let paddr: u64 = 0x0080_0000;
let ptb = PageTableBuilder::new().map_4k(vaddr, paddr, flags::WRITABLE);
let (cr3, mem) = ptb.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = walk_iomem_regions(&reader).unwrap();
assert!(result.is_empty(), "missing symbol should yield empty vec");
}
#[test]
fn classify_empty_name_exactly_1mib_not_suspicious() {
let size: u64 = 1024 * 1024;
assert!(!classify_iomem("", 0, size));
}
#[test]
fn classify_empty_name_1mib_plus_1_suspicious() {
let size: u64 = 1024 * 1024 + 1;
assert!(classify_iomem("", 0, size));
}
#[test]
fn classify_empty_name_small_region_explicit_benign() {
assert!(!classify_iomem("", 100, 100)); }
#[test]
fn classify_named_region_small_benign() {
assert!(!classify_iomem("Reserved", 0, 0x0100_0000)); }
#[test]
fn classify_kernel_text_overlap_exact_boundary_suspicious() {
const KERNEL_TEXT_START: u64 = 0xffff_ffff_8100_0000;
const KERNEL_TEXT_END: u64 = 0xffff_ffff_8200_0000;
assert!(classify_iomem("Other", KERNEL_TEXT_START, KERNEL_TEXT_END));
}
#[test]
fn classify_region_just_before_kernel_text_benign() {
const KERNEL_TEXT_START: u64 = 0xffff_ffff_8100_0000;
assert!(!classify_iomem(
"Anything",
0xffff_ffff_8000_0000,
KERNEL_TEXT_START
));
}
#[test]
fn classify_region_just_after_kernel_text_benign() {
const KERNEL_TEXT_END: u64 = 0xffff_ffff_8200_0000;
assert!(!classify_iomem(
"Anything",
KERNEL_TEXT_END,
KERNEL_TEXT_END + 0x1000
));
}
#[test]
fn classify_kernel_code_partial_overlap_benign() {
const KERNEL_TEXT_START: u64 = 0xffff_ffff_8100_0000;
assert!(!classify_iomem(
"Kernel code",
KERNEL_TEXT_START,
KERNEL_TEXT_START + 0x1000
));
}
#[test]
fn classify_tab_char_in_name_suspicious() {
assert!(classify_iomem("System\tRAM", 0, 0x1000));
}
#[test]
fn classify_newline_char_in_name_suspicious() {
assert!(classify_iomem("Sys\nRAM", 0, 0x1000));
}
#[test]
fn classify_saturating_sub_overflow_protection() {
assert!(!classify_iomem("", 0x1000, 0x0)); }
#[test]
fn walk_iomem_symbol_present_no_children_returns_empty() {
use memf_core::test_builders::{flags as ptf, PageTableBuilder};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
let root_vaddr: u64 = 0xFFFF_8800_00A0_0000;
let root_paddr: u64 = 0x00A0_0000;
let isf = IsfBuilder::new()
.add_symbol("iomem_resource", root_vaddr)
.add_struct("resource", 0x60)
.add_field("resource", "start", 0x00, "unsigned long")
.add_field("resource", "end", 0x08, "unsigned long")
.add_field("resource", "flags", 0x10, "unsigned long")
.add_field("resource", "name", 0x18, "pointer")
.add_field("resource", "sibling", 0x20, "pointer")
.add_field("resource", "child", 0x28, "pointer")
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let page = [0u8; 4096];
let (cr3, mem) = PageTableBuilder::new()
.map_4k(root_vaddr, root_paddr, ptf::WRITABLE)
.write_phys(root_paddr, &page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = walk_iomem_regions(&reader).unwrap();
assert!(
result.is_empty(),
"iomem_resource with zero child pointer → no regions"
);
}
#[test]
fn walk_iomem_symbol_present_with_one_child_returns_entry() {
use memf_core::test_builders::{flags as ptf, PageTableBuilder};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
let root_vaddr: u64 = 0xFFFF_8800_00B0_0000;
let root_paddr: u64 = 0x00B0_0000;
let child_vaddr: u64 = 0xFFFF_8800_00B1_0000;
let child_paddr: u64 = 0x00B1_0000;
let mut root_page = [0u8; 4096];
root_page[0x28..0x30].copy_from_slice(&child_vaddr.to_le_bytes());
let mut child_page = [0u8; 4096];
child_page[0x00..0x08].copy_from_slice(&0x1000u64.to_le_bytes()); child_page[0x08..0x10].copy_from_slice(&0x2000u64.to_le_bytes()); child_page[0x10..0x18].copy_from_slice(&0x0200u64.to_le_bytes());
let isf = IsfBuilder::new()
.add_symbol("iomem_resource", root_vaddr)
.add_struct("resource", 0x60)
.add_field("resource", "start", 0x00u64, "unsigned long")
.add_field("resource", "end", 0x08u64, "unsigned long")
.add_field("resource", "flags", 0x10u64, "unsigned long")
.add_field("resource", "name", 0x18u64, "pointer")
.add_field("resource", "sibling", 0x20u64, "pointer")
.add_field("resource", "child", 0x28u64, "pointer")
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(root_vaddr, root_paddr, ptf::WRITABLE)
.write_phys(root_paddr, &root_page)
.map_4k(child_vaddr, child_paddr, ptf::WRITABLE)
.write_phys(child_paddr, &child_page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = walk_iomem_regions(&reader).unwrap_or_default();
assert_eq!(result.len(), 1, "should find exactly one resource entry");
assert_eq!(result[0].start, 0x1000);
assert_eq!(result[0].end, 0x2000);
assert_eq!(result[0].flags, 0x200);
assert_eq!(result[0].depth, 0);
assert!(!result[0].is_suspicious);
}
#[test]
fn walk_iomem_symbol_present_child_with_sibling() {
use memf_core::test_builders::{flags as ptf, PageTableBuilder};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
let root_vaddr: u64 = 0xFFFF_8800_00C0_0000;
let root_paddr: u64 = 0x00C0_0000;
let child_a_vaddr: u64 = 0xFFFF_8800_00C1_0000;
let child_a_paddr: u64 = 0x00C1_0000;
let child_b_vaddr: u64 = 0xFFFF_8800_00C2_0000;
let child_b_paddr: u64 = 0x00C2_0000;
let mut root_page = [0u8; 4096];
root_page[0x28..0x30].copy_from_slice(&child_a_vaddr.to_le_bytes());
let mut a_page = [0u8; 4096];
a_page[0x00..0x08].copy_from_slice(&0x0001_0000u64.to_le_bytes());
a_page[0x08..0x10].copy_from_slice(&0x0002_0000u64.to_le_bytes());
a_page[0x20..0x28].copy_from_slice(&child_b_vaddr.to_le_bytes());
let mut b_page = [0u8; 4096];
b_page[0x00..0x08].copy_from_slice(&0x0003_0000u64.to_le_bytes());
b_page[0x08..0x10].copy_from_slice(&0x0004_0000u64.to_le_bytes());
let isf = IsfBuilder::new()
.add_symbol("iomem_resource", root_vaddr)
.add_struct("resource", 0x60)
.add_field("resource", "start", 0x00u64, "unsigned long")
.add_field("resource", "end", 0x08u64, "unsigned long")
.add_field("resource", "flags", 0x10u64, "unsigned long")
.add_field("resource", "name", 0x18u64, "pointer")
.add_field("resource", "sibling", 0x20u64, "pointer")
.add_field("resource", "child", 0x28u64, "pointer")
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(root_vaddr, root_paddr, ptf::WRITABLE)
.write_phys(root_paddr, &root_page)
.map_4k(child_a_vaddr, child_a_paddr, ptf::WRITABLE)
.write_phys(child_a_paddr, &a_page)
.map_4k(child_b_vaddr, child_b_paddr, ptf::WRITABLE)
.write_phys(child_b_paddr, &b_page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = walk_iomem_regions(&reader).unwrap_or_default();
assert_eq!(result.len(), 2, "should find both sibling resource entries");
}
#[test]
fn walk_iomem_symbol_present_nested_child() {
use memf_core::test_builders::{flags as ptf, PageTableBuilder};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
let root_vaddr: u64 = 0xFFFF_8800_00D0_0000;
let root_paddr: u64 = 0x00D0_0000;
let child_vaddr: u64 = 0xFFFF_8800_00D1_0000;
let child_paddr: u64 = 0x00D1_0000;
let grandchild_vaddr: u64 = 0xFFFF_8800_00D2_0000;
let grandchild_paddr: u64 = 0x00D2_0000;
let mut root_page = [0u8; 4096];
root_page[0x28..0x30].copy_from_slice(&child_vaddr.to_le_bytes());
let mut child_page = [0u8; 4096];
child_page[0x00..0x08].copy_from_slice(&0x1000u64.to_le_bytes());
child_page[0x08..0x10].copy_from_slice(&0x2000u64.to_le_bytes());
child_page[0x28..0x30].copy_from_slice(&grandchild_vaddr.to_le_bytes());
let mut gc_page = [0u8; 4096];
gc_page[0x00..0x08].copy_from_slice(&0x5000u64.to_le_bytes());
gc_page[0x08..0x10].copy_from_slice(&0x6000u64.to_le_bytes());
let isf = IsfBuilder::new()
.add_symbol("iomem_resource", root_vaddr)
.add_struct("resource", 0x60)
.add_field("resource", "start", 0x00u64, "unsigned long")
.add_field("resource", "end", 0x08u64, "unsigned long")
.add_field("resource", "flags", 0x10u64, "unsigned long")
.add_field("resource", "name", 0x18u64, "pointer")
.add_field("resource", "sibling", 0x20u64, "pointer")
.add_field("resource", "child", 0x28u64, "pointer")
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(root_vaddr, root_paddr, ptf::WRITABLE)
.write_phys(root_paddr, &root_page)
.map_4k(child_vaddr, child_paddr, ptf::WRITABLE)
.write_phys(child_paddr, &child_page)
.map_4k(grandchild_vaddr, grandchild_paddr, ptf::WRITABLE)
.write_phys(grandchild_paddr, &gc_page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = walk_iomem_regions(&reader).unwrap_or_default();
assert_eq!(result.len(), 2, "child + grandchild = 2 entries");
let gc = result
.iter()
.find(|r| r.start == 0x5000)
.expect("grandchild entry");
assert_eq!(gc.depth, 1);
}
#[test]
fn io_mem_region_clone_debug_serialize() {
let region = IoMemRegion {
start: 0x1000,
end: 0x2000,
name: "System RAM".to_string(),
flags: 0x200,
depth: 0,
is_suspicious: false,
};
let cloned = region.clone();
assert_eq!(cloned.start, 0x1000);
assert_eq!(cloned.depth, 0);
let dbg = format!("{cloned:?}");
assert!(dbg.contains("System RAM"));
let json = serde_json::to_string(&cloned).unwrap();
assert!(json.contains("\"start\":4096"));
assert!(json.contains("\"is_suspicious\":false"));
}
}