Skip to main content

memf_linux/
iomem.rs

1//! I/O memory resource region enumeration.
2//!
3//! Enumerates I/O memory resource regions from the `iomem_resource` kernel
4//! structure. Shows system memory layout, ACPI regions, PCI MMIO, firmware
5//! areas. Useful for understanding hardware layout and detecting suspicious
6//! memory-mapped regions. Equivalent to `/proc/iomem` from memory.
7//!
8//! The kernel maintains a tree of `struct resource` rooted at `iomem_resource`.
9//! Each resource has `start`, `end`, `name`, `flags`, and pointers to `child`
10//! and `sibling` forming a tree of nested memory regions.
11
12use memf_core::object_reader::ObjectReader;
13use memf_format::PhysicalMemoryProvider;
14
15use crate::Result;
16
17/// Maximum number of resource entries to walk (runaway protection).
18const MAX_RESOURCES: usize = 10_000;
19
20/// Information about a single I/O memory resource region.
21#[derive(Debug, Clone, serde::Serialize)]
22pub struct IoMemRegion {
23    /// Start physical address of the region.
24    pub start: u64,
25    /// End physical address of the region (inclusive).
26    pub end: u64,
27    /// Human-readable name of the region (e.g., "System RAM", "ACPI Tables").
28    pub name: String,
29    /// Resource flags from the kernel (`IORESOURCE_*` bitmask).
30    pub flags: u64,
31    /// Depth in the resource tree (0 = top-level).
32    pub depth: u32,
33    /// Whether this region is classified as suspicious.
34    pub is_suspicious: bool,
35}
36
37/// Classify whether an I/O memory region is suspicious.
38///
39/// A region is suspicious if:
40/// - The name is empty and the region spans more than 1 MiB (large unnamed regions).
41/// - The name contains unusual characters (control chars, non-ASCII).
42/// - The region overlaps kernel text (`0xffffffff81000000..0xffffffff82000000`)
43///   but is not named "Kernel code".
44pub use crate::heuristics::classify_iomem;
45
46/// Walk I/O memory resource regions from the `iomem_resource` kernel structure.
47///
48/// Returns `Ok(Vec::new())` if the `iomem_resource` symbol is not found
49/// (graceful degradation).
50pub fn walk_iomem_regions<P: PhysicalMemoryProvider>(
51    reader: &ObjectReader<P>,
52) -> Result<Vec<IoMemRegion>> {
53    // Resolve iomem_resource symbol (a struct resource, not a pointer).
54    let root_addr = match reader.symbols().symbol_address("iomem_resource") {
55        Some(addr) => addr,
56        None => return Ok(Vec::new()),
57    };
58
59    // Resolve field offsets within `struct resource`.
60    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    // Iterative DFS stack: (resource_addr, depth).
88    // Start with the children of the root (skip the root itself).
89    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        // Read name: pointer to a C string.
110        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        // Push sibling (to be visited after returning from children).
135        let sibling = read_ptr(reader, addr + sibling_offset);
136        if sibling != 0 {
137            stack.push((sibling, depth));
138        }
139
140        // Push child (deeper level).
141        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
150/// Read a 64-bit little-endian value from memory; returns 0 on failure.
151fn 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
158/// Read a pointer (64-bit) from memory; returns 0 on failure.
159fn 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    // ---------------------------------------------------------------
168    // Classifier unit tests
169    // ---------------------------------------------------------------
170
171    #[test]
172    fn classify_normal_system_ram_benign() {
173        // Standard "System RAM" region is not suspicious.
174        assert!(!classify_iomem("System RAM", 0x0010_0000, 0x7FFF_FFFF));
175    }
176
177    #[test]
178    fn classify_empty_name_large_region_suspicious() {
179        // Empty name on a region larger than 1 MiB is suspicious.
180        assert!(classify_iomem("", 0x0, 0x0020_0000)); // 2 MiB
181    }
182
183    #[test]
184    fn classify_empty_name_small_region_benign() {
185        // Empty name on a small region (< 1 MiB) is not suspicious.
186        assert!(!classify_iomem("", 0x0, 0x100)); // 256 bytes
187    }
188
189    #[test]
190    fn classify_control_chars_in_name_suspicious() {
191        // Name containing control characters is suspicious (corrupted name pointer).
192        assert!(classify_iomem("System\x00RAM", 0x0, 0x1000));
193    }
194
195    #[test]
196    fn classify_non_ascii_name_suspicious() {
197        // Name containing non-ASCII bytes is suspicious.
198        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        // Region overlapping kernel text but not named "Kernel code" is suspicious.
204        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        // The legitimate "Kernel code" region overlapping kernel text is fine.
214        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        // Standard ACPI region is not suspicious.
224        assert!(!classify_iomem("ACPI Tables", 0xBFFE_0000, 0xBFFF_FFFF));
225    }
226
227    #[test]
228    fn classify_pci_mmio_benign() {
229        // Standard PCI MMIO region is not suspicious.
230        assert!(!classify_iomem("PCI Bus 0000:00", 0xE000_0000, 0xEFFF_FFFF));
231    }
232
233    // ---------------------------------------------------------------
234    // Walker test — missing symbol graceful degradation
235    // ---------------------------------------------------------------
236
237    #[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        // With no iomem_resource symbol the walker must return empty, not panic.
254        let result = walk_iomem_regions(&reader).unwrap();
255        assert!(result.is_empty(), "missing symbol should yield empty vec");
256    }
257
258    // ---------------------------------------------------------------
259    // classify_iomem: additional boundary and branch tests
260    // ---------------------------------------------------------------
261
262    #[test]
263    fn classify_empty_name_exactly_1mib_not_suspicious() {
264        // Empty name, region size exactly 1 MiB → NOT suspicious (> not >=)
265        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        // Empty name, region size 1 MiB + 1 byte → suspicious
272        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        // Empty name on 0-byte region → not suspicious
279        assert!(!classify_iomem("", 100, 100)); // end == start → size = 0
280    }
281
282    #[test]
283    fn classify_named_region_small_benign() {
284        // Named region of any size is not suspicious on name-check alone
285        assert!(!classify_iomem("Reserved", 0, 0x0100_0000)); // 16 MiB but named
286    }
287
288    #[test]
289    fn classify_kernel_text_overlap_exact_boundary_suspicious() {
290        // Region starts exactly at KERNEL_TEXT_START, not named "Kernel code"
291        const KERNEL_TEXT_START: u64 = 0xffff_ffff_8100_0000;
292        const KERNEL_TEXT_END: u64 = 0xffff_ffff_8200_0000;
293        // start < KERNEL_TEXT_END AND end > KERNEL_TEXT_START
294        assert!(classify_iomem("Other", KERNEL_TEXT_START, KERNEL_TEXT_END));
295    }
296
297    #[test]
298    fn classify_region_just_before_kernel_text_benign() {
299        // Region ends at exactly KERNEL_TEXT_START → no overlap (end > start check: end == start fails >)
300        const KERNEL_TEXT_START: u64 = 0xffff_ffff_8100_0000;
301        // end == KERNEL_TEXT_START means end is NOT > KERNEL_TEXT_START
302        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        // Region starts at exactly KERNEL_TEXT_END → start < KERNEL_TEXT_END fails (== not <)
312        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        // Legitimately named "Kernel code" overlapping kernel text range is benign
323        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        // Tab is a control character → suspicious
334        assert!(classify_iomem("System\tRAM", 0, 0x1000));
335    }
336
337    #[test]
338    fn classify_newline_char_in_name_suspicious() {
339        // Newline is a control character → suspicious
340        assert!(classify_iomem("Sys\nRAM", 0, 0x1000));
341    }
342
343    #[test]
344    fn classify_saturating_sub_overflow_protection() {
345        // end < start → saturating_sub yields 0 → not suspicious on size alone
346        assert!(!classify_iomem("", 0x1000, 0x0)); // end < start → size = 0
347    }
348
349    // ---------------------------------------------------------------
350    // walk_iomem_regions: symbol present, child == 0 → exercises body, returns empty
351    // ---------------------------------------------------------------
352
353    #[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        // iomem_resource is the root struct resource (not a pointer to it).
361        // The walk reads root_addr + child_offset; child_offset defaults to 0x28.
362        // If that value is 0, the walk returns Ok(Vec::new()) immediately.
363        let root_vaddr: u64 = 0xFFFF_8800_00A0_0000;
364        let root_paddr: u64 = 0x00A0_0000; // unique, < 16 MB
365
366        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        // All zeros: child pointer at 0x28 == 0 → walk returns empty immediately.
379        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    // ---------------------------------------------------------------
397    // walk_iomem_regions: symbol present, child != 0 → exercises DFS loop
398    // ---------------------------------------------------------------
399
400    #[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        // Layout:
408        //   root_vaddr  = iomem_resource (struct resource)
409        //     child ptr (at +0x28) → child_vaddr
410        //   child_vaddr = one resource entry; sibling=0, child=0, name_ptr=0
411        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        // Root page: zeros except child pointer at offset 0x28
417        let mut root_page = [0u8; 4096];
418        root_page[0x28..0x30].copy_from_slice(&child_vaddr.to_le_bytes());
419
420        // Child page: start=0x1000, end=0x2000, flags=0x200, name_ptr=0, sibling=0, child=0
421        let mut child_page = [0u8; 4096];
422        child_page[0x00..0x08].copy_from_slice(&0x1000u64.to_le_bytes()); // start
423        child_page[0x08..0x10].copy_from_slice(&0x2000u64.to_le_bytes()); // end
424        child_page[0x10..0x18].copy_from_slice(&0x0200u64.to_le_bytes()); // flags
425                                                                          // name_ptr at 0x18 = 0 (null → name = "")
426                                                                          // sibling at 0x20 = 0
427                                                                          // child at 0x28 = 0
428
429        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        // name_ptr=0 → empty name; size=0x1000 < 1MiB → not suspicious
457        assert!(!result[0].is_suspicious);
458    }
459
460    // ---------------------------------------------------------------
461    // walk_iomem_regions: child has a sibling → exercises sibling push
462    // ---------------------------------------------------------------
463
464    #[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        // root → child_a (sibling → child_b)
472        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        // root: child ptr at 0x28 = child_a_vaddr
480        let mut root_page = [0u8; 4096];
481        root_page[0x28..0x30].copy_from_slice(&child_a_vaddr.to_le_bytes());
482
483        // child_a: start=0x10000, end=0x20000, sibling=child_b, child=0
484        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()); // sibling
488
489        // child_b: start=0x30000, end=0x40000, sibling=0, child=0
490        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    // ---------------------------------------------------------------
522    // walk_iomem_regions: child has a sub-child → exercises child push (depth+1)
523    // ---------------------------------------------------------------
524
525    #[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        // child: has a child → grandchild
543        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()); // child ptr
547
548        // grandchild: no further children
549        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        // grandchild should be at depth 1
579        let gc = result
580            .iter()
581            .find(|r| r.start == 0x5000)
582            .expect("grandchild entry");
583        assert_eq!(gc.depth, 1);
584    }
585
586    // ---------------------------------------------------------------
587    // IoMemRegion: Clone + Debug + Serialize
588    // ---------------------------------------------------------------
589
590    #[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}