1use memf_core::object_reader::ObjectReader;
13use memf_format::PhysicalMemoryProvider;
14
15use crate::Result;
16
17const MAX_RESOURCES: usize = 10_000;
19
20#[derive(Debug, Clone, serde::Serialize)]
22pub struct IoMemRegion {
23 pub start: u64,
25 pub end: u64,
27 pub name: String,
29 pub flags: u64,
31 pub depth: u32,
33 pub is_suspicious: bool,
35}
36
37pub use crate::heuristics::classify_iomem;
45
46pub fn walk_iomem_regions<P: PhysicalMemoryProvider>(
51 reader: &ObjectReader<P>,
52) -> Result<Vec<IoMemRegion>> {
53 let root_addr = match reader.symbols().symbol_address("iomem_resource") {
55 Some(addr) => addr,
56 None => return Ok(Vec::new()),
57 };
58
59 let start_offset = reader
61 .symbols()
62 .field_offset("resource", "start")
63 .unwrap_or(0x00);
64 let end_offset = reader
65 .symbols()
66 .field_offset("resource", "end")
67 .unwrap_or(0x08);
68 let flags_offset = reader
69 .symbols()
70 .field_offset("resource", "flags")
71 .unwrap_or(0x10);
72 let name_offset = reader
73 .symbols()
74 .field_offset("resource", "name")
75 .unwrap_or(0x18);
76 let child_offset = reader
77 .symbols()
78 .field_offset("resource", "child")
79 .unwrap_or(0x28);
80 let sibling_offset = reader
81 .symbols()
82 .field_offset("resource", "sibling")
83 .unwrap_or(0x20);
84
85 let mut regions = Vec::new();
86
87 let first_child = read_ptr(reader, root_addr + child_offset);
90 if first_child == 0 {
91 return Ok(Vec::new());
92 }
93
94 let mut stack: Vec<(u64, u32)> = vec![(first_child, 0)];
95 let mut seen = std::collections::HashSet::new();
96
97 while let Some((addr, depth)) = stack.pop() {
98 if addr == 0 || regions.len() >= MAX_RESOURCES {
99 continue;
100 }
101 if !seen.insert(addr) {
102 continue;
103 }
104
105 let start = read_u64(reader, addr + start_offset);
106 let end = read_u64(reader, addr + end_offset);
107 let flags = read_u64(reader, addr + flags_offset);
108
109 let name_ptr = read_ptr(reader, addr + name_offset);
111 let name = if name_ptr != 0 {
112 match reader.read_bytes(name_ptr, 256) {
113 Ok(bytes) => {
114 let nul = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
115 String::from_utf8_lossy(&bytes[..nul]).into_owned()
116 }
117 Err(_) => String::new(),
118 }
119 } else {
120 String::new()
121 };
122
123 let is_suspicious = classify_iomem(&name, start, end);
124
125 regions.push(IoMemRegion {
126 start,
127 end,
128 name,
129 flags,
130 depth,
131 is_suspicious,
132 });
133
134 let sibling = read_ptr(reader, addr + sibling_offset);
136 if sibling != 0 {
137 stack.push((sibling, depth));
138 }
139
140 let child = read_ptr(reader, addr + child_offset);
142 if child != 0 {
143 stack.push((child, depth + 1));
144 }
145 }
146
147 Ok(regions)
148}
149
150fn read_u64<P: PhysicalMemoryProvider>(reader: &ObjectReader<P>, addr: u64) -> u64 {
152 match reader.read_bytes(addr, 8) {
153 Ok(b) if b.len() == 8 => b[..8].try_into().map_or(0, u64::from_le_bytes),
154 _ => 0,
155 }
156}
157
158fn read_ptr<P: PhysicalMemoryProvider>(reader: &ObjectReader<P>, addr: u64) -> u64 {
160 read_u64(reader, addr)
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
172 fn classify_normal_system_ram_benign() {
173 assert!(!classify_iomem("System RAM", 0x0010_0000, 0x7FFF_FFFF));
175 }
176
177 #[test]
178 fn classify_empty_name_large_region_suspicious() {
179 assert!(classify_iomem("", 0x0, 0x0020_0000)); }
182
183 #[test]
184 fn classify_empty_name_small_region_benign() {
185 assert!(!classify_iomem("", 0x0, 0x100)); }
188
189 #[test]
190 fn classify_control_chars_in_name_suspicious() {
191 assert!(classify_iomem("System\x00RAM", 0x0, 0x1000));
193 }
194
195 #[test]
196 fn classify_non_ascii_name_suspicious() {
197 assert!(classify_iomem("Syst\u{00e9}m RAM", 0x0, 0x1000));
199 }
200
201 #[test]
202 fn classify_kernel_text_overlap_not_named_kernel_code_suspicious() {
203 assert!(classify_iomem(
205 "Evil Region",
206 0xffff_ffff_8100_0000,
207 0xffff_ffff_8180_0000,
208 ));
209 }
210
211 #[test]
212 fn classify_kernel_code_region_benign() {
213 assert!(!classify_iomem(
215 "Kernel code",
216 0xffff_ffff_8100_0000,
217 0xffff_ffff_8180_0000,
218 ));
219 }
220
221 #[test]
222 fn classify_acpi_tables_benign() {
223 assert!(!classify_iomem("ACPI Tables", 0xBFFE_0000, 0xBFFF_FFFF));
225 }
226
227 #[test]
228 fn classify_pci_mmio_benign() {
229 assert!(!classify_iomem("PCI Bus 0000:00", 0xE000_0000, 0xEFFF_FFFF));
231 }
232
233 #[test]
238 fn walk_iomem_no_symbol_returns_empty() {
239 use memf_core::test_builders::{flags, PageTableBuilder};
240 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
241 use memf_symbols::isf::IsfResolver;
242 use memf_symbols::test_builders::IsfBuilder;
243
244 let isf = IsfBuilder::new().build_json();
245 let resolver = IsfResolver::from_value(&isf).unwrap();
246 let vaddr: u64 = 0xFFFF_8000_0010_0000;
247 let paddr: u64 = 0x0080_0000;
248 let ptb = PageTableBuilder::new().map_4k(vaddr, paddr, flags::WRITABLE);
249 let (cr3, mem) = ptb.build();
250 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
251 let reader = ObjectReader::new(vas, Box::new(resolver));
252
253 let result = walk_iomem_regions(&reader).unwrap();
255 assert!(result.is_empty(), "missing symbol should yield empty vec");
256 }
257
258 #[test]
263 fn classify_empty_name_exactly_1mib_not_suspicious() {
264 let size: u64 = 1024 * 1024;
266 assert!(!classify_iomem("", 0, size));
267 }
268
269 #[test]
270 fn classify_empty_name_1mib_plus_1_suspicious() {
271 let size: u64 = 1024 * 1024 + 1;
273 assert!(classify_iomem("", 0, size));
274 }
275
276 #[test]
277 fn classify_empty_name_small_region_explicit_benign() {
278 assert!(!classify_iomem("", 100, 100)); }
281
282 #[test]
283 fn classify_named_region_small_benign() {
284 assert!(!classify_iomem("Reserved", 0, 0x0100_0000)); }
287
288 #[test]
289 fn classify_kernel_text_overlap_exact_boundary_suspicious() {
290 const KERNEL_TEXT_START: u64 = 0xffff_ffff_8100_0000;
292 const KERNEL_TEXT_END: u64 = 0xffff_ffff_8200_0000;
293 assert!(classify_iomem("Other", KERNEL_TEXT_START, KERNEL_TEXT_END));
295 }
296
297 #[test]
298 fn classify_region_just_before_kernel_text_benign() {
299 const KERNEL_TEXT_START: u64 = 0xffff_ffff_8100_0000;
301 assert!(!classify_iomem(
303 "Anything",
304 0xffff_ffff_8000_0000,
305 KERNEL_TEXT_START
306 ));
307 }
308
309 #[test]
310 fn classify_region_just_after_kernel_text_benign() {
311 const KERNEL_TEXT_END: u64 = 0xffff_ffff_8200_0000;
313 assert!(!classify_iomem(
314 "Anything",
315 KERNEL_TEXT_END,
316 KERNEL_TEXT_END + 0x1000
317 ));
318 }
319
320 #[test]
321 fn classify_kernel_code_partial_overlap_benign() {
322 const KERNEL_TEXT_START: u64 = 0xffff_ffff_8100_0000;
324 assert!(!classify_iomem(
325 "Kernel code",
326 KERNEL_TEXT_START,
327 KERNEL_TEXT_START + 0x1000
328 ));
329 }
330
331 #[test]
332 fn classify_tab_char_in_name_suspicious() {
333 assert!(classify_iomem("System\tRAM", 0, 0x1000));
335 }
336
337 #[test]
338 fn classify_newline_char_in_name_suspicious() {
339 assert!(classify_iomem("Sys\nRAM", 0, 0x1000));
341 }
342
343 #[test]
344 fn classify_saturating_sub_overflow_protection() {
345 assert!(!classify_iomem("", 0x1000, 0x0)); }
348
349 #[test]
354 fn walk_iomem_symbol_present_no_children_returns_empty() {
355 use memf_core::test_builders::{flags as ptf, PageTableBuilder};
356 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
357 use memf_symbols::isf::IsfResolver;
358 use memf_symbols::test_builders::IsfBuilder;
359
360 let root_vaddr: u64 = 0xFFFF_8800_00A0_0000;
364 let root_paddr: u64 = 0x00A0_0000; let isf = IsfBuilder::new()
367 .add_symbol("iomem_resource", root_vaddr)
368 .add_struct("resource", 0x60)
369 .add_field("resource", "start", 0x00, "unsigned long")
370 .add_field("resource", "end", 0x08, "unsigned long")
371 .add_field("resource", "flags", 0x10, "unsigned long")
372 .add_field("resource", "name", 0x18, "pointer")
373 .add_field("resource", "sibling", 0x20, "pointer")
374 .add_field("resource", "child", 0x28, "pointer")
375 .build_json();
376 let resolver = IsfResolver::from_value(&isf).unwrap();
377
378 let page = [0u8; 4096];
380
381 let (cr3, mem) = PageTableBuilder::new()
382 .map_4k(root_vaddr, root_paddr, ptf::WRITABLE)
383 .write_phys(root_paddr, &page)
384 .build();
385
386 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
387 let reader = ObjectReader::new(vas, Box::new(resolver));
388
389 let result = walk_iomem_regions(&reader).unwrap();
390 assert!(
391 result.is_empty(),
392 "iomem_resource with zero child pointer → no regions"
393 );
394 }
395
396 #[test]
401 fn walk_iomem_symbol_present_with_one_child_returns_entry() {
402 use memf_core::test_builders::{flags as ptf, PageTableBuilder};
403 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
404 use memf_symbols::isf::IsfResolver;
405 use memf_symbols::test_builders::IsfBuilder;
406
407 let root_vaddr: u64 = 0xFFFF_8800_00B0_0000;
412 let root_paddr: u64 = 0x00B0_0000;
413 let child_vaddr: u64 = 0xFFFF_8800_00B1_0000;
414 let child_paddr: u64 = 0x00B1_0000;
415
416 let mut root_page = [0u8; 4096];
418 root_page[0x28..0x30].copy_from_slice(&child_vaddr.to_le_bytes());
419
420 let mut child_page = [0u8; 4096];
422 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()
430 .add_symbol("iomem_resource", root_vaddr)
431 .add_struct("resource", 0x60)
432 .add_field("resource", "start", 0x00u64, "unsigned long")
433 .add_field("resource", "end", 0x08u64, "unsigned long")
434 .add_field("resource", "flags", 0x10u64, "unsigned long")
435 .add_field("resource", "name", 0x18u64, "pointer")
436 .add_field("resource", "sibling", 0x20u64, "pointer")
437 .add_field("resource", "child", 0x28u64, "pointer")
438 .build_json();
439 let resolver = IsfResolver::from_value(&isf).unwrap();
440
441 let (cr3, mem) = PageTableBuilder::new()
442 .map_4k(root_vaddr, root_paddr, ptf::WRITABLE)
443 .write_phys(root_paddr, &root_page)
444 .map_4k(child_vaddr, child_paddr, ptf::WRITABLE)
445 .write_phys(child_paddr, &child_page)
446 .build();
447 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
448 let reader = ObjectReader::new(vas, Box::new(resolver));
449
450 let result = walk_iomem_regions(&reader).unwrap_or_default();
451 assert_eq!(result.len(), 1, "should find exactly one resource entry");
452 assert_eq!(result[0].start, 0x1000);
453 assert_eq!(result[0].end, 0x2000);
454 assert_eq!(result[0].flags, 0x200);
455 assert_eq!(result[0].depth, 0);
456 assert!(!result[0].is_suspicious);
458 }
459
460 #[test]
465 fn walk_iomem_symbol_present_child_with_sibling() {
466 use memf_core::test_builders::{flags as ptf, PageTableBuilder};
467 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
468 use memf_symbols::isf::IsfResolver;
469 use memf_symbols::test_builders::IsfBuilder;
470
471 let root_vaddr: u64 = 0xFFFF_8800_00C0_0000;
473 let root_paddr: u64 = 0x00C0_0000;
474 let child_a_vaddr: u64 = 0xFFFF_8800_00C1_0000;
475 let child_a_paddr: u64 = 0x00C1_0000;
476 let child_b_vaddr: u64 = 0xFFFF_8800_00C2_0000;
477 let child_b_paddr: u64 = 0x00C2_0000;
478
479 let mut root_page = [0u8; 4096];
481 root_page[0x28..0x30].copy_from_slice(&child_a_vaddr.to_le_bytes());
482
483 let mut a_page = [0u8; 4096];
485 a_page[0x00..0x08].copy_from_slice(&0x0001_0000u64.to_le_bytes());
486 a_page[0x08..0x10].copy_from_slice(&0x0002_0000u64.to_le_bytes());
487 a_page[0x20..0x28].copy_from_slice(&child_b_vaddr.to_le_bytes()); let mut b_page = [0u8; 4096];
491 b_page[0x00..0x08].copy_from_slice(&0x0003_0000u64.to_le_bytes());
492 b_page[0x08..0x10].copy_from_slice(&0x0004_0000u64.to_le_bytes());
493
494 let isf = IsfBuilder::new()
495 .add_symbol("iomem_resource", root_vaddr)
496 .add_struct("resource", 0x60)
497 .add_field("resource", "start", 0x00u64, "unsigned long")
498 .add_field("resource", "end", 0x08u64, "unsigned long")
499 .add_field("resource", "flags", 0x10u64, "unsigned long")
500 .add_field("resource", "name", 0x18u64, "pointer")
501 .add_field("resource", "sibling", 0x20u64, "pointer")
502 .add_field("resource", "child", 0x28u64, "pointer")
503 .build_json();
504 let resolver = IsfResolver::from_value(&isf).unwrap();
505
506 let (cr3, mem) = PageTableBuilder::new()
507 .map_4k(root_vaddr, root_paddr, ptf::WRITABLE)
508 .write_phys(root_paddr, &root_page)
509 .map_4k(child_a_vaddr, child_a_paddr, ptf::WRITABLE)
510 .write_phys(child_a_paddr, &a_page)
511 .map_4k(child_b_vaddr, child_b_paddr, ptf::WRITABLE)
512 .write_phys(child_b_paddr, &b_page)
513 .build();
514 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
515 let reader = ObjectReader::new(vas, Box::new(resolver));
516
517 let result = walk_iomem_regions(&reader).unwrap_or_default();
518 assert_eq!(result.len(), 2, "should find both sibling resource entries");
519 }
520
521 #[test]
526 fn walk_iomem_symbol_present_nested_child() {
527 use memf_core::test_builders::{flags as ptf, PageTableBuilder};
528 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
529 use memf_symbols::isf::IsfResolver;
530 use memf_symbols::test_builders::IsfBuilder;
531
532 let root_vaddr: u64 = 0xFFFF_8800_00D0_0000;
533 let root_paddr: u64 = 0x00D0_0000;
534 let child_vaddr: u64 = 0xFFFF_8800_00D1_0000;
535 let child_paddr: u64 = 0x00D1_0000;
536 let grandchild_vaddr: u64 = 0xFFFF_8800_00D2_0000;
537 let grandchild_paddr: u64 = 0x00D2_0000;
538
539 let mut root_page = [0u8; 4096];
540 root_page[0x28..0x30].copy_from_slice(&child_vaddr.to_le_bytes());
541
542 let mut child_page = [0u8; 4096];
544 child_page[0x00..0x08].copy_from_slice(&0x1000u64.to_le_bytes());
545 child_page[0x08..0x10].copy_from_slice(&0x2000u64.to_le_bytes());
546 child_page[0x28..0x30].copy_from_slice(&grandchild_vaddr.to_le_bytes()); let mut gc_page = [0u8; 4096];
550 gc_page[0x00..0x08].copy_from_slice(&0x5000u64.to_le_bytes());
551 gc_page[0x08..0x10].copy_from_slice(&0x6000u64.to_le_bytes());
552
553 let isf = IsfBuilder::new()
554 .add_symbol("iomem_resource", root_vaddr)
555 .add_struct("resource", 0x60)
556 .add_field("resource", "start", 0x00u64, "unsigned long")
557 .add_field("resource", "end", 0x08u64, "unsigned long")
558 .add_field("resource", "flags", 0x10u64, "unsigned long")
559 .add_field("resource", "name", 0x18u64, "pointer")
560 .add_field("resource", "sibling", 0x20u64, "pointer")
561 .add_field("resource", "child", 0x28u64, "pointer")
562 .build_json();
563 let resolver = IsfResolver::from_value(&isf).unwrap();
564
565 let (cr3, mem) = PageTableBuilder::new()
566 .map_4k(root_vaddr, root_paddr, ptf::WRITABLE)
567 .write_phys(root_paddr, &root_page)
568 .map_4k(child_vaddr, child_paddr, ptf::WRITABLE)
569 .write_phys(child_paddr, &child_page)
570 .map_4k(grandchild_vaddr, grandchild_paddr, ptf::WRITABLE)
571 .write_phys(grandchild_paddr, &gc_page)
572 .build();
573 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
574 let reader = ObjectReader::new(vas, Box::new(resolver));
575
576 let result = walk_iomem_regions(&reader).unwrap_or_default();
577 assert_eq!(result.len(), 2, "child + grandchild = 2 entries");
578 let gc = result
580 .iter()
581 .find(|r| r.start == 0x5000)
582 .expect("grandchild entry");
583 assert_eq!(gc.depth, 1);
584 }
585
586 #[test]
591 fn io_mem_region_clone_debug_serialize() {
592 let region = IoMemRegion {
593 start: 0x1000,
594 end: 0x2000,
595 name: "System RAM".to_string(),
596 flags: 0x200,
597 depth: 0,
598 is_suspicious: false,
599 };
600 let cloned = region.clone();
601 assert_eq!(cloned.start, 0x1000);
602 assert_eq!(cloned.depth, 0);
603 let dbg = format!("{cloned:?}");
604 assert!(dbg.contains("System RAM"));
605 let json = serde_json::to_string(&cloned).unwrap();
606 assert!(json.contains("\"start\":4096"));
607 assert!(json.contains("\"is_suspicious\":false"));
608 }
609}