Skip to main content

memf_linux/
memfd_create.rs

1//! Detect fileless payloads loaded via `memfd_create(2)`.
2//!
3//! `memfd_create` creates an anonymous file living only in RAM. Malware uses
4//! this to load shellcode or staged payloads without touching disk. The file
5//! descriptor appears in the process's open-fd table with a dentry name of
6//! `memfd:<name>` (e.g. `memfd:payload`).
7//!
8//! MITRE ATT&CK: T1055.009 — Process Injection: Process Hollowing (via anonymous memory).
9
10use memf_core::object_reader::ObjectReader;
11use memf_format::PhysicalMemoryProvider;
12use serde::Serialize;
13
14use crate::{vma_walker::for_each_task_vma, Result};
15
16/// VM flag bit: region is executable.
17const VM_EXEC: u64 = 0x4;
18
19/// Information about an open `memfd_create` file descriptor.
20#[derive(Debug, Clone, Serialize)]
21pub struct MemfdInfo {
22    /// Process ID.
23    pub pid: u32,
24    /// Process command name (`task_struct.comm`, max 16 chars).
25    pub comm: String,
26    /// Name given to `memfd_create`, e.g. `"payload"` (without the `memfd:` prefix).
27    pub memfd_name: String,
28    /// Total byte size of all VMAs backed by this memfd.
29    pub size_bytes: u64,
30    /// Whether any VMA backed by this memfd is mapped executable (`PROT_EXEC`).
31    pub is_executable: bool,
32    /// Whether this memfd is considered suspicious.
33    pub is_suspicious: bool,
34}
35
36/// Classify whether a memfd mapping is suspicious.
37///
38/// Returns `true` (suspicious) if any of:
39/// - `is_executable` — executable anonymous memory implies injected code.
40/// - `name` contains a known-malicious substring (`payload`, `shellcode`, …).
41/// - `name` is empty — anonymous memfd with no name is an evasion technique.
42///
43/// Returns `false` (benign) if `name` starts with a known-benign prefix.
44pub use crate::heuristics::classify_memfd;
45
46/// Walk the task list and collect information about open `memfd_create` file descriptors.
47///
48/// For each process, walks `mm_struct.mmap` (the VMA chain). For every VMA
49/// that is file-backed, the dentry name is read; if it starts with `"memfd:"`
50/// the mapping is recorded.
51///
52/// Gracefully returns `Ok(vec![])` if any required kernel symbol is absent,
53/// so callers on unexpected kernel versions are not broken.
54pub fn walk_memfd_create<P: PhysicalMemoryProvider>(
55    reader: &ObjectReader<P>,
56) -> Result<Vec<MemfdInfo>> {
57    // --- symbol resolution (graceful degradation) ---
58    let init_task_addr = match reader.symbols().symbol_address("init_task") {
59        Some(a) => a,
60        None => return Ok(vec![]),
61    };
62    let tasks_offset = match reader.symbols().field_offset("task_struct", "tasks") {
63        Some(o) => o,
64        None => return Ok(vec![]),
65    };
66
67    let head_vaddr = init_task_addr + tasks_offset;
68    let task_addrs = reader.walk_list(head_vaddr, "task_struct", "tasks")?;
69
70    let mut results: Vec<MemfdInfo> = Vec::new();
71
72    collect_memfd_for_task(reader, init_task_addr, &mut results);
73    for &task_addr in &task_addrs {
74        collect_memfd_for_task(reader, task_addr, &mut results);
75    }
76
77    results.sort_by_key(|r| r.pid);
78    Ok(results)
79}
80
81/// Collect all memfd VMAs for a single task.
82fn collect_memfd_for_task<P: PhysicalMemoryProvider>(
83    reader: &ObjectReader<P>,
84    task_addr: u64,
85    out: &mut Vec<MemfdInfo>,
86) {
87    let pid: u32 = match reader.read_field(task_addr, "task_struct", "pid") {
88        Ok(v) => v,
89        Err(_) => return,
90    };
91    let comm = reader
92        .read_field_string(task_addr, "task_struct", "comm", 16)
93        .unwrap_or_default();
94
95    // Walk VMAs via the shared abstraction (handles mm==0 kernel threads internally).
96    for_each_task_vma(reader, task_addr, &mut |e| {
97        if let Some(info) = try_read_memfd_vma(reader, pid, &comm, e.vma_addr) {
98            // Merge with existing entry for same (pid, memfd_name) if present.
99            let existing = out
100                .iter_mut()
101                .find(|entry| entry.pid == info.pid && entry.memfd_name == info.memfd_name);
102            if let Some(existing) = existing {
103                existing.size_bytes += info.size_bytes;
104                existing.is_executable |= info.is_executable;
105                existing.is_suspicious =
106                    classify_memfd(&existing.memfd_name, existing.is_executable);
107            } else {
108                out.push(info);
109            }
110        }
111    });
112}
113
114/// Attempt to read memfd information from a single VMA.
115///
116/// Returns `None` if the VMA is not a memfd mapping or fields cannot be read.
117fn try_read_memfd_vma<P: PhysicalMemoryProvider>(
118    reader: &ObjectReader<P>,
119    pid: u32,
120    comm: &str,
121    vma_addr: u64,
122) -> Option<MemfdInfo> {
123    // vm_file pointer — NULL means anonymous (not memfd-named).
124    let vm_file_ptr: u64 = reader
125        .read_field(vma_addr, "vm_area_struct", "vm_file")
126        .ok()?;
127    if vm_file_ptr == 0 {
128        return None;
129    }
130
131    // Read dentry name via file->f_path.dentry->d_name.name.
132    let dentry_name = read_file_dentry_name(reader, vm_file_ptr)?;
133
134    // memfd dentries are named "memfd:<user-name>".
135    let memfd_name = dentry_name.strip_prefix("memfd:")?;
136
137    let vm_start: u64 = reader
138        .read_field(vma_addr, "vm_area_struct", "vm_start")
139        .ok()?;
140    let vm_end: u64 = reader
141        .read_field(vma_addr, "vm_area_struct", "vm_end")
142        .ok()?;
143    let vm_flags: u64 = reader
144        .read_field(vma_addr, "vm_area_struct", "vm_flags")
145        .ok()?;
146
147    let size_bytes = vm_end.saturating_sub(vm_start);
148    let is_executable = (vm_flags & VM_EXEC) != 0;
149    let is_suspicious = classify_memfd(memfd_name, is_executable);
150
151    Some(MemfdInfo {
152        pid,
153        comm: comm.to_string(),
154        memfd_name: memfd_name.to_string(),
155        size_bytes,
156        is_executable,
157        is_suspicious,
158    })
159}
160
161/// Read the dentry name from a `struct file` pointer via `f_path.dentry->d_name`.
162fn read_file_dentry_name<P: PhysicalMemoryProvider>(
163    reader: &ObjectReader<P>,
164    file_ptr: u64,
165) -> Option<String> {
166    let f_path_offset = reader.symbols().field_offset("file", "f_path")?;
167    let dentry_in_path = reader.symbols().field_offset("path", "dentry")?;
168    let d_name_offset = reader.symbols().field_offset("dentry", "d_name")?;
169    let name_in_qstr = reader.symbols().field_offset("qstr", "name")?;
170
171    let dentry_addr = file_ptr + f_path_offset + dentry_in_path;
172    let dentry_raw = reader.read_bytes(dentry_addr, 8).ok()?;
173    let dentry_ptr = u64::from_le_bytes(dentry_raw.try_into().ok()?);
174    if dentry_ptr == 0 {
175        return None;
176    }
177
178    let name_addr = dentry_ptr + d_name_offset + name_in_qstr;
179    let name_raw = reader.read_bytes(name_addr, 8).ok()?;
180    let name_ptr = u64::from_le_bytes(name_raw.try_into().ok()?);
181    if name_ptr == 0 {
182        return None;
183    }
184
185    reader.read_string(name_ptr, 256).ok()
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use memf_core::object_reader::ObjectReader;
192    use memf_core::test_builders::{flags, PageTableBuilder, SyntheticPhysMem};
193    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
194    use memf_symbols::isf::IsfResolver;
195    use memf_symbols::test_builders::IsfBuilder;
196
197    // -----------------------------------------------------------------------
198    // classify_memfd unit tests
199    // -----------------------------------------------------------------------
200
201    #[test]
202    fn classify_memfd_executable_is_suspicious() {
203        assert!(
204            classify_memfd("harmless", true),
205            "an executable memfd mapping must always be suspicious"
206        );
207    }
208
209    #[test]
210    fn classify_memfd_shellcode_name_is_suspicious() {
211        assert!(
212            classify_memfd("shellcode", false),
213            "a memfd named 'shellcode' must be suspicious"
214        );
215    }
216
217    #[test]
218    fn classify_memfd_empty_name_is_suspicious() {
219        assert!(
220            classify_memfd("", false),
221            "an anonymous memfd with empty name must be suspicious (evasion)"
222        );
223    }
224
225    #[test]
226    fn classify_memfd_pulseaudio_benign() {
227        assert!(
228            !classify_memfd("pulseaudio-shm", false),
229            "a non-executable memfd named 'pulseaudio-shm' must not be suspicious"
230        );
231    }
232
233    #[test]
234    fn classify_memfd_payload_name_is_suspicious() {
235        assert!(
236            classify_memfd("payload", false),
237            "a memfd named 'payload' must be suspicious"
238        );
239    }
240
241    #[test]
242    fn classify_memfd_wayland_benign() {
243        assert!(
244            !classify_memfd("wayland-shm", false),
245            "a non-executable memfd named 'wayland-shm' must not be suspicious"
246        );
247    }
248
249    // -----------------------------------------------------------------------
250    // walk_memfd_create integration tests
251    // -----------------------------------------------------------------------
252
253    fn make_reader_no_init_task() -> ObjectReader<SyntheticPhysMem> {
254        let isf = IsfBuilder::new()
255            .add_struct("task_struct", 128)
256            .add_field("task_struct", "pid", 0, "int")
257            .add_field("task_struct", "tasks", 16, "list_head")
258            .add_struct("list_head", 16)
259            .add_field("list_head", "next", 0, "pointer")
260            .add_field("list_head", "prev", 8, "pointer")
261            .build_json();
262
263        let resolver = IsfResolver::from_value(&isf).unwrap();
264        let (cr3, mem) = PageTableBuilder::new().build();
265        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
266        ObjectReader::new(vas, Box::new(resolver))
267    }
268
269    fn make_reader_no_memfd() -> ObjectReader<SyntheticPhysMem> {
270        let vaddr: u64 = 0xFFFF_8000_0010_0000;
271        let paddr: u64 = 0x0080_0000;
272
273        let mut data = vec![0u8; 4096];
274        data[0..4].copy_from_slice(&1u32.to_le_bytes());
275        let tasks_next = vaddr + 16;
276        data[16..24].copy_from_slice(&tasks_next.to_le_bytes());
277        data[24..32].copy_from_slice(&tasks_next.to_le_bytes());
278        data[32..37].copy_from_slice(b"init\0");
279        // mm = 0 (kernel thread / no user mm)
280        data[48..56].copy_from_slice(&0u64.to_le_bytes());
281
282        let isf = IsfBuilder::new()
283            .add_struct("task_struct", 128)
284            .add_field("task_struct", "pid", 0, "int")
285            .add_field("task_struct", "tasks", 16, "list_head")
286            .add_field("task_struct", "comm", 32, "char")
287            .add_field("task_struct", "mm", 48, "pointer")
288            .add_struct("list_head", 16)
289            .add_field("list_head", "next", 0, "pointer")
290            .add_field("list_head", "prev", 8, "pointer")
291            .add_symbol("init_task", vaddr)
292            .build_json();
293
294        let resolver = IsfResolver::from_value(&isf).unwrap();
295        let (cr3, mem) = PageTableBuilder::new()
296            .map_4k(vaddr, paddr, flags::WRITABLE)
297            .write_phys(paddr, &data)
298            .build();
299        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
300        ObjectReader::new(vas, Box::new(resolver))
301    }
302
303    #[test]
304    fn walk_memfd_missing_init_task_returns_empty() {
305        let reader = make_reader_no_init_task();
306        let result = walk_memfd_create(&reader).expect("should not error");
307        assert!(
308            result.is_empty(),
309            "missing init_task symbol must yield empty result (graceful degradation)"
310        );
311    }
312
313    #[test]
314    fn walk_memfd_no_memfd_processes_returns_empty() {
315        let reader = make_reader_no_memfd();
316        let result = walk_memfd_create(&reader).expect("should not error");
317        assert!(
318            result.is_empty(),
319            "a kernel thread with mm==NULL must not produce any memfd results"
320        );
321    }
322
323    #[test]
324    fn walk_memfd_missing_tasks_offset_returns_empty() {
325        // init_task present but task_struct.tasks field missing → graceful degradation.
326        let isf = IsfBuilder::new()
327            .add_struct("task_struct", 128)
328            .add_field("task_struct", "pid", 0, "int")
329            // No "tasks" field
330            .add_symbol("init_task", 0xFFFF_8000_0000_0000)
331            .build_json();
332
333        let resolver = IsfResolver::from_value(&isf).unwrap();
334        let (cr3, mem) = PageTableBuilder::new().build();
335        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
336        let reader = ObjectReader::new(vas, Box::new(resolver));
337
338        let result = walk_memfd_create(&reader).expect("should not error");
339        assert!(
340            result.is_empty(),
341            "missing tasks offset must yield empty result"
342        );
343    }
344
345    // -----------------------------------------------------------------------
346    // Additional classify_memfd branch coverage
347    // -----------------------------------------------------------------------
348
349    #[test]
350    fn classify_memfd_shm_prefix_benign() {
351        assert!(
352            !classify_memfd("shm_region", false),
353            "shm prefix must be benign"
354        );
355    }
356
357    #[test]
358    fn classify_memfd_chrome_prefix_benign() {
359        assert!(
360            !classify_memfd("chrome_shared", false),
361            "chrome prefix must be benign"
362        );
363    }
364
365    #[test]
366    fn classify_memfd_firefox_prefix_benign() {
367        assert!(
368            !classify_memfd("firefox-ipc", false),
369            "firefox prefix must be benign"
370        );
371    }
372
373    #[test]
374    fn classify_memfd_v8_prefix_benign() {
375        assert!(
376            !classify_memfd("v8-heap", false),
377            "v8 prefix must be benign"
378        );
379    }
380
381    #[test]
382    fn classify_memfd_dbus_prefix_benign() {
383        assert!(
384            !classify_memfd("dbus-shm", false),
385            "dbus prefix must be benign"
386        );
387    }
388
389    #[test]
390    fn classify_memfd_stage_name_suspicious() {
391        assert!(
392            classify_memfd("stage2", false),
393            "stage substring must be suspicious"
394        );
395    }
396
397    #[test]
398    fn classify_memfd_loader_name_suspicious() {
399        assert!(
400            classify_memfd("loader", false),
401            "loader substring must be suspicious"
402        );
403    }
404
405    #[test]
406    fn classify_memfd_inject_name_suspicious() {
407        assert!(
408            classify_memfd("inject_hook", false),
409            "inject substring must be suspicious"
410        );
411    }
412
413    #[test]
414    fn classify_memfd_hack_name_suspicious() {
415        assert!(
416            classify_memfd("hack_tool", false),
417            "hack substring must be suspicious"
418        );
419    }
420
421    #[test]
422    fn classify_memfd_benign_non_prefix_non_suspicious_name() {
423        // Name does not match any prefix or suspicious substring, not executable
424        assert!(
425            !classify_memfd("my_normal_buffer", false),
426            "innocuous name must be benign"
427        );
428    }
429
430    #[test]
431    fn classify_memfd_case_insensitive_suspicious() {
432        // Suspicious substring matching should be case-insensitive
433        assert!(
434            classify_memfd("PAYLOAD_EXEC", false),
435            "case-insensitive suspicious match"
436        );
437    }
438
439    // -----------------------------------------------------------------------
440    // walk_memfd_create: symbol present + self-pointing list (walk body runs)
441    // -----------------------------------------------------------------------
442
443    #[test]
444    fn walk_memfd_symbol_present_empty_list() {
445        // init_task present with self-pointing tasks list and mm==NULL.
446        // The walk body runs but finds no memfd entries.
447        let sym_vaddr: u64 = 0xFFFF_8800_0020_0000;
448        let sym_paddr: u64 = 0x0030_0000;
449        let tasks_offset = 16u64;
450
451        let mut page = [0u8; 4096];
452        // pid = 1
453        page[0..4].copy_from_slice(&1u32.to_le_bytes());
454        // tasks.next = tasks.prev = &init_task.tasks  (self-pointing = empty list)
455        let list_self = sym_vaddr + tasks_offset;
456        page[tasks_offset as usize..tasks_offset as usize + 8]
457            .copy_from_slice(&list_self.to_le_bytes());
458        page[tasks_offset as usize + 8..tasks_offset as usize + 16]
459            .copy_from_slice(&list_self.to_le_bytes());
460        // comm = "init"
461        page[32..36].copy_from_slice(b"init");
462        // mm = 0 (kernel thread — no user mm, so collect_memfd_for_task returns early)
463        page[48..56].copy_from_slice(&0u64.to_le_bytes());
464
465        let isf = IsfBuilder::new()
466            .add_struct("task_struct", 128)
467            .add_field("task_struct", "pid", 0, "unsigned int")
468            .add_field("task_struct", "tasks", 16, "pointer")
469            .add_field("task_struct", "comm", 32, "char")
470            .add_field("task_struct", "mm", 48, "pointer")
471            .add_symbol("init_task", sym_vaddr)
472            .build_json();
473
474        let resolver = IsfResolver::from_value(&isf).unwrap();
475        let (cr3, mem) = PageTableBuilder::new()
476            .map_4k(sym_vaddr, sym_paddr, flags::WRITABLE)
477            .write_phys(sym_paddr, &page)
478            .build();
479        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
480        let reader = ObjectReader::new(vas, Box::new(resolver));
481
482        let result = walk_memfd_create(&reader).unwrap_or_default();
483        assert!(
484            result.is_empty(),
485            "no memfd mappings expected for a kernel thread"
486        );
487    }
488
489    #[test]
490    fn memfd_info_serializes() {
491        let info = MemfdInfo {
492            pid: 999,
493            comm: "evil".to_string(),
494            memfd_name: "payload".to_string(),
495            size_bytes: 4096,
496            is_executable: true,
497            is_suspicious: true,
498        };
499        let json = serde_json::to_string(&info).unwrap();
500        assert!(json.contains("\"pid\":999"));
501        assert!(json.contains("\"is_suspicious\":true"));
502        assert!(json.contains("\"is_executable\":true"));
503    }
504
505    // --- collect_memfd_for_task: mm != 0 but mm_struct.mmap read fails → graceful ---
506    // Exercises the `read_field(mm_ptr, "mm_struct", "mmap")` Err → return branch.
507    #[test]
508    fn walk_memfd_mm_nonzero_mmap_unreadable_returns_empty() {
509        let sym_vaddr: u64 = 0xFFFF_8800_0060_0000;
510        let sym_paddr: u64 = 0x0060_0000;
511        let tasks_offset: u64 = 16;
512        let mm_offset: u64 = 48;
513
514        // mm pointer is set to an unmapped address → mm_struct.mmap read will fail.
515        let mm_vaddr: u64 = 0xFFFF_DEAD_BEEF_0000; // unmapped
516
517        let mut page = [0u8; 4096];
518        // pid = 1
519        page[0..4].copy_from_slice(&1u32.to_le_bytes());
520        // tasks self-pointing
521        let self_ptr = sym_vaddr + tasks_offset;
522        page[tasks_offset as usize..tasks_offset as usize + 8]
523            .copy_from_slice(&self_ptr.to_le_bytes());
524        // comm = "proc\0"
525        page[32..36].copy_from_slice(b"proc");
526        // mm = mm_vaddr (non-zero but unmapped → mmap read fails)
527        page[mm_offset as usize..mm_offset as usize + 8].copy_from_slice(&mm_vaddr.to_le_bytes());
528
529        let isf = IsfBuilder::new()
530            .add_struct("list_head", 16)
531            .add_field("list_head", "next", 0, "pointer")
532            .add_struct("task_struct", 128)
533            .add_field("task_struct", "pid", 0, "unsigned int")
534            .add_field("task_struct", "tasks", tasks_offset, "pointer")
535            .add_field("task_struct", "comm", 32, "char")
536            .add_field("task_struct", "mm", mm_offset, "pointer")
537            .add_struct("mm_struct", 0x200)
538            .add_field("mm_struct", "mmap", 0x00, "pointer")
539            .add_symbol("init_task", sym_vaddr)
540            .build_json();
541
542        let resolver = IsfResolver::from_value(&isf).unwrap();
543        let (cr3, mem) = PageTableBuilder::new()
544            .map_4k(sym_vaddr, sym_paddr, flags::WRITABLE)
545            .write_phys(sym_paddr, &page)
546            .build();
547        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
548        let reader = ObjectReader::new(vas, Box::new(resolver));
549
550        let result = walk_memfd_create(&reader).expect("should not error");
551        assert!(
552            result.is_empty(),
553            "unreadable mm_struct → mmap unreadable → no memfd results"
554        );
555    }
556
557    // --- collect_memfd_for_task: mm != 0, mmap readable, mmap_ptr == 0 → no VMAs ---
558    // Exercises the VMA while loop: vma_addr = 0 → loop body never entered.
559    #[test]
560    fn walk_memfd_mm_nonzero_mmap_zero_returns_empty() {
561        let sym_vaddr: u64 = 0xFFFF_8800_0061_0000;
562        let sym_paddr: u64 = 0x0061_0000;
563        let tasks_offset: u64 = 16;
564        let mm_offset: u64 = 48;
565
566        let mm_vaddr: u64 = 0xFFFF_8800_0062_0000;
567        let mm_paddr: u64 = 0x0062_0000;
568
569        let mut task_page = [0u8; 4096];
570        page_write_u32(&mut task_page, 0, 2u32); // pid = 2
571        let self_ptr = sym_vaddr + tasks_offset;
572        task_page[tasks_offset as usize..tasks_offset as usize + 8]
573            .copy_from_slice(&self_ptr.to_le_bytes());
574        task_page[32..36].copy_from_slice(b"proc");
575        task_page[mm_offset as usize..mm_offset as usize + 8]
576            .copy_from_slice(&mm_vaddr.to_le_bytes());
577
578        // mm_struct page: mmap at offset 0 = 0 → vma_addr = 0 → loop never runs
579        let mm_page = [0u8; 4096];
580
581        let isf = IsfBuilder::new()
582            .add_struct("list_head", 16)
583            .add_field("list_head", "next", 0, "pointer")
584            .add_struct("task_struct", 128)
585            .add_field("task_struct", "pid", 0, "unsigned int")
586            .add_field("task_struct", "tasks", tasks_offset, "pointer")
587            .add_field("task_struct", "comm", 32, "char")
588            .add_field("task_struct", "mm", mm_offset, "pointer")
589            .add_struct("mm_struct", 0x200)
590            .add_field("mm_struct", "mmap", 0x00, "pointer")
591            .add_symbol("init_task", sym_vaddr)
592            .build_json();
593
594        let resolver = IsfResolver::from_value(&isf).unwrap();
595        let (cr3, mem) = PageTableBuilder::new()
596            .map_4k(sym_vaddr, sym_paddr, flags::WRITABLE)
597            .write_phys(sym_paddr, &task_page)
598            .map_4k(mm_vaddr, mm_paddr, flags::WRITABLE)
599            .write_phys(mm_paddr, &mm_page)
600            .build();
601        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
602        let reader = ObjectReader::new(vas, Box::new(resolver));
603
604        let result = walk_memfd_create(&reader).expect("should not error");
605        assert!(
606            result.is_empty(),
607            "mmap_ptr == 0 → no VMAs → no memfd results"
608        );
609    }
610
611    // --- collect_memfd_for_task: VMA chain with vm_file = 0 → skipped ---
612    // Exercises try_read_memfd_vma: vm_file == 0 → returns None → no entry.
613    // Then vm_next read fails → VMA loop breaks.
614    #[test]
615    fn walk_memfd_vma_vm_file_null_skipped() {
616        let sym_vaddr: u64 = 0xFFFF_8800_0063_0000;
617        let sym_paddr: u64 = 0x0063_0000;
618        let tasks_offset: u64 = 16;
619        let mm_offset: u64 = 48;
620
621        let mm_vaddr: u64 = 0xFFFF_8800_0064_0000;
622        let mm_paddr: u64 = 0x0064_0000;
623
624        let vma_vaddr: u64 = 0xFFFF_8800_0065_0000;
625        let vma_paddr: u64 = 0x0065_0000;
626
627        let mut task_page = [0u8; 4096];
628        page_write_u32(&mut task_page, 0, 3u32);
629        let self_ptr = sym_vaddr + tasks_offset;
630        task_page[tasks_offset as usize..tasks_offset as usize + 8]
631            .copy_from_slice(&self_ptr.to_le_bytes());
632        task_page[32..36].copy_from_slice(b"proc");
633        task_page[mm_offset as usize..mm_offset as usize + 8]
634            .copy_from_slice(&mm_vaddr.to_le_bytes());
635
636        // mm_struct: mmap = vma_vaddr
637        let mut mm_page = [0u8; 4096];
638        mm_page[0..8].copy_from_slice(&vma_vaddr.to_le_bytes());
639
640        // vma page: vm_file at 0x08 = 0 (null → try_read_memfd_vma returns None)
641        //           vm_next at 0x00 = 0 (loop ends)
642        let vma_page = [0u8; 4096]; // all zeros
643
644        let isf = IsfBuilder::new()
645            .add_struct("list_head", 16)
646            .add_field("list_head", "next", 0, "pointer")
647            .add_struct("task_struct", 128)
648            .add_field("task_struct", "pid", 0, "unsigned int")
649            .add_field("task_struct", "tasks", tasks_offset, "pointer")
650            .add_field("task_struct", "comm", 32, "char")
651            .add_field("task_struct", "mm", mm_offset, "pointer")
652            .add_struct("mm_struct", 0x200)
653            .add_field("mm_struct", "mmap", 0x00, "pointer")
654            .add_struct("vm_area_struct", 0x100)
655            .add_field("vm_area_struct", "vm_next", 0x00, "pointer")
656            .add_field("vm_area_struct", "vm_file", 0x08, "pointer")
657            .add_field("vm_area_struct", "vm_start", 0x10, "unsigned long")
658            .add_field("vm_area_struct", "vm_end", 0x18, "unsigned long")
659            .add_field("vm_area_struct", "vm_flags", 0x20, "unsigned long")
660            .add_symbol("init_task", sym_vaddr)
661            .build_json();
662
663        let resolver = IsfResolver::from_value(&isf).unwrap();
664        let (cr3, mem) = PageTableBuilder::new()
665            .map_4k(sym_vaddr, sym_paddr, flags::WRITABLE)
666            .write_phys(sym_paddr, &task_page)
667            .map_4k(mm_vaddr, mm_paddr, flags::WRITABLE)
668            .write_phys(mm_paddr, &mm_page)
669            .map_4k(vma_vaddr, vma_paddr, flags::WRITABLE)
670            .write_phys(vma_paddr, &vma_page)
671            .build();
672        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
673        let reader = ObjectReader::new(vas, Box::new(resolver));
674
675        let result = walk_memfd_create(&reader).expect("should not error");
676        assert!(
677            result.is_empty(),
678            "vm_file = 0 → try_read_memfd_vma returns None → no entries"
679        );
680    }
681
682    // Helper to write a u32 into a page slice.
683    fn page_write_u32(page: &mut [u8], offset: usize, val: u32) {
684        page[offset..offset + 4].copy_from_slice(&val.to_le_bytes());
685    }
686
687    // Helper to write a u64 into a page slice.
688    fn page_write_u64(page: &mut [u8], offset: usize, val: u64) {
689        page[offset..offset + 8].copy_from_slice(&val.to_le_bytes());
690    }
691
692    // --- collect_memfd_for_task: pid read fails → return early (line 131) ---
693    #[test]
694    fn walk_memfd_second_task_pid_read_fails_skipped() {
695        // init_task list points to a second task that is on an unmapped page.
696        // collect_memfd_for_task for that task → read_field "pid" fails → return.
697        let init_vaddr: u64 = 0xFFFF_8800_0080_0000;
698        let init_paddr: u64 = 0x0080_0000;
699        let tasks_offset: u64 = 16;
700
701        // task2 lives on an unmapped address
702        let task2_vaddr: u64 = 0xFFFF_DEAD_0000_0000; // unmapped
703
704        let mut init_page = [0u8; 4096];
705        page_write_u32(&mut init_page, 0, 1); // pid=1
706                                              // tasks.next → task2 (unmapped), tasks.prev → init itself
707        page_write_u64(
708            &mut init_page,
709            tasks_offset as usize,
710            task2_vaddr + tasks_offset,
711        );
712        page_write_u64(
713            &mut init_page,
714            tasks_offset as usize + 8,
715            task2_vaddr + tasks_offset,
716        );
717        init_page[32..36].copy_from_slice(b"init");
718        page_write_u64(&mut init_page, 48, 0); // mm = 0
719
720        let isf = IsfBuilder::new()
721            .add_struct("list_head", 16)
722            .add_field("list_head", "next", 0u64, "pointer")
723            .add_field("list_head", "prev", 8u64, "pointer")
724            .add_struct("task_struct", 128)
725            .add_field("task_struct", "pid", 0u64, "unsigned int")
726            .add_field("task_struct", "tasks", tasks_offset, "list_head")
727            .add_field("task_struct", "comm", 32u64, "char")
728            .add_field("task_struct", "mm", 48u64, "pointer")
729            .add_symbol("init_task", init_vaddr)
730            .build_json();
731
732        let resolver = IsfResolver::from_value(&isf).unwrap();
733        let (cr3, mem) = PageTableBuilder::new()
734            .map_4k(init_vaddr, init_paddr, flags::WRITABLE)
735            .write_phys(init_paddr, &init_page)
736            .build();
737        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
738        let reader = ObjectReader::new(vas, Box::new(resolver));
739
740        // Should succeed (walk_list might error on unmapped task2, gracefully handled)
741        let result = walk_memfd_create(&reader);
742        // Either Ok(empty) or Err — both are acceptable since task2 is unmapped.
743        // What matters is no panic and no memfd entries returned.
744        let entries = result.unwrap_or_default();
745        assert!(entries.is_empty(), "unmapped task2 → no memfd entries");
746    }
747
748    // regression guard: file-backed VMA with memfd: dentry name detected and classified
749    // --- full path: memfd VMA found → MemfdInfo created (lines 154-165, 199-220, 233-244) ---
750    #[test]
751    fn walk_memfd_full_path_memfd_vma_detected() {
752        // Build a synthetic memory layout where:
753        //   init_task has mm → mm_struct → VMA → file → dentry → name "memfd:payload"
754        // This exercises the deepest branches: try_read_memfd_vma success, MemfdInfo push.
755
756        let init_vaddr: u64 = 0xFFFF_8800_00A0_0000;
757        let init_paddr: u64 = 0x00A0_0000;
758        let mm_vaddr: u64 = 0xFFFF_8800_00A1_0000;
759        let mm_paddr: u64 = 0x00A1_0000;
760        let vma_vaddr: u64 = 0xFFFF_8800_00A2_0000;
761        let vma_paddr: u64 = 0x00A2_0000;
762        let file_vaddr: u64 = 0xFFFF_8800_00A3_0000;
763        let file_paddr: u64 = 0x00A3_0000;
764        let dentry_vaddr: u64 = 0xFFFF_8800_00A4_0000;
765        let dentry_paddr: u64 = 0x00A4_0000;
766        let name_vaddr: u64 = 0xFFFF_8800_00A5_0000;
767        let name_paddr: u64 = 0x00A5_0000;
768
769        let tasks_offset: u64 = 16;
770        let mm_offset: u64 = 48;
771        let mmap_offset: u64 = 0; // mmap at mm_struct offset 0
772
773        // file struct layout
774        let f_path_off: u64 = 0x10;
775        let dentry_in_path: u64 = 0x00; // dentry* at path+0
776        let d_name_off: u64 = 0x08; // qstr at dentry+8
777        let name_in_qstr: u64 = 0x00; // name* at qstr+0
778
779        // init_task page
780        let mut init_page = [0u8; 4096];
781        page_write_u32(&mut init_page, 0, 7u32); // pid=7
782        let tasks_self = init_vaddr + tasks_offset;
783        page_write_u64(&mut init_page, tasks_offset as usize, tasks_self);
784        page_write_u64(&mut init_page, tasks_offset as usize + 8, tasks_self);
785        init_page[32..37].copy_from_slice(b"evil\0");
786        page_write_u64(&mut init_page, mm_offset as usize, mm_vaddr); // mm = mm_vaddr
787
788        // mm_struct page: mmap = vma_vaddr
789        let mut mm_page = [0u8; 4096];
790        page_write_u64(&mut mm_page, mmap_offset as usize, vma_vaddr);
791
792        // vm_area_struct page:
793        //   vm_next   at 0x00 = 0 (single VMA)
794        //   vm_file   at 0x08 = file_vaddr
795        //   vm_start  at 0x10 = 0x1000
796        //   vm_end    at 0x18 = 0x2000
797        //   vm_flags  at 0x20 = VM_EXEC (0x4) → executable → suspicious
798        let mut vma_page = [0u8; 4096];
799        page_write_u64(&mut vma_page, 0x00, 0u64); // vm_next = NULL
800        page_write_u64(&mut vma_page, 0x08, file_vaddr); // vm_file
801        page_write_u64(&mut vma_page, 0x10, 0x1000u64); // vm_start
802        page_write_u64(&mut vma_page, 0x18, 0x2000u64); // vm_end
803        page_write_u64(&mut vma_page, 0x20, 4u64); // vm_flags = VM_EXEC
804
805        // file page: dentry ptr at f_path_off + dentry_in_path = 0x10
806        let mut file_page = [0u8; 4096];
807        page_write_u64(&mut file_page, 0x10, dentry_vaddr);
808
809        // dentry page: name ptr at d_name_off + name_in_qstr = 0x08
810        let mut dentry_page = [0u8; 4096];
811        page_write_u64(&mut dentry_page, 0x08, name_vaddr);
812
813        // name string page: "memfd:payload\0"
814        let mut name_page = [0u8; 4096];
815        name_page[..14].copy_from_slice(b"memfd:payload\0");
816
817        let isf = IsfBuilder::new()
818            .add_struct("list_head", 16)
819            .add_field("list_head", "next", 0u64, "pointer")
820            .add_field("list_head", "prev", 8u64, "pointer")
821            .add_struct("task_struct", 128)
822            .add_field("task_struct", "pid", 0u64, "unsigned int")
823            .add_field("task_struct", "tasks", tasks_offset, "list_head")
824            .add_field("task_struct", "comm", 32u64, "char")
825            .add_field("task_struct", "mm", mm_offset, "pointer")
826            .add_struct("mm_struct", 0x100)
827            .add_field("mm_struct", "mmap", mmap_offset, "pointer")
828            .add_struct("vm_area_struct", 0x100)
829            .add_field("vm_area_struct", "vm_next", 0x00u64, "pointer")
830            .add_field("vm_area_struct", "vm_file", 0x08u64, "pointer")
831            .add_field("vm_area_struct", "vm_start", 0x10u64, "unsigned long")
832            .add_field("vm_area_struct", "vm_end", 0x18u64, "unsigned long")
833            .add_field("vm_area_struct", "vm_flags", 0x20u64, "unsigned long")
834            .add_struct("file", 0x100)
835            .add_field("file", "f_path", f_path_off, "pointer")
836            .add_struct("path", 0x20)
837            .add_field("path", "dentry", dentry_in_path, "pointer")
838            .add_struct("dentry", 0x100)
839            .add_field("dentry", "d_name", d_name_off, "pointer")
840            .add_struct("qstr", 0x20)
841            .add_field("qstr", "name", name_in_qstr, "pointer")
842            .add_symbol("init_task", init_vaddr)
843            .build_json();
844
845        let resolver = IsfResolver::from_value(&isf).unwrap();
846        let (cr3, mem) = PageTableBuilder::new()
847            .map_4k(init_vaddr, init_paddr, flags::WRITABLE)
848            .write_phys(init_paddr, &init_page)
849            .map_4k(mm_vaddr, mm_paddr, flags::WRITABLE)
850            .write_phys(mm_paddr, &mm_page)
851            .map_4k(vma_vaddr, vma_paddr, flags::WRITABLE)
852            .write_phys(vma_paddr, &vma_page)
853            .map_4k(file_vaddr, file_paddr, flags::WRITABLE)
854            .write_phys(file_paddr, &file_page)
855            .map_4k(dentry_vaddr, dentry_paddr, flags::WRITABLE)
856            .write_phys(dentry_paddr, &dentry_page)
857            .map_4k(name_vaddr, name_paddr, flags::WRITABLE)
858            .write_phys(name_paddr, &name_page)
859            .build();
860        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
861        let reader = ObjectReader::new(vas, Box::new(resolver));
862
863        let result = walk_memfd_create(&reader).expect("should not error");
864        assert_eq!(result.len(), 1, "expected one memfd entry");
865        assert_eq!(result[0].pid, 7);
866        assert_eq!(result[0].comm, "evil");
867        assert_eq!(result[0].memfd_name, "payload");
868        assert_eq!(result[0].size_bytes, 0x1000);
869        assert!(result[0].is_executable, "VM_EXEC flag set → executable");
870        assert!(result[0].is_suspicious, "executable memfd → suspicious");
871    }
872
873    // --- read_file_dentry_name: missing ISF fields → returns None → no entry (lines 237-244) ---
874    #[test]
875    fn walk_memfd_dentry_missing_isf_fields_no_entry() {
876        // vm_file != 0 but ISF has no "file.f_path" field → read_file_dentry_name returns None.
877        let init_vaddr: u64 = 0xFFFF_8800_00B0_0000;
878        let init_paddr: u64 = 0x00B0_0000;
879        let mm_vaddr: u64 = 0xFFFF_8800_00B1_0000;
880        let mm_paddr: u64 = 0x00B1_0000;
881        let vma_vaddr: u64 = 0xFFFF_8800_00B2_0000;
882        let vma_paddr: u64 = 0x00B2_0000;
883        let file_vaddr: u64 = 0xFFFF_8800_00B3_0000;
884
885        let tasks_offset: u64 = 16;
886        let mm_offset: u64 = 48;
887
888        let mut init_page = [0u8; 4096];
889        page_write_u32(&mut init_page, 0, 8u32);
890        let tasks_self = init_vaddr + tasks_offset;
891        page_write_u64(&mut init_page, tasks_offset as usize, tasks_self);
892        page_write_u64(&mut init_page, tasks_offset as usize + 8, tasks_self);
893        init_page[32..36].copy_from_slice(b"proc");
894        page_write_u64(&mut init_page, mm_offset as usize, mm_vaddr);
895
896        let mut mm_page = [0u8; 4096];
897        page_write_u64(&mut mm_page, 0, vma_vaddr); // mmap = vma_vaddr
898
899        let mut vma_page = [0u8; 4096];
900        page_write_u64(&mut vma_page, 0x00, 0u64); // vm_next = NULL
901        page_write_u64(&mut vma_page, 0x08, file_vaddr); // vm_file != 0
902        page_write_u64(&mut vma_page, 0x10, 0x1000u64); // vm_start
903        page_write_u64(&mut vma_page, 0x18, 0x2000u64); // vm_end
904        page_write_u64(&mut vma_page, 0x20, 0u64); // vm_flags
905
906        let isf = IsfBuilder::new()
907            .add_struct("list_head", 16)
908            .add_field("list_head", "next", 0u64, "pointer")
909            .add_field("list_head", "prev", 8u64, "pointer")
910            .add_struct("task_struct", 128)
911            .add_field("task_struct", "pid", 0u64, "unsigned int")
912            .add_field("task_struct", "tasks", tasks_offset, "list_head")
913            .add_field("task_struct", "comm", 32u64, "char")
914            .add_field("task_struct", "mm", mm_offset, "pointer")
915            .add_struct("mm_struct", 0x100)
916            .add_field("mm_struct", "mmap", 0u64, "pointer")
917            .add_struct("vm_area_struct", 0x100)
918            .add_field("vm_area_struct", "vm_next", 0x00u64, "pointer")
919            .add_field("vm_area_struct", "vm_file", 0x08u64, "pointer")
920            .add_field("vm_area_struct", "vm_start", 0x10u64, "unsigned long")
921            .add_field("vm_area_struct", "vm_end", 0x18u64, "unsigned long")
922            .add_field("vm_area_struct", "vm_flags", 0x20u64, "unsigned long")
923            // No "file", "path", "dentry", "qstr" structs → read_file_dentry_name returns None
924            .add_symbol("init_task", init_vaddr)
925            .build_json();
926
927        let resolver = IsfResolver::from_value(&isf).unwrap();
928        let (cr3, mem) = PageTableBuilder::new()
929            .map_4k(init_vaddr, init_paddr, flags::WRITABLE)
930            .write_phys(init_paddr, &init_page)
931            .map_4k(mm_vaddr, mm_paddr, flags::WRITABLE)
932            .write_phys(mm_paddr, &mm_page)
933            .map_4k(vma_vaddr, vma_paddr, flags::WRITABLE)
934            .write_phys(vma_paddr, &vma_page)
935            .build();
936        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
937        let reader = ObjectReader::new(vas, Box::new(resolver));
938
939        let result = walk_memfd_create(&reader).expect("should not error");
940        assert!(
941            result.is_empty(),
942            "missing dentry ISF fields → read_file_dentry_name None → no entries"
943        );
944    }
945
946    // --- merge logic: two VMAs for the same memfd name → merged into one entry (line 159-162) ---
947    #[test]
948    fn walk_memfd_two_vmas_same_name_merged() {
949        // Two VMAs for "memfd:payload" → merged into one MemfdInfo entry.
950        let init_vaddr: u64 = 0xFFFF_8800_00C0_0000;
951        let init_paddr: u64 = 0x00C0_0000;
952        let mm_vaddr: u64 = 0xFFFF_8800_00C1_0000;
953        let mm_paddr: u64 = 0x00C1_0000;
954        let vma1_vaddr: u64 = 0xFFFF_8800_00C2_0000;
955        let vma1_paddr: u64 = 0x00C2_0000;
956        let vma2_vaddr: u64 = 0xFFFF_8800_00C3_0000;
957        let vma2_paddr: u64 = 0x00C3_0000;
958        let file_vaddr: u64 = 0xFFFF_8800_00C4_0000;
959        let file_paddr: u64 = 0x00C4_0000;
960        let dentry_vaddr: u64 = 0xFFFF_8800_00C5_0000;
961        let dentry_paddr: u64 = 0x00C5_0000;
962        let name_vaddr: u64 = 0xFFFF_8800_00C6_0000;
963        let name_paddr: u64 = 0x00C6_0000;
964
965        let tasks_offset: u64 = 16;
966        let mm_offset: u64 = 48;
967        let f_path_off: u64 = 0x10;
968        let d_name_off: u64 = 0x08;
969
970        let mut init_page = [0u8; 4096];
971        page_write_u32(&mut init_page, 0, 9u32);
972        let tasks_self = init_vaddr + tasks_offset;
973        page_write_u64(&mut init_page, tasks_offset as usize, tasks_self);
974        page_write_u64(&mut init_page, tasks_offset as usize + 8, tasks_self);
975        init_page[32..40].copy_from_slice(b"malware\0");
976        page_write_u64(&mut init_page, mm_offset as usize, mm_vaddr);
977
978        // mm: mmap = vma1
979        let mut mm_page = [0u8; 4096];
980        page_write_u64(&mut mm_page, 0, vma1_vaddr);
981
982        // vma1: vm_next = vma2, vm_file = file_vaddr, non-executable
983        let mut vma1_page = [0u8; 4096];
984        page_write_u64(&mut vma1_page, 0x00, vma2_vaddr); // vm_next
985        page_write_u64(&mut vma1_page, 0x08, file_vaddr); // vm_file
986        page_write_u64(&mut vma1_page, 0x10, 0x1000u64); // vm_start
987        page_write_u64(&mut vma1_page, 0x18, 0x2000u64); // vm_end
988        page_write_u64(&mut vma1_page, 0x20, 0u64); // vm_flags (non-exec)
989
990        // vma2: vm_next = NULL, same vm_file, VM_EXEC set
991        let mut vma2_page = [0u8; 4096];
992        page_write_u64(&mut vma2_page, 0x00, 0u64); // vm_next = NULL
993        page_write_u64(&mut vma2_page, 0x08, file_vaddr); // vm_file (same)
994        page_write_u64(&mut vma2_page, 0x10, 0x2000u64); // vm_start
995        page_write_u64(&mut vma2_page, 0x18, 0x3000u64); // vm_end
996        page_write_u64(&mut vma2_page, 0x20, 4u64); // vm_flags = VM_EXEC
997
998        // file: dentry ptr at 0x10
999        let mut file_page = [0u8; 4096];
1000        page_write_u64(&mut file_page, 0x10, dentry_vaddr);
1001
1002        // dentry: name ptr at 0x08
1003        let mut dentry_page = [0u8; 4096];
1004        page_write_u64(&mut dentry_page, 0x08, name_vaddr);
1005
1006        // name: "memfd:payload\0"
1007        let mut name_page = [0u8; 4096];
1008        name_page[..14].copy_from_slice(b"memfd:payload\0");
1009
1010        let isf = IsfBuilder::new()
1011            .add_struct("list_head", 16)
1012            .add_field("list_head", "next", 0u64, "pointer")
1013            .add_field("list_head", "prev", 8u64, "pointer")
1014            .add_struct("task_struct", 128)
1015            .add_field("task_struct", "pid", 0u64, "unsigned int")
1016            .add_field("task_struct", "tasks", tasks_offset, "list_head")
1017            .add_field("task_struct", "comm", 32u64, "char")
1018            .add_field("task_struct", "mm", mm_offset, "pointer")
1019            .add_struct("mm_struct", 0x100)
1020            .add_field("mm_struct", "mmap", 0u64, "pointer")
1021            .add_struct("vm_area_struct", 0x100)
1022            .add_field("vm_area_struct", "vm_next", 0x00u64, "pointer")
1023            .add_field("vm_area_struct", "vm_file", 0x08u64, "pointer")
1024            .add_field("vm_area_struct", "vm_start", 0x10u64, "unsigned long")
1025            .add_field("vm_area_struct", "vm_end", 0x18u64, "unsigned long")
1026            .add_field("vm_area_struct", "vm_flags", 0x20u64, "unsigned long")
1027            .add_struct("file", 0x100)
1028            .add_field("file", "f_path", f_path_off, "pointer")
1029            .add_struct("path", 0x20)
1030            .add_field("path", "dentry", 0u64, "pointer")
1031            .add_struct("dentry", 0x100)
1032            .add_field("dentry", "d_name", d_name_off, "pointer")
1033            .add_struct("qstr", 0x20)
1034            .add_field("qstr", "name", 0u64, "pointer")
1035            .add_symbol("init_task", init_vaddr)
1036            .build_json();
1037
1038        let resolver = IsfResolver::from_value(&isf).unwrap();
1039        let (cr3, mem) = PageTableBuilder::new()
1040            .map_4k(init_vaddr, init_paddr, flags::WRITABLE)
1041            .write_phys(init_paddr, &init_page)
1042            .map_4k(mm_vaddr, mm_paddr, flags::WRITABLE)
1043            .write_phys(mm_paddr, &mm_page)
1044            .map_4k(vma1_vaddr, vma1_paddr, flags::WRITABLE)
1045            .write_phys(vma1_paddr, &vma1_page)
1046            .map_4k(vma2_vaddr, vma2_paddr, flags::WRITABLE)
1047            .write_phys(vma2_paddr, &vma2_page)
1048            .map_4k(file_vaddr, file_paddr, flags::WRITABLE)
1049            .write_phys(file_paddr, &file_page)
1050            .map_4k(dentry_vaddr, dentry_paddr, flags::WRITABLE)
1051            .write_phys(dentry_paddr, &dentry_page)
1052            .map_4k(name_vaddr, name_paddr, flags::WRITABLE)
1053            .write_phys(name_paddr, &name_page)
1054            .build();
1055        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
1056        let reader = ObjectReader::new(vas, Box::new(resolver));
1057
1058        let result = walk_memfd_create(&reader).expect("should not error");
1059        assert_eq!(
1060            result.len(),
1061            1,
1062            "two VMAs for same memfd → merged to 1 entry"
1063        );
1064        assert_eq!(result[0].memfd_name, "payload");
1065        // size_bytes = 0x1000 (vma1) + 0x1000 (vma2) = 0x2000
1066        assert_eq!(result[0].size_bytes, 0x2000);
1067        // vma2 has VM_EXEC → after merge is_executable = true
1068        assert!(
1069            result[0].is_executable,
1070            "merged entry must be executable after vma2"
1071        );
1072        assert!(
1073            result[0].is_suspicious,
1074            "executable memfd:payload must be suspicious"
1075        );
1076    }
1077
1078    // --- try_read_memfd_vma: dentry_ptr == 0 → None (line 237) ---
1079    #[test]
1080    fn walk_memfd_dentry_ptr_null_returns_none() {
1081        // vm_file != 0, dentry chain readable, but dentry pointer == 0 → None.
1082        let init_vaddr: u64 = 0xFFFF_8800_00D0_0000;
1083        let init_paddr: u64 = 0x00D0_0000;
1084        let mm_vaddr: u64 = 0xFFFF_8800_00D1_0000;
1085        let mm_paddr: u64 = 0x00D1_0000;
1086        let vma_vaddr: u64 = 0xFFFF_8800_00D2_0000;
1087        let vma_paddr: u64 = 0x00D2_0000;
1088        let file_vaddr: u64 = 0xFFFF_8800_00D3_0000;
1089        let file_paddr: u64 = 0x00D3_0000;
1090
1091        let tasks_offset: u64 = 16;
1092        let mm_offset: u64 = 48;
1093        let f_path_off: u64 = 0x10;
1094        let d_name_off: u64 = 0x08;
1095
1096        let mut init_page = [0u8; 4096];
1097        page_write_u32(&mut init_page, 0, 11u32);
1098        let tasks_self = init_vaddr + tasks_offset;
1099        page_write_u64(&mut init_page, tasks_offset as usize, tasks_self);
1100        page_write_u64(&mut init_page, tasks_offset as usize + 8, tasks_self);
1101        init_page[32..36].copy_from_slice(b"proc");
1102        page_write_u64(&mut init_page, mm_offset as usize, mm_vaddr);
1103
1104        let mut mm_page = [0u8; 4096];
1105        page_write_u64(&mut mm_page, 0, vma_vaddr);
1106
1107        let mut vma_page = [0u8; 4096];
1108        page_write_u64(&mut vma_page, 0x00, 0u64); // vm_next = NULL
1109        page_write_u64(&mut vma_page, 0x08, file_vaddr); // vm_file != 0
1110        page_write_u64(&mut vma_page, 0x10, 0x1000u64);
1111        page_write_u64(&mut vma_page, 0x18, 0x2000u64);
1112        page_write_u64(&mut vma_page, 0x20, 0u64);
1113
1114        // file: dentry ptr at f_path_off = 0x10 → value = 0 (NULL dentry)
1115        let mut file_page = [0u8; 4096];
1116        page_write_u64(&mut file_page, 0x10, 0u64); // dentry_ptr = NULL
1117
1118        let isf = IsfBuilder::new()
1119            .add_struct("list_head", 16)
1120            .add_field("list_head", "next", 0u64, "pointer")
1121            .add_field("list_head", "prev", 8u64, "pointer")
1122            .add_struct("task_struct", 128)
1123            .add_field("task_struct", "pid", 0u64, "unsigned int")
1124            .add_field("task_struct", "tasks", tasks_offset, "list_head")
1125            .add_field("task_struct", "comm", 32u64, "char")
1126            .add_field("task_struct", "mm", mm_offset, "pointer")
1127            .add_struct("mm_struct", 0x100)
1128            .add_field("mm_struct", "mmap", 0u64, "pointer")
1129            .add_struct("vm_area_struct", 0x100)
1130            .add_field("vm_area_struct", "vm_next", 0x00u64, "pointer")
1131            .add_field("vm_area_struct", "vm_file", 0x08u64, "pointer")
1132            .add_field("vm_area_struct", "vm_start", 0x10u64, "unsigned long")
1133            .add_field("vm_area_struct", "vm_end", 0x18u64, "unsigned long")
1134            .add_field("vm_area_struct", "vm_flags", 0x20u64, "unsigned long")
1135            .add_struct("file", 0x100)
1136            .add_field("file", "f_path", f_path_off, "pointer")
1137            .add_struct("path", 0x20)
1138            .add_field("path", "dentry", 0u64, "pointer")
1139            .add_struct("dentry", 0x100)
1140            .add_field("dentry", "d_name", d_name_off, "pointer")
1141            .add_struct("qstr", 0x20)
1142            .add_field("qstr", "name", 0u64, "pointer")
1143            .add_symbol("init_task", init_vaddr)
1144            .build_json();
1145
1146        let resolver = IsfResolver::from_value(&isf).unwrap();
1147        let (cr3, mem) = PageTableBuilder::new()
1148            .map_4k(init_vaddr, init_paddr, flags::WRITABLE)
1149            .write_phys(init_paddr, &init_page)
1150            .map_4k(mm_vaddr, mm_paddr, flags::WRITABLE)
1151            .write_phys(mm_paddr, &mm_page)
1152            .map_4k(vma_vaddr, vma_paddr, flags::WRITABLE)
1153            .write_phys(vma_paddr, &vma_page)
1154            .map_4k(file_vaddr, file_paddr, flags::WRITABLE)
1155            .write_phys(file_paddr, &file_page)
1156            .build();
1157        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
1158        let reader = ObjectReader::new(vas, Box::new(resolver));
1159
1160        let result = walk_memfd_create(&reader).expect("should not error");
1161        assert!(
1162            result.is_empty(),
1163            "dentry_ptr == 0 → read_file_dentry_name None → no entries"
1164        );
1165    }
1166
1167    // --- try_read_memfd_vma: name_ptr == 0 → None (line 244) ---
1168    #[test]
1169    fn walk_memfd_name_ptr_null_returns_none() {
1170        // dentry_ptr != 0 but name_ptr == 0 → None.
1171        let init_vaddr: u64 = 0xFFFF_8800_00E0_0000;
1172        let init_paddr: u64 = 0x00E0_0000;
1173        let mm_vaddr: u64 = 0xFFFF_8800_00E1_0000;
1174        let mm_paddr: u64 = 0x00E1_0000;
1175        let vma_vaddr: u64 = 0xFFFF_8800_00E2_0000;
1176        let vma_paddr: u64 = 0x00E2_0000;
1177        let file_vaddr: u64 = 0xFFFF_8800_00E3_0000;
1178        let file_paddr: u64 = 0x00E3_0000;
1179        let dentry_vaddr: u64 = 0xFFFF_8800_00E4_0000;
1180        let dentry_paddr: u64 = 0x00E4_0000;
1181
1182        let tasks_offset: u64 = 16;
1183        let mm_offset: u64 = 48;
1184        let f_path_off: u64 = 0x10;
1185        let d_name_off: u64 = 0x08;
1186
1187        let mut init_page = [0u8; 4096];
1188        page_write_u32(&mut init_page, 0, 12u32);
1189        let tasks_self = init_vaddr + tasks_offset;
1190        page_write_u64(&mut init_page, tasks_offset as usize, tasks_self);
1191        page_write_u64(&mut init_page, tasks_offset as usize + 8, tasks_self);
1192        init_page[32..36].copy_from_slice(b"proc");
1193        page_write_u64(&mut init_page, mm_offset as usize, mm_vaddr);
1194
1195        let mut mm_page = [0u8; 4096];
1196        page_write_u64(&mut mm_page, 0, vma_vaddr);
1197
1198        let mut vma_page = [0u8; 4096];
1199        page_write_u64(&mut vma_page, 0x00, 0u64);
1200        page_write_u64(&mut vma_page, 0x08, file_vaddr);
1201        page_write_u64(&mut vma_page, 0x10, 0x1000u64);
1202        page_write_u64(&mut vma_page, 0x18, 0x2000u64);
1203        page_write_u64(&mut vma_page, 0x20, 0u64);
1204
1205        let mut file_page = [0u8; 4096];
1206        page_write_u64(&mut file_page, 0x10, dentry_vaddr); // dentry ptr != 0
1207
1208        // dentry page: name ptr at d_name_off = 0x08 → value = 0 (NULL)
1209        let mut dentry_page = [0u8; 4096];
1210        page_write_u64(&mut dentry_page, 0x08, 0u64); // name_ptr = NULL
1211
1212        let isf = IsfBuilder::new()
1213            .add_struct("list_head", 16)
1214            .add_field("list_head", "next", 0u64, "pointer")
1215            .add_field("list_head", "prev", 8u64, "pointer")
1216            .add_struct("task_struct", 128)
1217            .add_field("task_struct", "pid", 0u64, "unsigned int")
1218            .add_field("task_struct", "tasks", tasks_offset, "list_head")
1219            .add_field("task_struct", "comm", 32u64, "char")
1220            .add_field("task_struct", "mm", mm_offset, "pointer")
1221            .add_struct("mm_struct", 0x100)
1222            .add_field("mm_struct", "mmap", 0u64, "pointer")
1223            .add_struct("vm_area_struct", 0x100)
1224            .add_field("vm_area_struct", "vm_next", 0x00u64, "pointer")
1225            .add_field("vm_area_struct", "vm_file", 0x08u64, "pointer")
1226            .add_field("vm_area_struct", "vm_start", 0x10u64, "unsigned long")
1227            .add_field("vm_area_struct", "vm_end", 0x18u64, "unsigned long")
1228            .add_field("vm_area_struct", "vm_flags", 0x20u64, "unsigned long")
1229            .add_struct("file", 0x100)
1230            .add_field("file", "f_path", f_path_off, "pointer")
1231            .add_struct("path", 0x20)
1232            .add_field("path", "dentry", 0u64, "pointer")
1233            .add_struct("dentry", 0x100)
1234            .add_field("dentry", "d_name", d_name_off, "pointer")
1235            .add_struct("qstr", 0x20)
1236            .add_field("qstr", "name", 0u64, "pointer")
1237            .add_symbol("init_task", init_vaddr)
1238            .build_json();
1239
1240        let resolver = IsfResolver::from_value(&isf).unwrap();
1241        let (cr3, mem) = PageTableBuilder::new()
1242            .map_4k(init_vaddr, init_paddr, flags::WRITABLE)
1243            .write_phys(init_paddr, &init_page)
1244            .map_4k(mm_vaddr, mm_paddr, flags::WRITABLE)
1245            .write_phys(mm_paddr, &mm_page)
1246            .map_4k(vma_vaddr, vma_paddr, flags::WRITABLE)
1247            .write_phys(vma_paddr, &vma_page)
1248            .map_4k(file_vaddr, file_paddr, flags::WRITABLE)
1249            .write_phys(file_paddr, &file_page)
1250            .map_4k(dentry_vaddr, dentry_paddr, flags::WRITABLE)
1251            .write_phys(dentry_paddr, &dentry_page)
1252            .build();
1253        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
1254        let reader = ObjectReader::new(vas, Box::new(resolver));
1255
1256        let result = walk_memfd_create(&reader).expect("should not error");
1257        assert!(
1258            result.is_empty(),
1259            "name_ptr == 0 → read_file_dentry_name None → no entries"
1260        );
1261    }
1262
1263    // --- try_read_memfd_vma: dentry chain readable, name NOT "memfd:" → None ---
1264    // Exercises read_file_dentry_name (lines 267-307) and the strip_prefix check.
1265    // Also exercises collect_memfd_for_task merge logic placeholder (no merge needed when empty).
1266    #[test]
1267    fn walk_memfd_vm_file_nonzero_non_memfd_name_skipped() {
1268        let sym_vaddr: u64 = 0xFFFF_8800_0070_0000;
1269        let sym_paddr: u64 = 0x0070_0000;
1270        let tasks_offset: u64 = 16;
1271        let mm_offset: u64 = 48;
1272
1273        let mm_vaddr: u64 = 0xFFFF_8800_0071_0000;
1274        let mm_paddr: u64 = 0x0071_0000;
1275
1276        let vma_vaddr: u64 = 0xFFFF_8800_0072_0000;
1277        let vma_paddr: u64 = 0x0072_0000;
1278
1279        // file, dentry, name chain
1280        let file_vaddr: u64 = 0xFFFF_8800_0073_0000;
1281        let file_paddr: u64 = 0x0073_0000;
1282        let dentry_vaddr: u64 = 0xFFFF_8800_0074_0000;
1283        let dentry_paddr: u64 = 0x0074_0000;
1284        let name_vaddr: u64 = 0xFFFF_8800_0075_0000;
1285        let name_paddr: u64 = 0x0075_0000;
1286
1287        // Field offsets
1288        let f_path_off: u64 = 0x10; // f_path embedded at file+0x10
1289        let dentry_in_path: u64 = 0x00; // dentry* at path+0x00
1290        let d_name_off: u64 = 0x08; // qstr embedded at dentry+0x08
1291        let name_in_qstr: u64 = 0x00; // name* at qstr+0x00
1292
1293        let mut task_page = [0u8; 4096];
1294        page_write_u32(&mut task_page, 0, 5u32); // pid=5
1295        let self_ptr = sym_vaddr + tasks_offset;
1296        task_page[tasks_offset as usize..tasks_offset as usize + 8]
1297            .copy_from_slice(&self_ptr.to_le_bytes());
1298        task_page[32..37].copy_from_slice(b"bash\0");
1299        task_page[mm_offset as usize..mm_offset as usize + 8]
1300            .copy_from_slice(&mm_vaddr.to_le_bytes());
1301
1302        // mm: mmap = vma_vaddr
1303        let mut mm_page = [0u8; 4096];
1304        mm_page[0..8].copy_from_slice(&vma_vaddr.to_le_bytes());
1305
1306        // vma: vm_next at 0, vm_file at 8, vm_start at 0x10, vm_end at 0x18, vm_flags at 0x20
1307        let mut vma_page = [0u8; 4096];
1308        vma_page[0..8].copy_from_slice(&0u64.to_le_bytes()); // vm_next = 0 → loop ends
1309        vma_page[8..16].copy_from_slice(&file_vaddr.to_le_bytes()); // vm_file != 0
1310        vma_page[0x10..0x18].copy_from_slice(&0x1000u64.to_le_bytes()); // vm_start
1311        vma_page[0x18..0x20].copy_from_slice(&0x2000u64.to_le_bytes()); // vm_end
1312        vma_page[0x20..0x28].copy_from_slice(&0u64.to_le_bytes()); // vm_flags
1313
1314        // file: dentry ptr at f_path_off + dentry_in_path = 0x10
1315        let mut file_page = [0u8; 4096];
1316        file_page[0x10..0x18].copy_from_slice(&dentry_vaddr.to_le_bytes());
1317
1318        // dentry: name ptr at d_name_off + name_in_qstr = 0x08
1319        let mut dentry_page = [0u8; 4096];
1320        dentry_page[0x08..0x10].copy_from_slice(&name_vaddr.to_le_bytes());
1321
1322        // name string: NOT a memfd prefix (no "memfd:") → strip_prefix returns None
1323        let mut name_page = [0u8; 4096];
1324        name_page[..10].copy_from_slice(b"/dev/null\0");
1325
1326        let isf = IsfBuilder::new()
1327            .add_struct("list_head", 16)
1328            .add_field("list_head", "next", 0, "pointer")
1329            .add_struct("task_struct", 128)
1330            .add_field("task_struct", "pid", 0, "unsigned int")
1331            .add_field("task_struct", "tasks", tasks_offset, "pointer")
1332            .add_field("task_struct", "comm", 32, "char")
1333            .add_field("task_struct", "mm", mm_offset, "pointer")
1334            .add_struct("mm_struct", 0x200)
1335            .add_field("mm_struct", "mmap", 0x00, "pointer")
1336            .add_struct("vm_area_struct", 0x100)
1337            .add_field("vm_area_struct", "vm_next", 0x00, "pointer")
1338            .add_field("vm_area_struct", "vm_file", 0x08, "pointer")
1339            .add_field("vm_area_struct", "vm_start", 0x10, "unsigned long")
1340            .add_field("vm_area_struct", "vm_end", 0x18, "unsigned long")
1341            .add_field("vm_area_struct", "vm_flags", 0x20, "unsigned long")
1342            .add_struct("file", 0x200)
1343            .add_field("file", "f_path", f_path_off, "pointer")
1344            .add_struct("path", 0x20)
1345            .add_field("path", "dentry", dentry_in_path, "pointer")
1346            .add_struct("dentry", 0x200)
1347            .add_field("dentry", "d_name", d_name_off, "pointer")
1348            .add_struct("qstr", 0x20)
1349            .add_field("qstr", "name", name_in_qstr, "pointer")
1350            .add_symbol("init_task", sym_vaddr)
1351            .build_json();
1352
1353        let resolver = IsfResolver::from_value(&isf).unwrap();
1354        let (cr3, mem) = PageTableBuilder::new()
1355            .map_4k(sym_vaddr, sym_paddr, flags::WRITABLE)
1356            .write_phys(sym_paddr, &task_page)
1357            .map_4k(mm_vaddr, mm_paddr, flags::WRITABLE)
1358            .write_phys(mm_paddr, &mm_page)
1359            .map_4k(vma_vaddr, vma_paddr, flags::WRITABLE)
1360            .write_phys(vma_paddr, &vma_page)
1361            .map_4k(file_vaddr, file_paddr, flags::WRITABLE)
1362            .write_phys(file_paddr, &file_page)
1363            .map_4k(dentry_vaddr, dentry_paddr, flags::WRITABLE)
1364            .write_phys(dentry_paddr, &dentry_page)
1365            .map_4k(name_vaddr, name_paddr, flags::WRITABLE)
1366            .write_phys(name_paddr, &name_page)
1367            .build();
1368
1369        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
1370        let reader = ObjectReader::new(vas, Box::new(resolver));
1371
1372        let result = walk_memfd_create(&reader).expect("should not error");
1373        assert!(
1374            result.is_empty(),
1375            "non-memfd dentry name → strip_prefix fails → no entries"
1376        );
1377    }
1378}