Skip to main content

memf_core/
object_reader.rs

1//! High-level kernel object reading using symbol resolution.
2
3use bytemuck::Pod;
4use memf_format::PhysicalMemoryProvider;
5use memf_symbols::SymbolResolver;
6
7use crate::vas::VirtualAddressSpace;
8use crate::{Error, Result};
9
10/// Maximum number of iterations when walking a linked list (cycle protection).
11const MAX_LIST_ITERATIONS: usize = 100_000;
12
13/// Reads kernel objects from a physical memory dump using symbol information.
14///
15/// Combines a [`VirtualAddressSpace`] with a [`SymbolResolver`] to provide
16/// high-level access to kernel data structures like task_struct, modules, etc.
17pub struct ObjectReader<P: PhysicalMemoryProvider> {
18    vas: VirtualAddressSpace<P>,
19    symbols: Box<dyn SymbolResolver>,
20    /// Kernel image base virtual address (KVO). Windows ISF symbols are RVAs
21    /// relative to this; it is added in [`Self::required_symbol`] so no caller
22    /// can obtain an un-rebased address. Zero for resolvers whose symbols are
23    /// already absolute VAs (e.g. Linux kallsyms), leaving them unchanged.
24    kernel_base: u64,
25}
26
27impl<P: PhysicalMemoryProvider> ObjectReader<P> {
28    /// Create a new object reader (kernel base unset → symbols used verbatim).
29    pub fn new(vas: VirtualAddressSpace<P>, symbols: Box<dyn SymbolResolver>) -> Self {
30        Self {
31            vas,
32            symbols,
33            kernel_base: 0,
34        }
35    }
36
37    /// Set the kernel image base VA (KVO) so RVA-based symbols are rebased to
38    /// real virtual addresses. Builder form; the default (0) is a no-op.
39    #[must_use]
40    pub fn with_kernel_base(mut self, kernel_base: u64) -> Self {
41        self.kernel_base = kernel_base;
42        self
43    }
44
45    /// Access the underlying symbol resolver.
46    pub fn symbols(&self) -> &dyn SymbolResolver {
47        self.symbols.as_ref()
48    }
49
50    /// Access the underlying virtual address space.
51    pub fn vas(&self) -> &VirtualAddressSpace<P> {
52        &self.vas
53    }
54
55    /// Create a new reader sharing the same physical memory and symbols but
56    /// using a different page table root (CR3). Useful for switching to a
57    /// process's user-mode address space.
58    pub fn with_cr3(&self, cr3: u64) -> Self
59    where
60        P: Clone,
61    {
62        let vas = VirtualAddressSpace::new(self.vas.physical().clone(), cr3, self.vas.mode());
63        Self {
64            vas,
65            symbols: self.symbols.clone_boxed(),
66            kernel_base: self.kernel_base,
67        }
68    }
69
70    /// Consume this reader and return one over the **same** address space (the
71    /// physical memory is moved, not cloned — so this works even when the
72    /// provider is not `Clone`) but with its symbol resolver transformed by `f`.
73    ///
74    /// Used to wrap the kernel-only resolver in a multi-module resolver carrying
75    /// e.g. `tcpip.sys` symbols before a netstat walk.
76    #[must_use]
77    pub fn map_symbols(
78        self,
79        f: impl FnOnce(Box<dyn SymbolResolver>) -> Box<dyn SymbolResolver>,
80    ) -> Self {
81        let Self {
82            vas,
83            symbols,
84            kernel_base,
85        } = self;
86        Self {
87            vas,
88            symbols: f(symbols),
89            kernel_base,
90        }
91    }
92
93    /// Read a field from a struct at `base_vaddr` and interpret it as type `T`.
94    ///
95    /// Looks up the field offset from the symbol resolver, reads `size_of::<T>()`
96    /// bytes from virtual memory, and casts via `bytemuck::from_bytes`.
97    pub fn read_field<T: Pod + Default>(
98        &self,
99        base_vaddr: u64,
100        struct_name: &str,
101        field_name: &str,
102    ) -> Result<T> {
103        let offset = self
104            .symbols
105            .field_offset(struct_name, field_name)
106            .ok_or_else(|| Error::MissingSymbol(format!("{struct_name}.{field_name}")))?;
107
108        let size = std::mem::size_of::<T>();
109        let mut buf = vec![0u8; size];
110        self.vas
111            .read_virt(base_vaddr.wrapping_add(offset), &mut buf)?;
112
113        if buf.len() != size {
114            return Err(Error::SizeMismatch {
115                expected: size,
116                got: buf.len(),
117            });
118        }
119
120        Ok(*bytemuck::from_bytes::<T>(&buf))
121    }
122
123    /// Read a pointer (u64) from a struct field.
124    pub fn read_pointer(
125        &self,
126        base_vaddr: u64,
127        struct_name: &str,
128        field_name: &str,
129    ) -> Result<u64> {
130        self.read_field::<u64>(base_vaddr, struct_name, field_name)
131    }
132
133    /// Read a null-terminated string from virtual memory, up to `max_len` bytes.
134    pub fn read_string(&self, vaddr: u64, max_len: usize) -> Result<String> {
135        let mut buf = vec![0u8; max_len];
136        self.vas.read_virt(vaddr, &mut buf)?;
137
138        let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
139        Ok(String::from_utf8_lossy(&buf[..end]).into_owned())
140    }
141
142    /// Read a string from a struct field (the field contains inline char data, not a pointer).
143    pub fn read_field_string(
144        &self,
145        base_vaddr: u64,
146        struct_name: &str,
147        field_name: &str,
148        max_len: usize,
149    ) -> Result<String> {
150        let offset = self
151            .symbols
152            .field_offset(struct_name, field_name)
153            .ok_or_else(|| Error::MissingSymbol(format!("{struct_name}.{field_name}")))?;
154
155        self.read_string(base_vaddr.wrapping_add(offset), max_len)
156    }
157
158    /// Walk a Linux `list_head` doubly-linked list.
159    ///
160    /// Starting from `head_vaddr` (the address of the list_head embedded in the
161    /// head/sentinel node), follows `next` pointers and returns the virtual address
162    /// of each containing struct (using container_of logic with `list_field` offset).
163    ///
164    /// Stops when the walk loops back to `head_vaddr` or hits `MAX_LIST_ITERATIONS`.
165    pub fn walk_list(
166        &self,
167        head_vaddr: u64,
168        struct_name: &str,
169        list_field: &str,
170    ) -> Result<Vec<u64>> {
171        self.walk_list_with(head_vaddr, "list_head", "next", struct_name, list_field)
172    }
173
174    /// Walk a doubly-linked list with configurable list struct and field names.
175    ///
176    /// This is a generalized version of [`walk_list`](Self::walk_list) that works
177    /// with any linked-list structure, not just Linux `list_head`.
178    ///
179    /// For example, Windows uses `_LIST_ENTRY` with `Flink`/`Blink` fields
180    /// instead of `list_head` with `next`/`prev`.
181    ///
182    /// # Arguments
183    /// * `head_vaddr` — virtual address of the list head (sentinel node)
184    /// * `list_struct` — name of the list-link struct (e.g., `"list_head"`, `"_LIST_ENTRY"`)
185    /// * `next_field` — name of the forward pointer field (e.g., `"next"`, `"Flink"`)
186    /// * `container_struct` — name of the containing struct (e.g., `"_EPROCESS"`)
187    /// * `list_field` — name of the list-link field in the container struct (e.g., `"ActiveProcessLinks"`)
188    pub fn walk_list_with(
189        &self,
190        head_vaddr: u64,
191        list_struct: &str,
192        next_field: &str,
193        container_struct: &str,
194        list_field: &str,
195    ) -> Result<Vec<u64>> {
196        let list_offset = self
197            .symbols
198            .field_offset(container_struct, list_field)
199            .ok_or_else(|| Error::MissingSymbol(format!("{container_struct}.{list_field}")))?;
200
201        let next_offset = self
202            .symbols
203            .field_offset(list_struct, next_field)
204            .ok_or_else(|| Error::MissingSymbol(format!("{list_struct}.{next_field}")))?;
205
206        // Read the first forward pointer from head
207        let mut current = self.read_u64_at(head_vaddr.wrapping_add(next_offset))?;
208
209        let mut result = Vec::new();
210
211        for _ in 0..MAX_LIST_ITERATIONS {
212            // If we've looped back to head, the walk is complete
213            if current == head_vaddr {
214                return Ok(result);
215            }
216
217            // Smear tolerance: a live-acquired dump can contain a torn-down node
218            // whose link reads 0 (a null terminus). Stop rather than dereference
219            // null or fabricate a container from `0 - offset`.
220            if current == 0 {
221                return Ok(result);
222            }
223
224            // Peek-before-record: read this node's forward pointer FIRST. If its
225            // LIST_ENTRY page is not mapped, `current` is not a real node — a
226            // torn-down node's link can hold garbage (e.g. the user-half value
227            // 0x5a289000 seen on DESKTOP-SDN1RPT.mem, which is canonical but
228            // unmapped). Terminate without fabricating a container that a later
229            // field read would fault on. This works for BOTH kernel object lists
230            // and user-space lists (PEB/LDR modules), so it must NOT assume a
231            // kernel-half address.
232            let Ok(next) = self.read_u64_at(current.wrapping_add(next_offset)) else {
233                return Ok(result);
234            };
235
236            // container_of: subtract list_offset to get the containing struct base
237            result.push(current.wrapping_sub(list_offset));
238            current = next;
239        }
240
241        Err(Error::ListCycle(MAX_LIST_ITERATIONS))
242    }
243
244    /// Walk a doubly-linked list in BOTH directions and return the union of
245    /// containers, deduplicated (forward order first, then backward-only nodes).
246    ///
247    /// On a live-acquired dump a single torn-down node can break the forward
248    /// (`next_field`/Flink) chain, orphaning every node beyond it from a
249    /// forward-only walk — yet those nodes remain reachable from the head via the
250    /// backward (`prev_field`/Blink) chain. Walking both directions recovers them
251    /// without resorting to pool-tag scanning. (A node unlinked from *both*
252    /// directions — full DKOM hiding — still requires a pool scan.)
253    pub fn walk_list_bidirectional(
254        &self,
255        head_vaddr: u64,
256        list_struct: &str,
257        next_field: &str,
258        prev_field: &str,
259        container_struct: &str,
260        list_field: &str,
261    ) -> Result<Vec<u64>> {
262        let mut forward = self.walk_list_with(
263            head_vaddr,
264            list_struct,
265            next_field,
266            container_struct,
267            list_field,
268        )?;
269        let backward = self.walk_list_with(
270            head_vaddr,
271            list_struct,
272            prev_field,
273            container_struct,
274            list_field,
275        )?;
276        let mut seen: std::collections::HashSet<u64> = forward.iter().copied().collect();
277        for container in backward {
278            if seen.insert(container) {
279                forward.push(container);
280            }
281        }
282        Ok(forward)
283    }
284
285    /// Read `len` raw bytes from virtual memory at `vaddr`.
286    pub fn read_bytes(&self, vaddr: u64, len: usize) -> Result<Vec<u8>> {
287        let mut buf = vec![0u8; len];
288        self.vas.read_virt(vaddr, &mut buf)?;
289        Ok(buf)
290    }
291
292    /// Resolve a global kernel symbol to a real virtual address, returning an
293    /// error if absent. The resolver yields an RVA (Windows ISF); the kernel
294    /// base (KVO) is added here so callers cannot obtain an un-rebased address.
295    pub fn required_symbol(&self, name: &str) -> Result<u64> {
296        self.symbols()
297            .symbol_address(name)
298            .map(|rva| self.kernel_base.wrapping_add(rva))
299            .ok_or_else(|| Error::MissingSymbol(name.to_owned()))
300    }
301
302    /// Resolve a struct field offset, returning an error if absent.
303    pub fn required_field_offset(&self, struct_name: &str, field_name: &str) -> Result<usize> {
304        self.symbols()
305            .field_offset(struct_name, field_name)
306            .map(|v| v as usize)
307            .ok_or_else(|| Error::MissingSymbol(format!("{struct_name}.{field_name}")))
308    }
309
310    fn read_u64_at(&self, vaddr: u64) -> Result<u64> {
311        let mut buf = [0u8; 8];
312        self.vas.read_virt(vaddr, &mut buf)?;
313        Ok(u64::from_le_bytes(buf))
314    }
315
316    /// Returns a lazy iterator over a kernel linked list (Linux `list_head`).
317    ///
318    /// Yields the virtual address of each containing struct (container_of adjusted),
319    /// same as [`walk_list`](Self::walk_list). Unlike `walk_list`, this does not
320    /// allocate a `Vec` — entries are yielded one at a time. Use `.take(n)` for
321    /// early termination or filter with `.filter_map`.
322    ///
323    /// # Errors
324    ///
325    /// Each yielded item is `Result<u64>`. The iterator stops (returning `None`) on
326    /// cycle or when the list loops back to `head_vaddr`. If a pointer read fails,
327    /// the failing `Err` is yielded as the last item.
328    pub fn iter_list<'a>(
329        &'a self,
330        head_vaddr: u64,
331        container_struct: &'a str,
332        list_field: &'a str,
333    ) -> ListIter<'a, P> {
334        let list_offset = self
335            .symbols
336            .field_offset(container_struct, list_field)
337            .unwrap_or(0);
338        let next_offset = self.symbols.field_offset("list_head", "next").unwrap_or(0);
339
340        let current = match self.read_u64_at(head_vaddr.wrapping_add(next_offset)) {
341            Ok(v) => v,
342            Err(_) => head_vaddr, // will immediately return None (current == head)
343        };
344
345        ListIter {
346            reader: self,
347            head_vaddr,
348            current,
349            list_offset,
350            next_offset,
351            seen: std::collections::HashSet::new(),
352            done: false,
353        }
354    }
355}
356
357/// Streaming iterator over a kernel doubly-linked list.
358///
359/// Returned by [`ObjectReader::iter_list`]. Yields the virtual address of each
360/// container struct (using container_of logic, same as [`ObjectReader::walk_list`]).
361pub struct ListIter<'a, P: PhysicalMemoryProvider> {
362    reader: &'a ObjectReader<P>,
363    head_vaddr: u64,
364    current: u64,
365    list_offset: u64,
366    next_offset: u64,
367    seen: std::collections::HashSet<u64>,
368    done: bool,
369}
370
371impl<P: PhysicalMemoryProvider> Iterator for ListIter<'_, P> {
372    type Item = crate::Result<u64>;
373
374    fn next(&mut self) -> Option<Self::Item> {
375        if self.done {
376            return None;
377        }
378        // Termination: looped back to head
379        if self.current == self.head_vaddr {
380            return None;
381        }
382        // Cycle detection
383        if !self.seen.insert(self.current) {
384            self.done = true;
385            return None; // silently stop on detected cycle without valid entry
386        }
387        if self.seen.len() > MAX_LIST_ITERATIONS {
388            self.done = true;
389            return Some(Err(crate::Error::ListCycle(MAX_LIST_ITERATIONS)));
390        }
391
392        // container_of: subtract list_offset to get containing struct base
393        let container = self.current.wrapping_sub(self.list_offset);
394
395        // Advance: follow next pointer
396        match self
397            .reader
398            .read_u64_at(self.current.wrapping_add(self.next_offset))
399        {
400            Ok(next) => self.current = next,
401            Err(e) => {
402                self.done = true;
403                return Some(Err(e));
404            }
405        }
406
407        Some(Ok(container))
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414    use crate::test_builders::{flags, PageTableBuilder};
415    use crate::vas::TranslationMode;
416    use memf_symbols::isf::IsfResolver;
417    use memf_symbols::test_builders::IsfBuilder;
418
419    fn make_reader(
420        isf: &IsfBuilder,
421        builder: PageTableBuilder,
422    ) -> ObjectReader<crate::test_builders::SyntheticPhysMem> {
423        let json = isf.build_json();
424        let resolver = IsfResolver::from_value(&json).unwrap();
425        let (cr3, mem) = builder.build();
426        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
427        ObjectReader::new(vas, Box::new(resolver))
428    }
429
430    #[test]
431    fn read_field_u32() {
432        let isf = IsfBuilder::new().add_struct("task_struct", 128).add_field(
433            "task_struct",
434            "pid",
435            0,
436            "int",
437        );
438
439        let vaddr: u64 = 0xFFFF_8000_0010_0000;
440        let paddr: u64 = 0x0080_0000;
441
442        let ptb = PageTableBuilder::new()
443            .map_4k(vaddr, paddr, flags::WRITABLE)
444            .write_phys_u64(paddr, u64::from(42u32)); // pid = 42 at offset 0
445
446        let reader = make_reader(&isf, ptb);
447        let pid: u32 = reader.read_field(vaddr, "task_struct", "pid").unwrap();
448        assert_eq!(pid, 42);
449    }
450
451    #[test]
452    fn read_field_u64() {
453        let isf = IsfBuilder::new().add_struct("task_struct", 128).add_field(
454            "task_struct",
455            "mm",
456            8,
457            "pointer",
458        );
459
460        let vaddr: u64 = 0xFFFF_8000_0010_0000;
461        let paddr: u64 = 0x0080_0000;
462        let mm_value: u64 = 0xFFFF_8000_DEAD_BEEF;
463
464        let ptb = PageTableBuilder::new()
465            .map_4k(vaddr, paddr, flags::WRITABLE)
466            .write_phys_u64(paddr + 8, mm_value);
467
468        let reader = make_reader(&isf, ptb);
469        let mm: u64 = reader.read_field(vaddr, "task_struct", "mm").unwrap();
470        assert_eq!(mm, mm_value);
471    }
472
473    #[test]
474    fn read_field_missing_symbol() {
475        let isf = IsfBuilder::new().add_struct("task_struct", 128);
476
477        let vaddr: u64 = 0xFFFF_8000_0010_0000;
478        let paddr: u64 = 0x0080_0000;
479
480        let ptb = PageTableBuilder::new().map_4k(vaddr, paddr, flags::WRITABLE);
481
482        let reader = make_reader(&isf, ptb);
483        let result = reader.read_field::<u32>(vaddr, "task_struct", "nonexistent");
484        assert!(result.is_err());
485        match result.unwrap_err() {
486            Error::MissingSymbol(s) => assert_eq!(s, "task_struct.nonexistent"),
487            other => panic!("unexpected error: {other}"),
488        }
489    }
490
491    #[test]
492    fn map_symbols_applies_the_transform() {
493        // Reader's resolver knows `old_sym` but not `new_sym`.
494        let isf_old = IsfBuilder::new().add_symbol("old_sym", 0x1000);
495        let reader = make_reader(&isf_old, PageTableBuilder::new());
496        assert_eq!(reader.symbols().symbol_address("old_sym"), Some(0x1000));
497        assert_eq!(reader.symbols().symbol_address("new_sym"), None);
498
499        // `map_symbols` must hand the existing resolver to `f` and install the
500        // result — here a fresh resolver that knows `new_sym` instead.
501        let new_json = IsfBuilder::new().add_symbol("new_sym", 0x2000).build_json();
502        let reader =
503            reader.map_symbols(|_old| Box::new(IsfResolver::from_value(&new_json).unwrap()));
504
505        assert_eq!(
506            reader.symbols().symbol_address("new_sym"),
507            Some(0x2000),
508            "map_symbols must apply f to the resolver"
509        );
510        assert_eq!(
511            reader.symbols().symbol_address("old_sym"),
512            None,
513            "the old resolver must be replaced by f's result"
514        );
515    }
516
517    #[test]
518    fn read_field_string_test() {
519        let isf = IsfBuilder::new().add_struct("task_struct", 128).add_field(
520            "task_struct",
521            "comm",
522            16,
523            "char",
524        );
525
526        let vaddr: u64 = 0xFFFF_8000_0010_0000;
527        let paddr: u64 = 0x0080_0000;
528
529        let ptb = PageTableBuilder::new()
530            .map_4k(vaddr, paddr, flags::WRITABLE)
531            .write_phys(paddr + 16, b"systemd\0");
532
533        let reader = make_reader(&isf, ptb);
534        let comm = reader
535            .read_field_string(vaddr, "task_struct", "comm", 16)
536            .unwrap();
537        assert_eq!(comm, "systemd");
538    }
539
540    #[test]
541    fn read_string_with_null() {
542        let isf = IsfBuilder::new().add_struct("task_struct", 128).add_field(
543            "task_struct",
544            "comm",
545            16,
546            "char",
547        );
548
549        let vaddr: u64 = 0xFFFF_8000_0010_0000;
550        let paddr: u64 = 0x0080_0000;
551
552        let ptb = PageTableBuilder::new()
553            .map_4k(vaddr, paddr, flags::WRITABLE)
554            .write_phys(paddr + 16, b"init\0\0\0\0\0\0\0\0\0\0\0\0");
555
556        let reader = make_reader(&isf, ptb);
557        let s = reader.read_string(vaddr + 16, 16).unwrap();
558        assert_eq!(s, "init");
559    }
560
561    #[test]
562    fn walk_list_simple() {
563        // Create a simplified task_struct layout:
564        //   offset 0: pid (u32)
565        //   offset 8: tasks.next (u64)  -- list_head embedded at offset 8
566        //   offset 16: comm (16 bytes)
567        //   struct size: 128
568        let isf = IsfBuilder::new()
569            .add_struct("task_struct", 128)
570            .add_field("task_struct", "pid", 0, "int")
571            .add_field("task_struct", "tasks", 8, "list_head")
572            .add_field("task_struct", "comm", 16, "char")
573            .add_struct("list_head", 16)
574            .add_field("list_head", "next", 0, "pointer")
575            .add_field("list_head", "prev", 8, "pointer");
576
577        // Physical layout:
578        //   paddr 0x0080_0000: head task_struct (init_task)
579        //   paddr 0x0080_1000: task A
580        //   paddr 0x0080_2000: task B
581        //
582        // Circular list:
583        //   head.tasks.next -> A.tasks -> B.tasks -> head.tasks
584        let head_paddr: u64 = 0x0080_0000;
585        let a_paddr: u64 = 0x0080_1000;
586        let b_paddr: u64 = 0x0080_2000;
587
588        let head_vaddr: u64 = 0xFFFF_8000_0010_0000;
589        let a_vaddr: u64 = 0xFFFF_8000_0010_1000;
590        let b_vaddr: u64 = 0xFFFF_8000_0010_2000;
591
592        let list_offset: u64 = 8; // tasks field offset
593
594        // head.tasks.next = &A.tasks
595        // A.tasks.next = &B.tasks
596        // B.tasks.next = &head.tasks
597        let ptb = PageTableBuilder::new()
598            .map_4k(head_vaddr, head_paddr, flags::WRITABLE)
599            .map_4k(a_vaddr, a_paddr, flags::WRITABLE)
600            .map_4k(b_vaddr, b_paddr, flags::WRITABLE)
601            // head: pid=0, tasks.next = a_vaddr + list_offset
602            .write_phys_u64(head_paddr, 0) // pid
603            .write_phys_u64(head_paddr + list_offset, a_vaddr + list_offset) // tasks.next
604            // A: pid=100, tasks.next = b_vaddr + list_offset
605            .write_phys_u64(a_paddr, 100) // pid
606            .write_phys_u64(a_paddr + list_offset, b_vaddr + list_offset) // tasks.next
607            // B: pid=200, tasks.next = head_vaddr + list_offset (loops back)
608            .write_phys_u64(b_paddr, 200) // pid
609            .write_phys_u64(b_paddr + list_offset, head_vaddr + list_offset); // tasks.next
610
611        let reader = make_reader(&isf, ptb);
612
613        let containers = reader
614            .walk_list(head_vaddr + list_offset, "task_struct", "tasks")
615            .unwrap();
616        assert_eq!(containers.len(), 2);
617        assert_eq!(containers[0], a_vaddr);
618        assert_eq!(containers[1], b_vaddr);
619    }
620
621    #[test]
622    fn read_pointer_test() {
623        let isf = IsfBuilder::new().add_struct("task_struct", 128).add_field(
624            "task_struct",
625            "mm",
626            8,
627            "pointer",
628        );
629
630        let vaddr: u64 = 0xFFFF_8000_0010_0000;
631        let paddr: u64 = 0x0080_0000;
632        let mm_value: u64 = 0xFFFF_8000_CAFE_BABE;
633
634        let ptb = PageTableBuilder::new()
635            .map_4k(vaddr, paddr, flags::WRITABLE)
636            .write_phys_u64(paddr + 8, mm_value);
637
638        let reader = make_reader(&isf, ptb);
639        let ptr = reader.read_pointer(vaddr, "task_struct", "mm").unwrap();
640        assert_eq!(ptr, mm_value);
641    }
642
643    #[test]
644    fn read_field_invalid_struct_name() {
645        let isf = IsfBuilder::new().add_struct("task_struct", 128).add_field(
646            "task_struct",
647            "pid",
648            0,
649            "int",
650        );
651
652        let vaddr: u64 = 0xFFFF_8000_0010_0000;
653        let paddr: u64 = 0x0080_0000;
654
655        let ptb = PageTableBuilder::new().map_4k(vaddr, paddr, flags::WRITABLE);
656
657        let reader = make_reader(&isf, ptb);
658        let result = reader.read_field::<u32>(vaddr, "nonexistent_struct", "pid");
659        assert!(result.is_err());
660        match result.unwrap_err() {
661            Error::MissingSymbol(s) => assert_eq!(s, "nonexistent_struct.pid"),
662            other => panic!("unexpected error: {other}"),
663        }
664    }
665
666    #[test]
667    fn walk_list_empty_list() {
668        // A list where head.next points back to head (empty list)
669        let isf = IsfBuilder::new()
670            .add_struct("task_struct", 128)
671            .add_field("task_struct", "tasks", 8, "list_head")
672            .add_struct("list_head", 16)
673            .add_field("list_head", "next", 0, "pointer")
674            .add_field("list_head", "prev", 8, "pointer");
675
676        let head_paddr: u64 = 0x0080_0000;
677        let head_vaddr: u64 = 0xFFFF_8000_0010_0000;
678        let list_offset: u64 = 8;
679
680        // head.tasks.next = head.tasks (points back to itself -> empty list)
681        let ptb = PageTableBuilder::new()
682            .map_4k(head_vaddr, head_paddr, flags::WRITABLE)
683            .write_phys_u64(head_paddr + list_offset, head_vaddr + list_offset);
684
685        let reader = make_reader(&isf, ptb);
686        let containers = reader
687            .walk_list(head_vaddr + list_offset, "task_struct", "tasks")
688            .unwrap();
689        assert!(containers.is_empty());
690    }
691
692    #[test]
693    fn walk_list_with_windows_list_entry() {
694        // Test walk_list_with using Windows _LIST_ENTRY / Flink naming.
695        // Layout: _EPROCESS with ActiveProcessLinks at offset 0x10.
696        // _LIST_ENTRY with Flink at offset 0, Blink at offset 8.
697        let isf = IsfBuilder::new()
698            .add_struct("_EPROCESS", 256)
699            .add_field("_EPROCESS", "UniqueProcessId", 0, "pointer")
700            .add_field("_EPROCESS", "ActiveProcessLinks", 0x10, "_LIST_ENTRY")
701            .add_struct("_LIST_ENTRY", 16)
702            .add_field("_LIST_ENTRY", "Flink", 0, "pointer")
703            .add_field("_LIST_ENTRY", "Blink", 8, "pointer");
704
705        // Physical layout:
706        //   head (sentinel list head at some vaddr)
707        //   proc_a at paddr 0x0080_1000
708        //   proc_b at paddr 0x0080_2000
709        let head_paddr: u64 = 0x0080_0000;
710        let a_paddr: u64 = 0x0080_1000;
711        let b_paddr: u64 = 0x0080_2000;
712
713        let head_vaddr: u64 = 0xFFFF_8000_0010_0000; // sentinel list head
714        let a_vaddr: u64 = 0xFFFF_8000_0010_1000;
715        let b_vaddr: u64 = 0xFFFF_8000_0010_2000;
716
717        let list_offset: u64 = 0x10; // ActiveProcessLinks offset in _EPROCESS
718
719        // Circular: head.Flink -> A.ActiveProcessLinks -> B.ActiveProcessLinks -> head
720        let ptb = PageTableBuilder::new()
721            .map_4k(head_vaddr, head_paddr, flags::WRITABLE)
722            .map_4k(a_vaddr, a_paddr, flags::WRITABLE)
723            .map_4k(b_vaddr, b_paddr, flags::WRITABLE)
724            // head sentinel: Flink -> A.ActiveProcessLinks
725            .write_phys_u64(head_paddr, a_vaddr + list_offset) // Flink
726            // A: pid=4, ActiveProcessLinks.Flink -> B.ActiveProcessLinks
727            .write_phys_u64(a_paddr, 4) // UniqueProcessId
728            .write_phys_u64(a_paddr + list_offset, b_vaddr + list_offset) // Flink
729            // B: pid=100, ActiveProcessLinks.Flink -> head (loop back)
730            .write_phys_u64(b_paddr, 100) // UniqueProcessId
731            .write_phys_u64(b_paddr + list_offset, head_vaddr); // Flink -> head
732
733        let reader = make_reader(&isf, ptb);
734
735        let containers = reader
736            .walk_list_with(
737                head_vaddr,
738                "_LIST_ENTRY",
739                "Flink",
740                "_EPROCESS",
741                "ActiveProcessLinks",
742            )
743            .unwrap();
744
745        assert_eq!(containers.len(), 2);
746        assert_eq!(containers[0], a_vaddr);
747        assert_eq!(containers[1], b_vaddr);
748    }
749
750    #[test]
751    fn walk_list_with_tolerates_smeared_null_link() {
752        // Real raw dumps captured live contain "smear": a torn-down EPROCESS
753        // whose ActiveProcessLinks.Flink reads as 0 (validated on
754        // DESKTOP-SDN1RPT.mem at process #83, a duplicate pid-4096 empty-name
755        // rundown entry). The walk must return the processes collected so far —
756        // NOT hard-error and lose all of them, and NOT push a bogus container
757        // derived from the null pointer.
758        let isf = IsfBuilder::new()
759            .add_struct("_EPROCESS", 256)
760            .add_field("_EPROCESS", "ActiveProcessLinks", 0x10, "_LIST_ENTRY")
761            .add_struct("_LIST_ENTRY", 16)
762            .add_field("_LIST_ENTRY", "Flink", 0, "pointer")
763            .add_field("_LIST_ENTRY", "Blink", 8, "pointer");
764
765        let head_paddr: u64 = 0x0080_0000;
766        let a_paddr: u64 = 0x0080_1000;
767        let b_paddr: u64 = 0x0080_2000;
768        let head_vaddr: u64 = 0xFFFF_8000_0010_0000;
769        let a_vaddr: u64 = 0xFFFF_8000_0010_1000;
770        let b_vaddr: u64 = 0xFFFF_8000_0010_2000;
771        let list_offset: u64 = 0x10;
772
773        // head -> A -> B -> NULL (smear: B.Flink == 0, never loops back to head).
774        let ptb = PageTableBuilder::new()
775            .map_4k(head_vaddr, head_paddr, flags::WRITABLE)
776            .map_4k(a_vaddr, a_paddr, flags::WRITABLE)
777            .map_4k(b_vaddr, b_paddr, flags::WRITABLE)
778            .write_phys_u64(head_paddr, a_vaddr + list_offset)
779            .write_phys_u64(a_paddr + list_offset, b_vaddr + list_offset)
780            .write_phys_u64(b_paddr + list_offset, 0); // smeared null Flink
781
782        let reader = make_reader(&isf, ptb);
783
784        let containers = reader
785            .walk_list_with(
786                head_vaddr,
787                "_LIST_ENTRY",
788                "Flink",
789                "_EPROCESS",
790                "ActiveProcessLinks",
791            )
792            .expect("a smeared null link must terminate the walk gracefully, not error");
793
794        // A and B were reached before the smear; the null link is the terminus.
795        assert_eq!(containers, vec![a_vaddr, b_vaddr]);
796    }
797
798    #[test]
799    fn walk_list_with_stops_on_non_canonical_kernel_pointer() {
800        // A torn-down node's link can hold a non-canonical / user-half garbage
801        // value (DESKTOP-SDN1RPT.mem: a smeared Blink of 0x5a289000). The walk
802        // must treat it as a terminus — NOT fabricate a container from it (which
803        // a later field read would fault on), NOT error.
804        let isf = IsfBuilder::new()
805            .add_struct("_EPROCESS", 256)
806            .add_field("_EPROCESS", "ActiveProcessLinks", 0x10, "_LIST_ENTRY")
807            .add_struct("_LIST_ENTRY", 16)
808            .add_field("_LIST_ENTRY", "Flink", 0, "pointer")
809            .add_field("_LIST_ENTRY", "Blink", 8, "pointer");
810
811        let lo: u64 = 0x10;
812        let head_p = 0x0080_0000u64;
813        let a_p = 0x0080_1000u64;
814        let head_v = 0xFFFF_8000_0010_0000u64;
815        let a_v = 0xFFFF_8000_0010_1000u64;
816
817        // head -> A -> (garbage, non-canonical user-half pointer)
818        let ptb = PageTableBuilder::new()
819            .map_4k(head_v, head_p, flags::WRITABLE)
820            .map_4k(a_v, a_p, flags::WRITABLE)
821            .write_phys_u64(head_p, a_v + lo)
822            .write_phys_u64(a_p + lo, 0x0000_0000_5A28_9000); // non-canonical garbage
823
824        let reader = make_reader(&isf, ptb);
825        let containers = reader
826            .walk_list_with(
827                head_v,
828                "_LIST_ENTRY",
829                "Flink",
830                "_EPROCESS",
831                "ActiveProcessLinks",
832            )
833            .expect("non-canonical link terminates the walk, not errors");
834
835        assert_eq!(
836            containers,
837            vec![a_v],
838            "only the real node A; no bogus container"
839        );
840    }
841
842    #[test]
843    fn walk_list_bidirectional_recovers_forward_orphans() {
844        // A doubly-linked list whose FORWARD chain is smeared (B.Flink = 0) but
845        // whose BACKWARD chain (Blink) is intact. The forward walk reaches only
846        // A and B; the node C, orphaned forward, is still reachable via Blink
847        // from the head. A bidirectional walk must return all three. This is the
848        // DESKTOP-SDN1RPT.mem case: 11 processes after the smear are recovered
849        // from the Blink side.
850        let isf = IsfBuilder::new()
851            .add_struct("_EPROCESS", 256)
852            .add_field("_EPROCESS", "ActiveProcessLinks", 0x10, "_LIST_ENTRY")
853            .add_struct("_LIST_ENTRY", 16)
854            .add_field("_LIST_ENTRY", "Flink", 0, "pointer")
855            .add_field("_LIST_ENTRY", "Blink", 8, "pointer");
856
857        let lo: u64 = 0x10;
858        let head_p = 0x0080_0000u64;
859        let a_p = 0x0080_1000u64;
860        let b_p = 0x0080_2000u64;
861        let c_p = 0x0080_3000u64;
862        let head_v = 0xFFFF_8000_0010_0000u64;
863        let a_v = 0xFFFF_8000_0010_1000u64;
864        let b_v = 0xFFFF_8000_0010_2000u64;
865        let c_v = 0xFFFF_8000_0010_3000u64;
866
867        let ptb = PageTableBuilder::new()
868            .map_4k(head_v, head_p, flags::WRITABLE)
869            .map_4k(a_v, a_p, flags::WRITABLE)
870            .map_4k(b_v, b_p, flags::WRITABLE)
871            .map_4k(c_v, c_p, flags::WRITABLE)
872            // head: Flink -> A, Blink -> C
873            .write_phys_u64(head_p, a_v + lo)
874            .write_phys_u64(head_p + 8, c_v + lo)
875            // A: Flink -> B, Blink -> head
876            .write_phys_u64(a_p + lo, b_v + lo)
877            .write_phys_u64(a_p + lo + 8, head_v)
878            // B: Flink -> 0 (forward smear), Blink -> A
879            .write_phys_u64(b_p + lo, 0)
880            .write_phys_u64(b_p + lo + 8, a_v + lo)
881            // C: Flink -> head, Blink -> B
882            .write_phys_u64(c_p + lo, head_v)
883            .write_phys_u64(c_p + lo + 8, b_v + lo);
884
885        let reader = make_reader(&isf, ptb);
886
887        let containers = reader
888            .walk_list_bidirectional(
889                head_v,
890                "_LIST_ENTRY",
891                "Flink",
892                "Blink",
893                "_EPROCESS",
894                "ActiveProcessLinks",
895            )
896            .unwrap();
897
898        // Forward gives [A, B]; backward adds [C]. Order: forward first, then
899        // backward-only, deduplicated.
900        assert_eq!(
901            containers.len(),
902            3,
903            "all three nodes recovered: {containers:x?}"
904        );
905        assert!(containers.contains(&a_v));
906        assert!(containers.contains(&b_v));
907        assert!(
908            containers.contains(&c_v),
909            "forward-orphaned C recovered via Blink"
910        );
911    }
912
913    #[test]
914    fn walk_list_with_empty() {
915        // Empty list: head.Flink points back to head.
916        let isf = IsfBuilder::new()
917            .add_struct("_EPROCESS", 256)
918            .add_field("_EPROCESS", "ActiveProcessLinks", 0x10, "_LIST_ENTRY")
919            .add_struct("_LIST_ENTRY", 16)
920            .add_field("_LIST_ENTRY", "Flink", 0, "pointer")
921            .add_field("_LIST_ENTRY", "Blink", 8, "pointer");
922
923        let head_paddr: u64 = 0x0080_0000;
924        let head_vaddr: u64 = 0xFFFF_8000_0010_0000;
925
926        // head.Flink = head (empty circular list)
927        let ptb = PageTableBuilder::new()
928            .map_4k(head_vaddr, head_paddr, flags::WRITABLE)
929            .write_phys_u64(head_paddr, head_vaddr); // Flink -> self
930
931        let reader = make_reader(&isf, ptb);
932
933        let containers = reader
934            .walk_list_with(
935                head_vaddr,
936                "_LIST_ENTRY",
937                "Flink",
938                "_EPROCESS",
939                "ActiveProcessLinks",
940            )
941            .unwrap();
942
943        assert!(containers.is_empty());
944    }
945
946    #[test]
947    fn walk_list_still_works_after_refactor() {
948        // Ensure the existing walk_list (Linux list_head/next) still works
949        // after the refactor to call walk_list_with internally.
950        let isf = IsfBuilder::new()
951            .add_struct("task_struct", 128)
952            .add_field("task_struct", "pid", 0, "int")
953            .add_field("task_struct", "tasks", 8, "list_head")
954            .add_struct("list_head", 16)
955            .add_field("list_head", "next", 0, "pointer")
956            .add_field("list_head", "prev", 8, "pointer");
957
958        let head_paddr: u64 = 0x0080_0000;
959        let a_paddr: u64 = 0x0080_1000;
960
961        let head_vaddr: u64 = 0xFFFF_8000_0010_0000;
962        let a_vaddr: u64 = 0xFFFF_8000_0010_1000;
963
964        let list_offset: u64 = 8;
965
966        // Single-element list: head.next -> A.tasks -> head.tasks
967        let ptb = PageTableBuilder::new()
968            .map_4k(head_vaddr, head_paddr, flags::WRITABLE)
969            .map_4k(a_vaddr, a_paddr, flags::WRITABLE)
970            .write_phys_u64(head_paddr + list_offset, a_vaddr + list_offset)
971            .write_phys_u64(a_paddr, 42) // pid
972            .write_phys_u64(a_paddr + list_offset, head_vaddr + list_offset);
973
974        let reader = make_reader(&isf, ptb);
975
976        let containers = reader
977            .walk_list(head_vaddr + list_offset, "task_struct", "tasks")
978            .unwrap();
979        assert_eq!(containers.len(), 1);
980        assert_eq!(containers[0], a_vaddr);
981    }
982
983    #[test]
984    fn symbols_accessor() {
985        let isf = IsfBuilder::new()
986            .add_struct("task_struct", 128)
987            .add_field("task_struct", "pid", 0, "int")
988            .add_symbol("init_task", 0xFFFF_0000);
989
990        let vaddr: u64 = 0xFFFF_8000_0010_0000;
991        let paddr: u64 = 0x0080_0000;
992        let ptb = PageTableBuilder::new().map_4k(vaddr, paddr, flags::WRITABLE);
993
994        let reader = make_reader(&isf, ptb);
995        assert_eq!(reader.symbols().backend_name(), "ISF JSON");
996        assert_eq!(reader.symbols().field_offset("task_struct", "pid"), Some(0));
997    }
998
999    #[test]
1000    fn required_symbol_ok() {
1001        let isf = IsfBuilder::new()
1002            .add_struct("task_struct", 128)
1003            .add_symbol("init_task", 0xFFFF_8000_CAFE_0000);
1004
1005        let vaddr: u64 = 0xFFFF_8000_0010_0000;
1006        let paddr: u64 = 0x0080_0000;
1007        let ptb = PageTableBuilder::new().map_4k(vaddr, paddr, flags::WRITABLE);
1008
1009        let reader = make_reader(&isf, ptb);
1010        assert_eq!(
1011            reader.required_symbol("init_task").unwrap(),
1012            0xFFFF_8000_CAFE_0000
1013        );
1014    }
1015
1016    #[test]
1017    fn required_symbol_rebases_by_kernel_base() {
1018        let isf = IsfBuilder::new()
1019            .add_struct("x", 1)
1020            .add_symbol("PsActiveProcessHead", 0x002b_00a0);
1021        let reader =
1022            make_reader(&isf, PageTableBuilder::new()).with_kernel_base(0xFFFF_F800_CBE0_0000);
1023        assert_eq!(
1024            reader.required_symbol("PsActiveProcessHead").unwrap(),
1025            0xFFFF_F800_CC0B_00A0
1026        );
1027    }
1028
1029    #[test]
1030    fn required_symbol_missing_returns_error() {
1031        let isf = IsfBuilder::new().add_struct("task_struct", 128);
1032
1033        let vaddr: u64 = 0xFFFF_8000_0010_0000;
1034        let paddr: u64 = 0x0080_0000;
1035        let ptb = PageTableBuilder::new().map_4k(vaddr, paddr, flags::WRITABLE);
1036
1037        let reader = make_reader(&isf, ptb);
1038        assert!(reader.required_symbol("nonexistent").is_err());
1039    }
1040
1041    #[test]
1042    fn required_field_offset_ok() {
1043        let isf = IsfBuilder::new().add_struct("task_struct", 128).add_field(
1044            "task_struct",
1045            "pid",
1046            4,
1047            "int",
1048        );
1049
1050        let vaddr: u64 = 0xFFFF_8000_0010_0000;
1051        let paddr: u64 = 0x0080_0000;
1052        let ptb = PageTableBuilder::new().map_4k(vaddr, paddr, flags::WRITABLE);
1053
1054        let reader = make_reader(&isf, ptb);
1055        assert_eq!(
1056            reader.required_field_offset("task_struct", "pid").unwrap(),
1057            4
1058        );
1059    }
1060
1061    #[test]
1062    fn required_field_offset_missing_returns_error() {
1063        let isf = IsfBuilder::new().add_struct("task_struct", 128);
1064
1065        let vaddr: u64 = 0xFFFF_8000_0010_0000;
1066        let paddr: u64 = 0x0080_0000;
1067        let ptb = PageTableBuilder::new().map_4k(vaddr, paddr, flags::WRITABLE);
1068
1069        let reader = make_reader(&isf, ptb);
1070        assert!(reader
1071            .required_field_offset("task_struct", "nonexistent")
1072            .is_err());
1073    }
1074
1075    #[test]
1076    fn walk_list_cycle_detection() {
1077        // ISF: _EPROCESS with ActiveProcessLinks at offset 0x10;
1078        // _LIST_ENTRY with Flink at offset 0.
1079        let isf = IsfBuilder::new()
1080            .add_struct("_EPROCESS", 256)
1081            .add_field("_EPROCESS", "ActiveProcessLinks", 0x10, "_LIST_ENTRY")
1082            .add_struct("_LIST_ENTRY", 16)
1083            .add_field("_LIST_ENTRY", "Flink", 0, "pointer")
1084            .add_field("_LIST_ENTRY", "Blink", 8, "pointer");
1085
1086        // head: never referenced by the cycle, so the walk never terminates
1087        let head_paddr: u64 = 0x0080_0000;
1088        let a_paddr: u64 = 0x0080_1000;
1089        let b_paddr: u64 = 0x0080_2000;
1090
1091        let head_vaddr: u64 = 0xFFFF_8000_0010_0000;
1092        let a_vaddr: u64 = 0xFFFF_8000_0010_1000;
1093        let b_vaddr: u64 = 0xFFFF_8000_0010_2000;
1094
1095        let list_offset: u64 = 0x10; // ActiveProcessLinks offset
1096
1097        // head.Flink → a.ActiveProcessLinks (kick off the walk)
1098        // A.Flink → B.ActiveProcessLinks
1099        // B.Flink → A.ActiveProcessLinks  (cycle — never reaches head)
1100        let ptb = PageTableBuilder::new()
1101            .map_4k(head_vaddr, head_paddr, flags::WRITABLE)
1102            .map_4k(a_vaddr, a_paddr, flags::WRITABLE)
1103            .map_4k(b_vaddr, b_paddr, flags::WRITABLE)
1104            // head.Flink → a's list field
1105            .write_phys_u64(head_paddr, a_vaddr + list_offset)
1106            // A.ActiveProcessLinks.Flink → B.ActiveProcessLinks
1107            .write_phys_u64(a_paddr + list_offset, b_vaddr + list_offset)
1108            // B.ActiveProcessLinks.Flink → A.ActiveProcessLinks (cycle)
1109            .write_phys_u64(b_paddr + list_offset, a_vaddr + list_offset);
1110
1111        let reader = make_reader(&isf, ptb);
1112        let result = reader.walk_list_with(
1113            head_vaddr,
1114            "_LIST_ENTRY",
1115            "Flink",
1116            "_EPROCESS",
1117            "ActiveProcessLinks",
1118        );
1119
1120        assert!(
1121            matches!(result, Err(Error::ListCycle(_))),
1122            "expected ListCycle error, got: {result:?}"
1123        );
1124    }
1125
1126    #[test]
1127    fn iter_list_yields_same_as_walk_list() {
1128        let isf = IsfBuilder::new()
1129            .add_struct("task_struct", 128)
1130            .add_field("task_struct", "pid", 0, "int")
1131            .add_field("task_struct", "tasks", 8, "list_head")
1132            .add_field("task_struct", "comm", 16, "char")
1133            .add_struct("list_head", 16)
1134            .add_field("list_head", "next", 0, "pointer")
1135            .add_field("list_head", "prev", 8, "pointer");
1136
1137        let head_paddr: u64 = 0x0080_0000;
1138        let a_paddr: u64 = 0x0080_1000;
1139        let b_paddr: u64 = 0x0080_2000;
1140        let head_vaddr: u64 = 0xFFFF_8000_0010_0000;
1141        let a_vaddr: u64 = 0xFFFF_8000_0010_1000;
1142        let b_vaddr: u64 = 0xFFFF_8000_0010_2000;
1143        let list_offset: u64 = 8;
1144
1145        let ptb = PageTableBuilder::new()
1146            .map_4k(head_vaddr, head_paddr, flags::WRITABLE)
1147            .map_4k(a_vaddr, a_paddr, flags::WRITABLE)
1148            .map_4k(b_vaddr, b_paddr, flags::WRITABLE)
1149            .write_phys_u64(head_paddr, 0)
1150            .write_phys_u64(head_paddr + list_offset, a_vaddr + list_offset)
1151            .write_phys_u64(a_paddr, 100)
1152            .write_phys_u64(a_paddr + list_offset, b_vaddr + list_offset)
1153            .write_phys_u64(b_paddr, 200)
1154            .write_phys_u64(b_paddr + list_offset, head_vaddr + list_offset);
1155
1156        let reader = make_reader(&isf, ptb);
1157        let head = head_vaddr + list_offset;
1158
1159        let walk_result = reader.walk_list(head, "task_struct", "tasks").unwrap();
1160        let iter_result: Vec<u64> = reader
1161            .iter_list(head, "task_struct", "tasks")
1162            .collect::<crate::Result<Vec<_>>>()
1163            .unwrap();
1164        assert_eq!(iter_result, walk_result);
1165    }
1166
1167    #[test]
1168    fn iter_list_take_stops_early() {
1169        let isf = IsfBuilder::new()
1170            .add_struct("task_struct", 128)
1171            .add_field("task_struct", "pid", 0, "int")
1172            .add_field("task_struct", "tasks", 8, "list_head")
1173            .add_field("task_struct", "comm", 16, "char")
1174            .add_struct("list_head", 16)
1175            .add_field("list_head", "next", 0, "pointer")
1176            .add_field("list_head", "prev", 8, "pointer");
1177
1178        let head_paddr: u64 = 0x0080_0000;
1179        let a_paddr: u64 = 0x0080_1000;
1180        let b_paddr: u64 = 0x0080_2000;
1181        let head_vaddr: u64 = 0xFFFF_8000_0010_0000;
1182        let a_vaddr: u64 = 0xFFFF_8000_0010_1000;
1183        let b_vaddr: u64 = 0xFFFF_8000_0010_2000;
1184        let list_offset: u64 = 8;
1185
1186        let ptb = PageTableBuilder::new()
1187            .map_4k(head_vaddr, head_paddr, flags::WRITABLE)
1188            .map_4k(a_vaddr, a_paddr, flags::WRITABLE)
1189            .map_4k(b_vaddr, b_paddr, flags::WRITABLE)
1190            .write_phys_u64(head_paddr, 0)
1191            .write_phys_u64(head_paddr + list_offset, a_vaddr + list_offset)
1192            .write_phys_u64(a_paddr, 100)
1193            .write_phys_u64(a_paddr + list_offset, b_vaddr + list_offset)
1194            .write_phys_u64(b_paddr, 200)
1195            .write_phys_u64(b_paddr + list_offset, head_vaddr + list_offset);
1196
1197        let reader = make_reader(&isf, ptb);
1198        let head = head_vaddr + list_offset;
1199
1200        let first_two: Vec<u64> = reader
1201            .iter_list(head, "task_struct", "tasks")
1202            .take(2)
1203            .collect::<crate::Result<Vec<_>>>()
1204            .unwrap();
1205        assert_eq!(first_two.len(), 2);
1206    }
1207}