Skip to main content

memf_linux/
vma_walker.rs

1//! VMA region walker — shared abstraction for Linux walkers.
2//!
3//! Provides [`for_each_task_vma`], which encapsulates the repeated pattern of
4//! reading `mm_struct.mmap` and walking the VMA linked list via `vm_next`.
5//! Any walker that needs to inspect VMAs for a single task can delegate to
6//! this function instead of reimplementing the linked-list traversal.
7
8use std::collections::HashSet;
9
10use memf_core::object_reader::ObjectReader;
11use memf_format::PhysicalMemoryProvider;
12
13use crate::types::VmaFlags;
14
15/// Hard cap on VMAs walked for one task — defence in depth against a corrupt
16/// `vm_next` chain that is absurdly long but not strictly cyclic. The Linux
17/// default `vm.max_map_count` is 65 530, so this never trips on real data.
18const MAX_VMAS: usize = 1_000_000;
19
20/// Data read from a single `vm_area_struct` entry.
21#[derive(Debug, Clone)]
22pub struct VmaEntry {
23    /// Virtual address of the `vm_area_struct` in kernel memory.
24    pub vma_addr: u64,
25    /// First byte of the mapping (inclusive).
26    pub start: u64,
27    /// First byte past the mapping (exclusive).
28    pub end: u64,
29    /// Decoded page-protection flags.
30    pub flags: VmaFlags,
31    /// Pointer to the backing `struct file` (0 if anonymous).
32    pub file_ptr: u64,
33}
34
35/// Walk every VMA for a single task and call `callback` for each entry.
36///
37/// Gracefully skips the task if `mm == 0` (kernel thread or no address space)
38/// or if `mm_struct.mmap` is unreadable. Individual unreadable VMAs terminate
39/// the walk early (same behaviour as the walkers this replaces).
40///
41/// # Arguments
42///
43/// * `reader`     — kernel `ObjectReader` with the kernel CR3.
44/// * `task_addr`  — virtual address of the `task_struct`.
45/// * `callback`   — called for each successfully read VMA entry.
46pub fn for_each_task_vma<P, F>(reader: &ObjectReader<P>, task_addr: u64, callback: &mut F)
47where
48    P: PhysicalMemoryProvider,
49    F: FnMut(VmaEntry),
50{
51    let mm_ptr: u64 = match reader.read_field(task_addr, "task_struct", "mm") {
52        Ok(v) => v,
53        Err(_) => return,
54    };
55    if mm_ptr == 0 {
56        return;
57    }
58    let mmap_ptr: u64 = match reader.read_field(mm_ptr, "mm_struct", "mmap") {
59        Ok(v) => v,
60        Err(_) => return,
61    };
62
63    // Cycle / runaway guard: an attacker-controllable image can have a `vm_next`
64    // that points back into the list (e.g. a VMA whose vm_next is itself). Track
65    // visited VMA addresses and stop on revisit, plus a hard cap, so a corrupt
66    // chain can never spin forever.
67    let mut seen: HashSet<u64> = HashSet::new();
68    let mut vma_addr = mmap_ptr;
69    while vma_addr != 0 {
70        if seen.len() >= MAX_VMAS || !seen.insert(vma_addr) {
71            break;
72        }
73        let start: u64 = match reader.read_field(vma_addr, "vm_area_struct", "vm_start") {
74            Ok(v) => v,
75            Err(_) => break,
76        };
77        let end: u64 = match reader.read_field(vma_addr, "vm_area_struct", "vm_end") {
78            Ok(v) => v,
79            Err(_) => break,
80        };
81        let raw_flags: u64 = reader
82            .read_field(vma_addr, "vm_area_struct", "vm_flags")
83            .unwrap_or(0);
84        let file_ptr: u64 = reader
85            .read_field(vma_addr, "vm_area_struct", "vm_file")
86            .unwrap_or(0);
87
88        callback(VmaEntry {
89            vma_addr,
90            start,
91            end,
92            flags: VmaFlags::from_raw(raw_flags),
93            file_ptr,
94        });
95
96        vma_addr = reader
97            .read_field(vma_addr, "vm_area_struct", "vm_next")
98            .unwrap_or(0);
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use memf_core::test_builders::{flags as ptflags, PageTableBuilder};
106    use memf_symbols::test_builders::IsfBuilder;
107
108    fn make_reader_with_isf(
109        isf: &IsfBuilder,
110        ptb: PageTableBuilder,
111    ) -> memf_core::object_reader::ObjectReader<memf_core::test_builders::SyntheticPhysMem> {
112        let json = isf.build_json();
113        let resolver = memf_symbols::isf::IsfResolver::from_value(&json).unwrap();
114        let (cr3, mem) = ptb.build();
115        let vas = memf_core::vas::VirtualAddressSpace::new(
116            mem,
117            cr3,
118            memf_core::vas::TranslationMode::X86_64FourLevel,
119        );
120        memf_core::object_reader::ObjectReader::new(vas, Box::new(resolver))
121    }
122
123    // ---------------------------------------------------------------
124    // RED tests — these define the expected contract of for_each_task_vma
125    // ---------------------------------------------------------------
126
127    #[test]
128    fn task_with_null_mm_produces_no_vmas() {
129        // A task_struct where mm == 0 (kernel thread) must yield no entries.
130        let isf = IsfBuilder::new().add_struct("task_struct", 32).add_field(
131            "task_struct",
132            "mm",
133            0,
134            "pointer",
135        );
136        let task_vaddr: u64 = 0xFFFF_8000_0010_0000;
137        let task_paddr: u64 = 0x0010_0000;
138        let mut page = [0u8; 4096];
139        // mm field at offset 0 = 0 (null pointer)
140        page[0..8].copy_from_slice(&0u64.to_le_bytes());
141        let ptb = PageTableBuilder::new()
142            .map_4k(task_vaddr, task_paddr, ptflags::WRITABLE)
143            .write_phys(task_paddr, &page);
144        let reader = make_reader_with_isf(&isf, ptb);
145
146        let mut entries: Vec<VmaEntry> = Vec::new();
147        for_each_task_vma(&reader, task_vaddr, &mut |e| entries.push(e));
148        assert!(
149            entries.is_empty(),
150            "kernel thread (mm=0) should yield no VMAs"
151        );
152    }
153
154    #[test]
155    fn unreadable_mm_field_produces_no_vmas() {
156        // If the task_struct itself is not mapped, no VMAs should be yielded.
157        let isf = IsfBuilder::new().add_struct("task_struct", 32).add_field(
158            "task_struct",
159            "mm",
160            0,
161            "pointer",
162        );
163        let reader = memf_core::test_builders::make_reader(&isf);
164
165        let mut entries: Vec<VmaEntry> = Vec::new();
166        for_each_task_vma(&reader, 0xDEAD_BEEF_0000_0000, &mut |e| entries.push(e));
167        assert!(
168            entries.is_empty(),
169            "unreadable task_struct should yield no VMAs"
170        );
171    }
172
173    #[test]
174    fn single_vma_yields_correct_entry() {
175        // task_struct.mm → mm_struct.mmap → vm_area_struct → vm_next=0 (end)
176        let task_vaddr: u64 = 0xFFFF_8000_0001_0000;
177        let task_paddr: u64 = 0x0001_0000;
178        let mm_vaddr: u64 = 0xFFFF_8000_0002_0000;
179        let mm_paddr: u64 = 0x0002_0000;
180        let vma_vaddr: u64 = 0xFFFF_8000_0003_0000;
181        let vma_paddr: u64 = 0x0003_0000;
182
183        // task_struct layout: mm@0 (pointer, 8 bytes)
184        // mm_struct layout: mmap@0 (pointer, 8 bytes)
185        // vm_area_struct layout: vm_start@0, vm_end@8, vm_flags@16, vm_file@24, vm_next@32
186
187        let isf = IsfBuilder::new()
188            .add_struct("task_struct", 16)
189            .add_field("task_struct", "mm", 0, "pointer")
190            .add_struct("mm_struct", 16)
191            .add_field("mm_struct", "mmap", 0, "pointer")
192            .add_struct("vm_area_struct", 48)
193            .add_field("vm_area_struct", "vm_start", 0, "unsigned long")
194            .add_field("vm_area_struct", "vm_end", 8, "unsigned long")
195            .add_field("vm_area_struct", "vm_flags", 16, "unsigned long")
196            .add_field("vm_area_struct", "vm_file", 24, "pointer")
197            .add_field("vm_area_struct", "vm_next", 32, "pointer");
198
199        let mut task_page = [0u8; 4096];
200        task_page[0..8].copy_from_slice(&mm_vaddr.to_le_bytes()); // task.mm
201
202        let mut mm_page = [0u8; 4096];
203        mm_page[0..8].copy_from_slice(&vma_vaddr.to_le_bytes()); // mm.mmap
204
205        let vm_start: u64 = 0x0000_7FFF_0000_0000;
206        let vm_end: u64 = 0x0000_7FFF_0001_0000;
207        let vm_flags: u64 = 0x3; // read + write
208        let vm_file: u64 = 0; // anonymous
209        let vm_next: u64 = 0; // end of list
210
211        let mut vma_page = [0u8; 4096];
212        vma_page[0..8].copy_from_slice(&vm_start.to_le_bytes());
213        vma_page[8..16].copy_from_slice(&vm_end.to_le_bytes());
214        vma_page[16..24].copy_from_slice(&vm_flags.to_le_bytes());
215        vma_page[24..32].copy_from_slice(&vm_file.to_le_bytes());
216        vma_page[32..40].copy_from_slice(&vm_next.to_le_bytes());
217
218        let ptb = PageTableBuilder::new()
219            .map_4k(task_vaddr, task_paddr, ptflags::WRITABLE)
220            .write_phys(task_paddr, &task_page)
221            .map_4k(mm_vaddr, mm_paddr, ptflags::WRITABLE)
222            .write_phys(mm_paddr, &mm_page)
223            .map_4k(vma_vaddr, vma_paddr, ptflags::WRITABLE)
224            .write_phys(vma_paddr, &vma_page);
225
226        let reader = make_reader_with_isf(&isf, ptb);
227
228        let mut entries: Vec<VmaEntry> = Vec::new();
229        for_each_task_vma(&reader, task_vaddr, &mut |e| entries.push(e));
230
231        assert_eq!(entries.len(), 1, "expected exactly one VMA");
232        let e = &entries[0];
233        assert_eq!(e.start, vm_start);
234        assert_eq!(e.end, vm_end);
235        assert_eq!(e.file_ptr, 0, "anonymous mapping");
236        assert!(e.flags.read, "read flag should be set");
237        assert!(e.flags.write, "write flag should be set");
238        assert!(!e.flags.exec, "exec flag should not be set");
239    }
240
241    #[test]
242    fn cyclic_vm_next_terminates_after_one_visit() {
243        // An attacker-controllable image can have a vm_next that points back into
244        // the list. A VMA whose vm_next is itself must be visited once and then
245        // the cycle guard must stop the walk — never loop forever.
246        let task_vaddr: u64 = 0xFFFF_8000_0001_0000;
247        let task_paddr: u64 = 0x0001_0000;
248        let mm_vaddr: u64 = 0xFFFF_8000_0002_0000;
249        let mm_paddr: u64 = 0x0002_0000;
250        let vma_vaddr: u64 = 0xFFFF_8000_0003_0000;
251        let vma_paddr: u64 = 0x0003_0000;
252
253        let isf = IsfBuilder::new()
254            .add_struct("task_struct", 16)
255            .add_field("task_struct", "mm", 0, "pointer")
256            .add_struct("mm_struct", 16)
257            .add_field("mm_struct", "mmap", 0, "pointer")
258            .add_struct("vm_area_struct", 48)
259            .add_field("vm_area_struct", "vm_start", 0, "unsigned long")
260            .add_field("vm_area_struct", "vm_end", 8, "unsigned long")
261            .add_field("vm_area_struct", "vm_flags", 16, "unsigned long")
262            .add_field("vm_area_struct", "vm_file", 24, "pointer")
263            .add_field("vm_area_struct", "vm_next", 32, "pointer");
264
265        let mut task_page = [0u8; 4096];
266        task_page[0..8].copy_from_slice(&mm_vaddr.to_le_bytes());
267        let mut mm_page = [0u8; 4096];
268        mm_page[0..8].copy_from_slice(&vma_vaddr.to_le_bytes());
269
270        let mut vma_page = [0u8; 4096];
271        vma_page[0..8].copy_from_slice(&0x0000_7FFF_0000_0000u64.to_le_bytes()); // vm_start
272        vma_page[8..16].copy_from_slice(&0x0000_7FFF_0001_0000u64.to_le_bytes()); // vm_end
273        vma_page[16..24].copy_from_slice(&3u64.to_le_bytes()); // vm_flags
274        vma_page[24..32].copy_from_slice(&0u64.to_le_bytes()); // vm_file = anonymous
275        vma_page[32..40].copy_from_slice(&vma_vaddr.to_le_bytes()); // vm_next = self (cycle!)
276
277        let ptb = PageTableBuilder::new()
278            .map_4k(task_vaddr, task_paddr, ptflags::WRITABLE)
279            .write_phys(task_paddr, &task_page)
280            .map_4k(mm_vaddr, mm_paddr, ptflags::WRITABLE)
281            .write_phys(mm_paddr, &mm_page)
282            .map_4k(vma_vaddr, vma_paddr, ptflags::WRITABLE)
283            .write_phys(vma_paddr, &vma_page);
284
285        let reader = make_reader_with_isf(&isf, ptb);
286
287        let mut count = 0usize;
288        for_each_task_vma(&reader, task_vaddr, &mut |_| count += 1);
289        assert_eq!(
290            count, 1,
291            "self-referencing vm_next must be visited once, then the walk stops"
292        );
293    }
294
295    #[test]
296    fn two_vmas_chained_via_vm_next() {
297        let task_vaddr: u64 = 0xFFFF_8000_0001_0000;
298        let task_paddr: u64 = 0x0001_0000;
299        let mm_vaddr: u64 = 0xFFFF_8000_0002_0000;
300        let mm_paddr: u64 = 0x0002_0000;
301        let vma1_vaddr: u64 = 0xFFFF_8000_0003_0000;
302        let vma1_paddr: u64 = 0x0003_0000;
303        let vma2_vaddr: u64 = 0xFFFF_8000_0004_0000;
304        let vma2_paddr: u64 = 0x0004_0000;
305
306        let isf = IsfBuilder::new()
307            .add_struct("task_struct", 16)
308            .add_field("task_struct", "mm", 0, "pointer")
309            .add_struct("mm_struct", 16)
310            .add_field("mm_struct", "mmap", 0, "pointer")
311            .add_struct("vm_area_struct", 48)
312            .add_field("vm_area_struct", "vm_start", 0, "unsigned long")
313            .add_field("vm_area_struct", "vm_end", 8, "unsigned long")
314            .add_field("vm_area_struct", "vm_flags", 16, "unsigned long")
315            .add_field("vm_area_struct", "vm_file", 24, "pointer")
316            .add_field("vm_area_struct", "vm_next", 32, "pointer");
317
318        let mut task_page = [0u8; 4096];
319        task_page[0..8].copy_from_slice(&mm_vaddr.to_le_bytes());
320        let mut mm_page = [0u8; 4096];
321        mm_page[0..8].copy_from_slice(&vma1_vaddr.to_le_bytes());
322
323        // VMA 1: anonymous rw, vm_next → vma2
324        let mut vma1_page = [0u8; 4096];
325        vma1_page[0..8].copy_from_slice(&0x0000_7FFF_0000_0000u64.to_le_bytes());
326        vma1_page[8..16].copy_from_slice(&0x0000_7FFF_0001_0000u64.to_le_bytes());
327        vma1_page[16..24].copy_from_slice(&3u64.to_le_bytes()); // r+w
328        vma1_page[24..32].copy_from_slice(&0u64.to_le_bytes());
329        vma1_page[32..40].copy_from_slice(&vma2_vaddr.to_le_bytes());
330
331        // VMA 2: file-backed rx, vm_next = 0
332        let fake_file_ptr: u64 = 0xFFFF_8888_0000_0000;
333        let mut vma2_page = [0u8; 4096];
334        vma2_page[0..8].copy_from_slice(&0x0000_7FFF_0010_0000u64.to_le_bytes());
335        vma2_page[8..16].copy_from_slice(&0x0000_7FFF_0020_0000u64.to_le_bytes());
336        vma2_page[16..24].copy_from_slice(&5u64.to_le_bytes()); // r+x
337        vma2_page[24..32].copy_from_slice(&fake_file_ptr.to_le_bytes());
338        vma2_page[32..40].copy_from_slice(&0u64.to_le_bytes());
339
340        let ptb = PageTableBuilder::new()
341            .map_4k(task_vaddr, task_paddr, ptflags::WRITABLE)
342            .write_phys(task_paddr, &task_page)
343            .map_4k(mm_vaddr, mm_paddr, ptflags::WRITABLE)
344            .write_phys(mm_paddr, &mm_page)
345            .map_4k(vma1_vaddr, vma1_paddr, ptflags::WRITABLE)
346            .write_phys(vma1_paddr, &vma1_page)
347            .map_4k(vma2_vaddr, vma2_paddr, ptflags::WRITABLE)
348            .write_phys(vma2_paddr, &vma2_page);
349
350        let reader = make_reader_with_isf(&isf, ptb);
351
352        let mut entries: Vec<VmaEntry> = Vec::new();
353        for_each_task_vma(&reader, task_vaddr, &mut |e| entries.push(e));
354
355        assert_eq!(entries.len(), 2, "expected two VMAs");
356        assert_eq!(entries[0].file_ptr, 0, "vma1 is anonymous");
357        assert_eq!(entries[1].file_ptr, fake_file_ptr, "vma2 is file-backed");
358        assert!(entries[1].flags.read && entries[1].flags.exec, "vma2 is rx");
359    }
360}