Skip to main content

memf_linux/
maps.rs

1//! Linux process memory map (VMA) walker.
2//!
3//! Enumerates virtual memory areas by walking the `vm_area_struct` singly-linked
4//! list from `mm_struct.mmap` for each process in the task list.
5
6use memf_core::object_reader::ObjectReader;
7use memf_format::PhysicalMemoryProvider;
8
9use crate::{Error, Result, VmaFlags, VmaInfo};
10
11/// Walk all process VMAs from the task list.
12///
13/// For each process, follows `task_struct.mm → mm_struct.mmap` to the head
14/// of the `vm_area_struct` chain, then traverses via `vm_next` pointers.
15pub fn walk_maps<P: PhysicalMemoryProvider>(reader: &ObjectReader<P>) -> Result<Vec<VmaInfo>> {
16    let init_task_addr = reader
17        .symbols()
18        .symbol_address("init_task")
19        .ok_or_else(|| Error::MissingKernelSymbol {
20            name: "init_task".into(),
21        })?;
22
23    let tasks_offset = reader
24        .symbols()
25        .field_offset("task_struct", "tasks")
26        .ok_or_else(|| Error::MissingField {
27            struct_name: "task_struct".into(),
28            field_name: "tasks".into(),
29        })?;
30
31    let head_vaddr = init_task_addr + tasks_offset;
32    let task_addrs = reader.walk_list(head_vaddr, "task_struct", "tasks")?;
33
34    let mut all_vmas = Vec::new();
35
36    // Include init_task itself
37    collect_process_vmas(reader, init_task_addr, &mut all_vmas);
38
39    for &task_addr in &task_addrs {
40        collect_process_vmas(reader, task_addr, &mut all_vmas);
41    }
42
43    Ok(all_vmas)
44}
45
46/// Collect VMAs for a single process, silently skipping kernel threads (mm == NULL).
47fn collect_process_vmas<P: PhysicalMemoryProvider>(
48    reader: &ObjectReader<P>,
49    task_addr: u64,
50    out: &mut Vec<VmaInfo>,
51) {
52    let mm_ptr: u64 = match reader.read_field(task_addr, "task_struct", "mm") {
53        Ok(v) => v,
54        Err(_) => return,
55    };
56    if mm_ptr == 0 {
57        return; // kernel thread — no user VMAs
58    }
59
60    if let Ok(vmas) = walk_process_maps(reader, task_addr) {
61        out.extend(vmas);
62    }
63}
64
65/// Walk VMAs for a single process given its `task_struct` address.
66pub fn walk_process_maps<P: PhysicalMemoryProvider>(
67    reader: &ObjectReader<P>,
68    task_addr: u64,
69) -> Result<Vec<VmaInfo>> {
70    let pid: u32 = reader.read_field(task_addr, "task_struct", "pid")?;
71    let comm = reader.read_field_string(task_addr, "task_struct", "comm", 16)?;
72    let mm_ptr: u64 = reader.read_field(task_addr, "task_struct", "mm")?;
73
74    if mm_ptr == 0 {
75        return Err(Error::WalkFailed {
76            walker: "walk_process_maps",
77            reason: format!("task {comm} (PID {pid}) has NULL mm (kernel thread)"),
78        });
79    }
80
81    let mmap_ptr: u64 = reader.read_field(mm_ptr, "mm_struct", "mmap")?;
82
83    let mut vmas = Vec::new();
84    let mut vma_addr = mmap_ptr;
85
86    // Walk the singly-linked vm_next chain (NULL-terminated)
87    while vma_addr != 0 {
88        let vm_start: u64 = reader.read_field(vma_addr, "vm_area_struct", "vm_start")?;
89        let vm_end: u64 = reader.read_field(vma_addr, "vm_area_struct", "vm_end")?;
90        let vm_flags: u64 = reader.read_field(vma_addr, "vm_area_struct", "vm_flags")?;
91        let vm_pgoff: u64 = reader.read_field(vma_addr, "vm_area_struct", "vm_pgoff")?;
92        let vm_file: u64 = reader.read_field(vma_addr, "vm_area_struct", "vm_file")?;
93
94        vmas.push(VmaInfo {
95            pid: u64::from(pid),
96            comm: comm.clone(),
97            start: vm_start,
98            end: vm_end,
99            flags: VmaFlags::from_raw(vm_flags),
100            pgoff: vm_pgoff,
101            file_backed: vm_file != 0,
102        });
103
104        vma_addr = reader.read_field(vma_addr, "vm_area_struct", "vm_next")?;
105    }
106
107    Ok(vmas)
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use memf_core::test_builders::{flags, PageTableBuilder, SyntheticPhysMem};
114    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
115    use memf_symbols::isf::IsfResolver;
116    use memf_symbols::test_builders::IsfBuilder;
117
118    fn make_test_reader(data: &[u8], vaddr: u64, paddr: u64) -> ObjectReader<SyntheticPhysMem> {
119        let isf = IsfBuilder::new()
120            // task_struct
121            .add_struct("task_struct", 128)
122            .add_field("task_struct", "pid", 0, "int")
123            .add_field("task_struct", "state", 4, "long")
124            .add_field("task_struct", "tasks", 16, "list_head")
125            .add_field("task_struct", "comm", 32, "char")
126            .add_field("task_struct", "mm", 48, "pointer")
127            .add_field("task_struct", "real_parent", 56, "pointer")
128            // list_head
129            .add_struct("list_head", 16)
130            .add_field("list_head", "next", 0, "pointer")
131            .add_field("list_head", "prev", 8, "pointer")
132            // mm_struct
133            .add_struct("mm_struct", 128)
134            .add_field("mm_struct", "pgd", 0, "pointer")
135            .add_field("mm_struct", "mmap", 8, "pointer")
136            // vm_area_struct
137            .add_struct("vm_area_struct", 64)
138            .add_field("vm_area_struct", "vm_start", 0, "unsigned long")
139            .add_field("vm_area_struct", "vm_end", 8, "unsigned long")
140            .add_field("vm_area_struct", "vm_next", 16, "pointer")
141            .add_field("vm_area_struct", "vm_flags", 24, "unsigned long")
142            .add_field("vm_area_struct", "vm_pgoff", 32, "unsigned long")
143            .add_field("vm_area_struct", "vm_file", 40, "pointer")
144            // symbol
145            .add_symbol("init_task", vaddr)
146            .build_json();
147
148        let resolver = IsfResolver::from_value(&isf).unwrap();
149        let (cr3, mem) = PageTableBuilder::new()
150            .map_4k(vaddr, paddr, flags::WRITABLE)
151            .write_phys(paddr, data)
152            .build();
153        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
154        ObjectReader::new(vas, Box::new(resolver))
155    }
156
157    #[test]
158    fn walk_single_process_two_vmas() {
159        let vaddr: u64 = 0xFFFF_8000_0010_0000;
160        let paddr: u64 = 0x0080_0000;
161        let mut data = vec![0u8; 4096];
162
163        // init_task (PID 1, "systemd")
164        data[0..4].copy_from_slice(&1u32.to_le_bytes()); // pid
165        data[4..12].copy_from_slice(&0i64.to_le_bytes()); // state
166        let tasks_addr = vaddr + 16;
167        data[16..24].copy_from_slice(&tasks_addr.to_le_bytes()); // tasks.next → self
168        data[24..32].copy_from_slice(&tasks_addr.to_le_bytes()); // tasks.prev → self
169        data[32..39].copy_from_slice(b"systemd"); // comm
170        let mm_addr = vaddr + 0x200;
171        data[48..56].copy_from_slice(&mm_addr.to_le_bytes()); // mm → mm_struct
172
173        // mm_struct at +0x200
174        data[0x200..0x208].copy_from_slice(&0x1000u64.to_le_bytes()); // pgd (unused here)
175        let vma1_addr = vaddr + 0x300;
176        data[0x208..0x210].copy_from_slice(&vma1_addr.to_le_bytes()); // mmap → first VMA
177
178        // VMA #1 at +0x300: code segment (r-x)
179        data[0x300..0x308].copy_from_slice(&0x0040_0000u64.to_le_bytes()); // vm_start
180        data[0x308..0x310].copy_from_slice(&0x0040_1000u64.to_le_bytes()); // vm_end
181        let vma2_addr = vaddr + 0x400;
182        data[0x310..0x318].copy_from_slice(&vma2_addr.to_le_bytes()); // vm_next → VMA #2
183        data[0x318..0x320].copy_from_slice(&0x5u64.to_le_bytes()); // vm_flags: r-x
184        data[0x320..0x328].copy_from_slice(&0u64.to_le_bytes()); // vm_pgoff
185        data[0x328..0x330].copy_from_slice(&0x9999u64.to_le_bytes()); // vm_file (non-null)
186
187        // VMA #2 at +0x400: heap (rw-)
188        data[0x400..0x408].copy_from_slice(&0x7FFF_0000u64.to_le_bytes()); // vm_start
189        data[0x408..0x410].copy_from_slice(&0x7FFF_2000u64.to_le_bytes()); // vm_end
190        data[0x410..0x418].copy_from_slice(&0u64.to_le_bytes()); // vm_next: NULL (end)
191        data[0x418..0x420].copy_from_slice(&0x3u64.to_le_bytes()); // vm_flags: rw-
192        data[0x420..0x428].copy_from_slice(&0u64.to_le_bytes()); // vm_pgoff
193        data[0x428..0x430].copy_from_slice(&0u64.to_le_bytes()); // vm_file: NULL (anon)
194
195        let reader = make_test_reader(&data, vaddr, paddr);
196        let vmas = walk_maps(&reader).unwrap();
197
198        assert_eq!(vmas.len(), 2);
199
200        assert_eq!(vmas[0].pid, 1);
201        assert_eq!(vmas[0].comm, "systemd");
202        assert_eq!(vmas[0].start, 0x0040_0000);
203        assert_eq!(vmas[0].end, 0x0040_1000);
204        assert!(vmas[0].flags.read);
205        assert!(!vmas[0].flags.write);
206        assert!(vmas[0].flags.exec);
207        assert!(vmas[0].file_backed);
208
209        assert_eq!(vmas[1].start, 0x7FFF_0000);
210        assert_eq!(vmas[1].end, 0x7FFF_2000);
211        assert!(vmas[1].flags.read);
212        assert!(vmas[1].flags.write);
213        assert!(!vmas[1].flags.exec);
214        assert!(!vmas[1].file_backed);
215    }
216
217    #[test]
218    fn walk_maps_skips_kernel_threads() {
219        // Kernel threads have mm == NULL — should produce no VMAs
220        let vaddr: u64 = 0xFFFF_8000_0010_0000;
221        let paddr: u64 = 0x0080_0000;
222        let mut data = vec![0u8; 4096];
223
224        // init_task (PID 0, "swapper/0", mm = NULL)
225        data[0..4].copy_from_slice(&0u32.to_le_bytes());
226        let tasks_addr = vaddr + 16;
227        data[16..24].copy_from_slice(&tasks_addr.to_le_bytes());
228        data[24..32].copy_from_slice(&tasks_addr.to_le_bytes());
229        data[32..41].copy_from_slice(b"swapper/0");
230        data[48..56].copy_from_slice(&0u64.to_le_bytes()); // mm = NULL
231
232        let reader = make_test_reader(&data, vaddr, paddr);
233        let vmas = walk_maps(&reader).unwrap();
234
235        assert!(vmas.is_empty());
236    }
237
238    #[test]
239    fn walk_process_maps_returns_error_for_missing_mm() {
240        let vaddr: u64 = 0xFFFF_8000_0010_0000;
241        let paddr: u64 = 0x0080_0000;
242        let mut data = vec![0u8; 4096];
243
244        data[48..56].copy_from_slice(&0u64.to_le_bytes()); // mm = NULL
245
246        let reader = make_test_reader(&data, vaddr, paddr);
247        let result = walk_process_maps(&reader, vaddr);
248        assert!(result.is_err());
249    }
250
251    #[test]
252    fn missing_init_task_symbol() {
253        let isf = IsfBuilder::new()
254            .add_struct("task_struct", 64)
255            .add_field("task_struct", "pid", 0, "int")
256            .add_field("task_struct", "tasks", 8, "list_head")
257            .add_struct("list_head", 16)
258            .add_field("list_head", "next", 0, "pointer")
259            .add_field("list_head", "prev", 8, "pointer")
260            .build_json();
261
262        let resolver = IsfResolver::from_value(&isf).unwrap();
263        let (cr3, mem) = PageTableBuilder::new().build();
264        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
265        let reader = ObjectReader::new(vas, Box::new(resolver));
266
267        let result = walk_maps(&reader);
268        assert!(
269            matches!(result, Err(crate::Error::MissingKernelSymbol { ref name }) if name == "init_task"),
270            "expected MissingKernelSymbol {{name: \"init_task\"}}, got {result:?}"
271        );
272    }
273
274    #[test]
275    fn missing_tasks_field_returns_missing_field() {
276        let isf = IsfBuilder::new()
277            .add_struct("task_struct", 64)
278            .add_field("task_struct", "pid", 0, "int")
279            // tasks intentionally omitted
280            .add_struct("list_head", 16)
281            .add_field("list_head", "next", 0, "pointer")
282            .add_field("list_head", "prev", 8, "pointer")
283            .add_symbol("init_task", 0xFFFF_8000_0010_0000)
284            .build_json();
285        let resolver = IsfResolver::from_value(&isf).unwrap();
286        let (cr3, mem) = PageTableBuilder::new().build();
287        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
288        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
289        let result = walk_maps(&reader);
290        assert!(
291            matches!(result, Err(crate::Error::MissingField { ref struct_name, ref field_name }) if struct_name == "task_struct" && field_name == "tasks"),
292            "expected MissingField task_struct.tasks, got {result:?}"
293        );
294    }
295}