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}
21
22impl<P: PhysicalMemoryProvider> ObjectReader<P> {
23    /// Create a new object reader.
24    pub fn new(vas: VirtualAddressSpace<P>, symbols: Box<dyn SymbolResolver>) -> Self {
25        Self { vas, symbols }
26    }
27
28    /// Access the underlying symbol resolver.
29    pub fn symbols(&self) -> &dyn SymbolResolver {
30        self.symbols.as_ref()
31    }
32
33    /// Access the underlying virtual address space.
34    pub fn vas(&self) -> &VirtualAddressSpace<P> {
35        &self.vas
36    }
37
38    /// Create a new reader sharing the same physical memory and symbols but
39    /// using a different page table root (CR3). Useful for switching to a
40    /// process's user-mode address space.
41    pub fn with_cr3(&self, cr3: u64) -> Self
42    where
43        P: Clone,
44    {
45        let vas = VirtualAddressSpace::new(self.vas.physical().clone(), cr3, self.vas.mode());
46        Self {
47            vas,
48            symbols: self.symbols.clone_boxed(),
49        }
50    }
51
52    /// Read a field from a struct at `base_vaddr` and interpret it as type `T`.
53    ///
54    /// Looks up the field offset from the symbol resolver, reads `size_of::<T>()`
55    /// bytes from virtual memory, and casts via `bytemuck::from_bytes`.
56    pub fn read_field<T: Pod + Default>(
57        &self,
58        base_vaddr: u64,
59        struct_name: &str,
60        field_name: &str,
61    ) -> Result<T> {
62        let offset = self
63            .symbols
64            .field_offset(struct_name, field_name)
65            .ok_or_else(|| Error::MissingSymbol(format!("{struct_name}.{field_name}")))?;
66
67        let size = std::mem::size_of::<T>();
68        let mut buf = vec![0u8; size];
69        self.vas
70            .read_virt(base_vaddr.wrapping_add(offset), &mut buf)?;
71
72        if buf.len() != size {
73            return Err(Error::SizeMismatch {
74                expected: size,
75                got: buf.len(),
76            });
77        }
78
79        Ok(*bytemuck::from_bytes::<T>(&buf))
80    }
81
82    /// Read a pointer (u64) from a struct field.
83    pub fn read_pointer(
84        &self,
85        base_vaddr: u64,
86        struct_name: &str,
87        field_name: &str,
88    ) -> Result<u64> {
89        self.read_field::<u64>(base_vaddr, struct_name, field_name)
90    }
91
92    /// Read a null-terminated string from virtual memory, up to `max_len` bytes.
93    pub fn read_string(&self, vaddr: u64, max_len: usize) -> Result<String> {
94        let mut buf = vec![0u8; max_len];
95        self.vas.read_virt(vaddr, &mut buf)?;
96
97        let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
98        Ok(String::from_utf8_lossy(&buf[..end]).into_owned())
99    }
100
101    /// Read a string from a struct field (the field contains inline char data, not a pointer).
102    pub fn read_field_string(
103        &self,
104        base_vaddr: u64,
105        struct_name: &str,
106        field_name: &str,
107        max_len: usize,
108    ) -> Result<String> {
109        let offset = self
110            .symbols
111            .field_offset(struct_name, field_name)
112            .ok_or_else(|| Error::MissingSymbol(format!("{struct_name}.{field_name}")))?;
113
114        self.read_string(base_vaddr.wrapping_add(offset), max_len)
115    }
116
117    /// Walk a Linux `list_head` doubly-linked list.
118    ///
119    /// Starting from `head_vaddr` (the address of the list_head embedded in the
120    /// head/sentinel node), follows `next` pointers and returns the virtual address
121    /// of each containing struct (using container_of logic with `list_field` offset).
122    ///
123    /// Stops when the walk loops back to `head_vaddr` or hits `MAX_LIST_ITERATIONS`.
124    pub fn walk_list(
125        &self,
126        head_vaddr: u64,
127        struct_name: &str,
128        list_field: &str,
129    ) -> Result<Vec<u64>> {
130        self.walk_list_with(head_vaddr, "list_head", "next", struct_name, list_field)
131    }
132
133    /// Walk a doubly-linked list with configurable list struct and field names.
134    ///
135    /// This is a generalized version of [`walk_list`](Self::walk_list) that works
136    /// with any linked-list structure, not just Linux `list_head`.
137    ///
138    /// For example, Windows uses `_LIST_ENTRY` with `Flink`/`Blink` fields
139    /// instead of `list_head` with `next`/`prev`.
140    ///
141    /// # Arguments
142    /// * `head_vaddr` — virtual address of the list head (sentinel node)
143    /// * `list_struct` — name of the list-link struct (e.g., `"list_head"`, `"_LIST_ENTRY"`)
144    /// * `next_field` — name of the forward pointer field (e.g., `"next"`, `"Flink"`)
145    /// * `container_struct` — name of the containing struct (e.g., `"_EPROCESS"`)
146    /// * `list_field` — name of the list-link field in the container struct (e.g., `"ActiveProcessLinks"`)
147    pub fn walk_list_with(
148        &self,
149        head_vaddr: u64,
150        list_struct: &str,
151        next_field: &str,
152        container_struct: &str,
153        list_field: &str,
154    ) -> Result<Vec<u64>> {
155        let list_offset = self
156            .symbols
157            .field_offset(container_struct, list_field)
158            .ok_or_else(|| Error::MissingSymbol(format!("{container_struct}.{list_field}")))?;
159
160        let next_offset = self
161            .symbols
162            .field_offset(list_struct, next_field)
163            .ok_or_else(|| Error::MissingSymbol(format!("{list_struct}.{next_field}")))?;
164
165        // Read the first forward pointer from head
166        let mut current = self.read_u64_at(head_vaddr.wrapping_add(next_offset))?;
167
168        let mut result = Vec::new();
169
170        for _ in 0..MAX_LIST_ITERATIONS {
171            // If we've looped back to head, the walk is complete
172            if current == head_vaddr {
173                return Ok(result);
174            }
175
176            // container_of: subtract list_offset to get the containing struct base
177            let container = current.wrapping_sub(list_offset);
178            result.push(container);
179
180            // Follow next/Flink pointer
181            current = self.read_u64_at(current.wrapping_add(next_offset))?;
182        }
183
184        Err(Error::ListCycle(MAX_LIST_ITERATIONS))
185    }
186
187    /// Read `len` raw bytes from virtual memory at `vaddr`.
188    pub fn read_bytes(&self, vaddr: u64, len: usize) -> Result<Vec<u8>> {
189        let mut buf = vec![0u8; len];
190        self.vas.read_virt(vaddr, &mut buf)?;
191        Ok(buf)
192    }
193
194    /// Resolve a global kernel symbol address, returning an error if absent.
195    pub fn required_symbol(&self, name: &str) -> Result<u64> {
196        self.symbols()
197            .symbol_address(name)
198            .ok_or_else(|| Error::MissingSymbol(name.to_owned()))
199    }
200
201    /// Resolve a struct field offset, returning an error if absent.
202    pub fn required_field_offset(&self, struct_name: &str, field_name: &str) -> Result<usize> {
203        self.symbols()
204            .field_offset(struct_name, field_name)
205            .map(|v| v as usize)
206            .ok_or_else(|| Error::MissingSymbol(format!("{struct_name}.{field_name}")))
207    }
208
209    fn read_u64_at(&self, vaddr: u64) -> Result<u64> {
210        let mut buf = [0u8; 8];
211        self.vas.read_virt(vaddr, &mut buf)?;
212        Ok(u64::from_le_bytes(buf))
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use crate::test_builders::{flags, PageTableBuilder};
220    use crate::vas::TranslationMode;
221    use memf_symbols::isf::IsfResolver;
222    use memf_symbols::test_builders::IsfBuilder;
223
224    fn make_reader(
225        isf: &IsfBuilder,
226        builder: PageTableBuilder,
227    ) -> ObjectReader<crate::test_builders::SyntheticPhysMem> {
228        let json = isf.build_json();
229        let resolver = IsfResolver::from_value(&json).unwrap();
230        let (cr3, mem) = builder.build();
231        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
232        ObjectReader::new(vas, Box::new(resolver))
233    }
234
235    #[test]
236    fn read_field_u32() {
237        let isf = IsfBuilder::new().add_struct("task_struct", 128).add_field(
238            "task_struct",
239            "pid",
240            0,
241            "int",
242        );
243
244        let vaddr: u64 = 0xFFFF_8000_0010_0000;
245        let paddr: u64 = 0x0080_0000;
246
247        let ptb = PageTableBuilder::new()
248            .map_4k(vaddr, paddr, flags::WRITABLE)
249            .write_phys_u64(paddr, 42u32 as u64); // pid = 42 at offset 0
250
251        let reader = make_reader(&isf, ptb);
252        let pid: u32 = reader.read_field(vaddr, "task_struct", "pid").unwrap();
253        assert_eq!(pid, 42);
254    }
255
256    #[test]
257    fn read_field_u64() {
258        let isf = IsfBuilder::new().add_struct("task_struct", 128).add_field(
259            "task_struct",
260            "mm",
261            8,
262            "pointer",
263        );
264
265        let vaddr: u64 = 0xFFFF_8000_0010_0000;
266        let paddr: u64 = 0x0080_0000;
267        let mm_value: u64 = 0xFFFF_8000_DEAD_BEEF;
268
269        let ptb = PageTableBuilder::new()
270            .map_4k(vaddr, paddr, flags::WRITABLE)
271            .write_phys_u64(paddr + 8, mm_value);
272
273        let reader = make_reader(&isf, ptb);
274        let mm: u64 = reader.read_field(vaddr, "task_struct", "mm").unwrap();
275        assert_eq!(mm, mm_value);
276    }
277
278    #[test]
279    fn read_field_missing_symbol() {
280        let isf = IsfBuilder::new().add_struct("task_struct", 128);
281
282        let vaddr: u64 = 0xFFFF_8000_0010_0000;
283        let paddr: u64 = 0x0080_0000;
284
285        let ptb = PageTableBuilder::new().map_4k(vaddr, paddr, flags::WRITABLE);
286
287        let reader = make_reader(&isf, ptb);
288        let result = reader.read_field::<u32>(vaddr, "task_struct", "nonexistent");
289        assert!(result.is_err());
290        match result.unwrap_err() {
291            Error::MissingSymbol(s) => assert_eq!(s, "task_struct.nonexistent"),
292            other => panic!("unexpected error: {other}"),
293        }
294    }
295
296    #[test]
297    fn read_field_string_test() {
298        let isf = IsfBuilder::new().add_struct("task_struct", 128).add_field(
299            "task_struct",
300            "comm",
301            16,
302            "char",
303        );
304
305        let vaddr: u64 = 0xFFFF_8000_0010_0000;
306        let paddr: u64 = 0x0080_0000;
307
308        let ptb = PageTableBuilder::new()
309            .map_4k(vaddr, paddr, flags::WRITABLE)
310            .write_phys(paddr + 16, b"systemd\0");
311
312        let reader = make_reader(&isf, ptb);
313        let comm = reader
314            .read_field_string(vaddr, "task_struct", "comm", 16)
315            .unwrap();
316        assert_eq!(comm, "systemd");
317    }
318
319    #[test]
320    fn read_string_with_null() {
321        let isf = IsfBuilder::new().add_struct("task_struct", 128).add_field(
322            "task_struct",
323            "comm",
324            16,
325            "char",
326        );
327
328        let vaddr: u64 = 0xFFFF_8000_0010_0000;
329        let paddr: u64 = 0x0080_0000;
330
331        let ptb = PageTableBuilder::new()
332            .map_4k(vaddr, paddr, flags::WRITABLE)
333            .write_phys(paddr + 16, b"init\0\0\0\0\0\0\0\0\0\0\0\0");
334
335        let reader = make_reader(&isf, ptb);
336        let s = reader.read_string(vaddr + 16, 16).unwrap();
337        assert_eq!(s, "init");
338    }
339
340    #[test]
341    fn walk_list_simple() {
342        // Create a simplified task_struct layout:
343        //   offset 0: pid (u32)
344        //   offset 8: tasks.next (u64)  -- list_head embedded at offset 8
345        //   offset 16: comm (16 bytes)
346        //   struct size: 128
347        let isf = IsfBuilder::new()
348            .add_struct("task_struct", 128)
349            .add_field("task_struct", "pid", 0, "int")
350            .add_field("task_struct", "tasks", 8, "list_head")
351            .add_field("task_struct", "comm", 16, "char")
352            .add_struct("list_head", 16)
353            .add_field("list_head", "next", 0, "pointer")
354            .add_field("list_head", "prev", 8, "pointer");
355
356        // Physical layout:
357        //   paddr 0x0080_0000: head task_struct (init_task)
358        //   paddr 0x0080_1000: task A
359        //   paddr 0x0080_2000: task B
360        //
361        // Circular list:
362        //   head.tasks.next -> A.tasks -> B.tasks -> head.tasks
363        let head_paddr: u64 = 0x0080_0000;
364        let a_paddr: u64 = 0x0080_1000;
365        let b_paddr: u64 = 0x0080_2000;
366
367        let head_vaddr: u64 = 0xFFFF_8000_0010_0000;
368        let a_vaddr: u64 = 0xFFFF_8000_0010_1000;
369        let b_vaddr: u64 = 0xFFFF_8000_0010_2000;
370
371        let list_offset: u64 = 8; // tasks field offset
372
373        // head.tasks.next = &A.tasks
374        // A.tasks.next = &B.tasks
375        // B.tasks.next = &head.tasks
376        let ptb = PageTableBuilder::new()
377            .map_4k(head_vaddr, head_paddr, flags::WRITABLE)
378            .map_4k(a_vaddr, a_paddr, flags::WRITABLE)
379            .map_4k(b_vaddr, b_paddr, flags::WRITABLE)
380            // head: pid=0, tasks.next = a_vaddr + list_offset
381            .write_phys_u64(head_paddr, 0) // pid
382            .write_phys_u64(head_paddr + list_offset, a_vaddr + list_offset) // tasks.next
383            // A: pid=100, tasks.next = b_vaddr + list_offset
384            .write_phys_u64(a_paddr, 100) // pid
385            .write_phys_u64(a_paddr + list_offset, b_vaddr + list_offset) // tasks.next
386            // B: pid=200, tasks.next = head_vaddr + list_offset (loops back)
387            .write_phys_u64(b_paddr, 200) // pid
388            .write_phys_u64(b_paddr + list_offset, head_vaddr + list_offset); // tasks.next
389
390        let reader = make_reader(&isf, ptb);
391
392        let containers = reader
393            .walk_list(head_vaddr + list_offset, "task_struct", "tasks")
394            .unwrap();
395        assert_eq!(containers.len(), 2);
396        assert_eq!(containers[0], a_vaddr);
397        assert_eq!(containers[1], b_vaddr);
398    }
399
400    #[test]
401    fn read_pointer_test() {
402        let isf = IsfBuilder::new().add_struct("task_struct", 128).add_field(
403            "task_struct",
404            "mm",
405            8,
406            "pointer",
407        );
408
409        let vaddr: u64 = 0xFFFF_8000_0010_0000;
410        let paddr: u64 = 0x0080_0000;
411        let mm_value: u64 = 0xFFFF_8000_CAFE_BABE;
412
413        let ptb = PageTableBuilder::new()
414            .map_4k(vaddr, paddr, flags::WRITABLE)
415            .write_phys_u64(paddr + 8, mm_value);
416
417        let reader = make_reader(&isf, ptb);
418        let ptr = reader.read_pointer(vaddr, "task_struct", "mm").unwrap();
419        assert_eq!(ptr, mm_value);
420    }
421
422    #[test]
423    fn read_field_invalid_struct_name() {
424        let isf = IsfBuilder::new().add_struct("task_struct", 128).add_field(
425            "task_struct",
426            "pid",
427            0,
428            "int",
429        );
430
431        let vaddr: u64 = 0xFFFF_8000_0010_0000;
432        let paddr: u64 = 0x0080_0000;
433
434        let ptb = PageTableBuilder::new().map_4k(vaddr, paddr, flags::WRITABLE);
435
436        let reader = make_reader(&isf, ptb);
437        let result = reader.read_field::<u32>(vaddr, "nonexistent_struct", "pid");
438        assert!(result.is_err());
439        match result.unwrap_err() {
440            Error::MissingSymbol(s) => assert_eq!(s, "nonexistent_struct.pid"),
441            other => panic!("unexpected error: {other}"),
442        }
443    }
444
445    #[test]
446    fn walk_list_empty_list() {
447        // A list where head.next points back to head (empty list)
448        let isf = IsfBuilder::new()
449            .add_struct("task_struct", 128)
450            .add_field("task_struct", "tasks", 8, "list_head")
451            .add_struct("list_head", 16)
452            .add_field("list_head", "next", 0, "pointer")
453            .add_field("list_head", "prev", 8, "pointer");
454
455        let head_paddr: u64 = 0x0080_0000;
456        let head_vaddr: u64 = 0xFFFF_8000_0010_0000;
457        let list_offset: u64 = 8;
458
459        // head.tasks.next = head.tasks (points back to itself -> empty list)
460        let ptb = PageTableBuilder::new()
461            .map_4k(head_vaddr, head_paddr, flags::WRITABLE)
462            .write_phys_u64(head_paddr + list_offset, head_vaddr + list_offset);
463
464        let reader = make_reader(&isf, ptb);
465        let containers = reader
466            .walk_list(head_vaddr + list_offset, "task_struct", "tasks")
467            .unwrap();
468        assert!(containers.is_empty());
469    }
470
471    #[test]
472    fn walk_list_with_windows_list_entry() {
473        // Test walk_list_with using Windows _LIST_ENTRY / Flink naming.
474        // Layout: _EPROCESS with ActiveProcessLinks at offset 0x10.
475        // _LIST_ENTRY with Flink at offset 0, Blink at offset 8.
476        let isf = IsfBuilder::new()
477            .add_struct("_EPROCESS", 256)
478            .add_field("_EPROCESS", "UniqueProcessId", 0, "pointer")
479            .add_field("_EPROCESS", "ActiveProcessLinks", 0x10, "_LIST_ENTRY")
480            .add_struct("_LIST_ENTRY", 16)
481            .add_field("_LIST_ENTRY", "Flink", 0, "pointer")
482            .add_field("_LIST_ENTRY", "Blink", 8, "pointer");
483
484        // Physical layout:
485        //   head (sentinel list head at some vaddr)
486        //   proc_a at paddr 0x0080_1000
487        //   proc_b at paddr 0x0080_2000
488        let head_paddr: u64 = 0x0080_0000;
489        let a_paddr: u64 = 0x0080_1000;
490        let b_paddr: u64 = 0x0080_2000;
491
492        let head_vaddr: u64 = 0xFFFF_8000_0010_0000; // sentinel list head
493        let a_vaddr: u64 = 0xFFFF_8000_0010_1000;
494        let b_vaddr: u64 = 0xFFFF_8000_0010_2000;
495
496        let list_offset: u64 = 0x10; // ActiveProcessLinks offset in _EPROCESS
497
498        // Circular: head.Flink -> A.ActiveProcessLinks -> B.ActiveProcessLinks -> head
499        let ptb = PageTableBuilder::new()
500            .map_4k(head_vaddr, head_paddr, flags::WRITABLE)
501            .map_4k(a_vaddr, a_paddr, flags::WRITABLE)
502            .map_4k(b_vaddr, b_paddr, flags::WRITABLE)
503            // head sentinel: Flink -> A.ActiveProcessLinks
504            .write_phys_u64(head_paddr, a_vaddr + list_offset) // Flink
505            // A: pid=4, ActiveProcessLinks.Flink -> B.ActiveProcessLinks
506            .write_phys_u64(a_paddr, 4) // UniqueProcessId
507            .write_phys_u64(a_paddr + list_offset, b_vaddr + list_offset) // Flink
508            // B: pid=100, ActiveProcessLinks.Flink -> head (loop back)
509            .write_phys_u64(b_paddr, 100) // UniqueProcessId
510            .write_phys_u64(b_paddr + list_offset, head_vaddr); // Flink -> head
511
512        let reader = make_reader(&isf, ptb);
513
514        let containers = reader
515            .walk_list_with(
516                head_vaddr,
517                "_LIST_ENTRY",
518                "Flink",
519                "_EPROCESS",
520                "ActiveProcessLinks",
521            )
522            .unwrap();
523
524        assert_eq!(containers.len(), 2);
525        assert_eq!(containers[0], a_vaddr);
526        assert_eq!(containers[1], b_vaddr);
527    }
528
529    #[test]
530    fn walk_list_with_empty() {
531        // Empty list: head.Flink points back to head.
532        let isf = IsfBuilder::new()
533            .add_struct("_EPROCESS", 256)
534            .add_field("_EPROCESS", "ActiveProcessLinks", 0x10, "_LIST_ENTRY")
535            .add_struct("_LIST_ENTRY", 16)
536            .add_field("_LIST_ENTRY", "Flink", 0, "pointer")
537            .add_field("_LIST_ENTRY", "Blink", 8, "pointer");
538
539        let head_paddr: u64 = 0x0080_0000;
540        let head_vaddr: u64 = 0xFFFF_8000_0010_0000;
541
542        // head.Flink = head (empty circular list)
543        let ptb = PageTableBuilder::new()
544            .map_4k(head_vaddr, head_paddr, flags::WRITABLE)
545            .write_phys_u64(head_paddr, head_vaddr); // Flink -> self
546
547        let reader = make_reader(&isf, ptb);
548
549        let containers = reader
550            .walk_list_with(
551                head_vaddr,
552                "_LIST_ENTRY",
553                "Flink",
554                "_EPROCESS",
555                "ActiveProcessLinks",
556            )
557            .unwrap();
558
559        assert!(containers.is_empty());
560    }
561
562    #[test]
563    fn walk_list_still_works_after_refactor() {
564        // Ensure the existing walk_list (Linux list_head/next) still works
565        // after the refactor to call walk_list_with internally.
566        let isf = IsfBuilder::new()
567            .add_struct("task_struct", 128)
568            .add_field("task_struct", "pid", 0, "int")
569            .add_field("task_struct", "tasks", 8, "list_head")
570            .add_struct("list_head", 16)
571            .add_field("list_head", "next", 0, "pointer")
572            .add_field("list_head", "prev", 8, "pointer");
573
574        let head_paddr: u64 = 0x0080_0000;
575        let a_paddr: u64 = 0x0080_1000;
576
577        let head_vaddr: u64 = 0xFFFF_8000_0010_0000;
578        let a_vaddr: u64 = 0xFFFF_8000_0010_1000;
579
580        let list_offset: u64 = 8;
581
582        // Single-element list: head.next -> A.tasks -> head.tasks
583        let ptb = PageTableBuilder::new()
584            .map_4k(head_vaddr, head_paddr, flags::WRITABLE)
585            .map_4k(a_vaddr, a_paddr, flags::WRITABLE)
586            .write_phys_u64(head_paddr + list_offset, a_vaddr + list_offset)
587            .write_phys_u64(a_paddr, 42) // pid
588            .write_phys_u64(a_paddr + list_offset, head_vaddr + list_offset);
589
590        let reader = make_reader(&isf, ptb);
591
592        let containers = reader
593            .walk_list(head_vaddr + list_offset, "task_struct", "tasks")
594            .unwrap();
595        assert_eq!(containers.len(), 1);
596        assert_eq!(containers[0], a_vaddr);
597    }
598
599    #[test]
600    fn symbols_accessor() {
601        let isf = IsfBuilder::new()
602            .add_struct("task_struct", 128)
603            .add_field("task_struct", "pid", 0, "int")
604            .add_symbol("init_task", 0xFFFF_0000);
605
606        let vaddr: u64 = 0xFFFF_8000_0010_0000;
607        let paddr: u64 = 0x0080_0000;
608        let ptb = PageTableBuilder::new().map_4k(vaddr, paddr, flags::WRITABLE);
609
610        let reader = make_reader(&isf, ptb);
611        assert_eq!(reader.symbols().backend_name(), "ISF JSON");
612        assert_eq!(reader.symbols().field_offset("task_struct", "pid"), Some(0));
613    }
614
615    #[test]
616    fn required_symbol_ok() {
617        let isf = IsfBuilder::new()
618            .add_struct("task_struct", 128)
619            .add_symbol("init_task", 0xFFFF_8000_CAFE_0000);
620
621        let vaddr: u64 = 0xFFFF_8000_0010_0000;
622        let paddr: u64 = 0x0080_0000;
623        let ptb = PageTableBuilder::new().map_4k(vaddr, paddr, flags::WRITABLE);
624
625        let reader = make_reader(&isf, ptb);
626        assert_eq!(
627            reader.required_symbol("init_task").unwrap(),
628            0xFFFF_8000_CAFE_0000
629        );
630    }
631
632    #[test]
633    fn required_symbol_missing_returns_error() {
634        let isf = IsfBuilder::new().add_struct("task_struct", 128);
635
636        let vaddr: u64 = 0xFFFF_8000_0010_0000;
637        let paddr: u64 = 0x0080_0000;
638        let ptb = PageTableBuilder::new().map_4k(vaddr, paddr, flags::WRITABLE);
639
640        let reader = make_reader(&isf, ptb);
641        assert!(reader.required_symbol("nonexistent").is_err());
642    }
643
644    #[test]
645    fn required_field_offset_ok() {
646        let isf = IsfBuilder::new()
647            .add_struct("task_struct", 128)
648            .add_field("task_struct", "pid", 4, "int");
649
650        let vaddr: u64 = 0xFFFF_8000_0010_0000;
651        let paddr: u64 = 0x0080_0000;
652        let ptb = PageTableBuilder::new().map_4k(vaddr, paddr, flags::WRITABLE);
653
654        let reader = make_reader(&isf, ptb);
655        assert_eq!(
656            reader.required_field_offset("task_struct", "pid").unwrap(),
657            4
658        );
659    }
660
661    #[test]
662    fn required_field_offset_missing_returns_error() {
663        let isf = IsfBuilder::new().add_struct("task_struct", 128);
664
665        let vaddr: u64 = 0xFFFF_8000_0010_0000;
666        let paddr: u64 = 0x0080_0000;
667        let ptb = PageTableBuilder::new().map_4k(vaddr, paddr, flags::WRITABLE);
668
669        let reader = make_reader(&isf, ptb);
670        assert!(reader
671            .required_field_offset("task_struct", "nonexistent")
672            .is_err());
673    }
674
675    #[test]
676    fn walk_list_cycle_detection() {
677        // ISF: _EPROCESS with ActiveProcessLinks at offset 0x10;
678        // _LIST_ENTRY with Flink at offset 0.
679        let isf = IsfBuilder::new()
680            .add_struct("_EPROCESS", 256)
681            .add_field("_EPROCESS", "ActiveProcessLinks", 0x10, "_LIST_ENTRY")
682            .add_struct("_LIST_ENTRY", 16)
683            .add_field("_LIST_ENTRY", "Flink", 0, "pointer")
684            .add_field("_LIST_ENTRY", "Blink", 8, "pointer");
685
686        // head: never referenced by the cycle, so the walk never terminates
687        let head_paddr: u64 = 0x0080_0000;
688        let a_paddr: u64 = 0x0080_1000;
689        let b_paddr: u64 = 0x0080_2000;
690
691        let head_vaddr: u64 = 0xFFFF_8000_0010_0000;
692        let a_vaddr: u64 = 0xFFFF_8000_0010_1000;
693        let b_vaddr: u64 = 0xFFFF_8000_0010_2000;
694
695        let list_offset: u64 = 0x10; // ActiveProcessLinks offset
696
697        // head.Flink → a.ActiveProcessLinks (kick off the walk)
698        // A.Flink → B.ActiveProcessLinks
699        // B.Flink → A.ActiveProcessLinks  (cycle — never reaches head)
700        let ptb = PageTableBuilder::new()
701            .map_4k(head_vaddr, head_paddr, flags::WRITABLE)
702            .map_4k(a_vaddr, a_paddr, flags::WRITABLE)
703            .map_4k(b_vaddr, b_paddr, flags::WRITABLE)
704            // head.Flink → a's list field
705            .write_phys_u64(head_paddr, a_vaddr + list_offset)
706            // A.ActiveProcessLinks.Flink → B.ActiveProcessLinks
707            .write_phys_u64(a_paddr + list_offset, b_vaddr + list_offset)
708            // B.ActiveProcessLinks.Flink → A.ActiveProcessLinks (cycle)
709            .write_phys_u64(b_paddr + list_offset, a_vaddr + list_offset);
710
711        let reader = make_reader(&isf, ptb);
712        let result = reader.walk_list_with(
713            head_vaddr,
714            "_LIST_ENTRY",
715            "Flink",
716            "_EPROCESS",
717            "ActiveProcessLinks",
718        );
719
720        assert!(
721            matches!(result, Err(Error::ListCycle(_))),
722            "expected ListCycle error, got: {result:?}"
723        );
724    }
725}