1use memf_core::object_reader::ObjectReader;
8use memf_format::PhysicalMemoryProvider;
9
10use crate::Result;
11
12#[derive(Debug, Clone, serde::Serialize)]
14pub struct TmpfsFileInfo {
15 pub inode_number: u64,
17 pub filename: String,
19 pub file_size: u64,
21 pub uid: u32,
23 pub gid: u32,
25 pub mode: u32,
27 pub atime_sec: u64,
29 pub mtime_sec: u64,
31 pub ctime_sec: u64,
33 pub is_suspicious: bool,
35}
36
37pub use crate::heuristics::classify_tmpfs_file;
46
47pub fn walk_tmpfs_files<P: PhysicalMemoryProvider>(
52 reader: &ObjectReader<P>,
53) -> Result<Vec<TmpfsFileInfo>> {
54 let sb_list_addr = match reader.symbols().symbol_address("super_blocks") {
56 Some(addr) => addr,
57 None => return Ok(Vec::new()),
58 };
59
60 let sb_list_offset = match reader.symbols().field_offset("super_block", "s_list") {
62 Some(off) => off,
63 None => return Ok(Vec::new()),
64 };
65
66 let mut results = Vec::new();
67
68 let first_sb_list: u64 = match reader.read_bytes(sb_list_addr, 8) {
70 Ok(b) => u64::from_le_bytes(b.try_into().unwrap_or([0u8; 8])),
71 Err(_) => return Ok(Vec::new()),
72 };
73
74 let mut sb_cursor = first_sb_list;
75 let mut sb_guard = 0usize;
76 loop {
77 if sb_cursor == 0 || sb_cursor == sb_list_addr || sb_guard > 1024 {
78 break;
79 }
80 let sb_addr = sb_cursor.saturating_sub(sb_list_offset);
82
83 let s_type_ptr: u64 = if let Ok(v) = reader.read_field(sb_addr, "super_block", "s_type") {
85 v
86 } else {
87 sb_cursor = match reader.read_bytes(sb_cursor, 8) {
88 Ok(b) => u64::from_le_bytes(b.try_into().unwrap_or([0u8; 8])),
89 Err(_) => break,
90 };
91 sb_guard += 1;
92 continue;
93 };
94
95 let is_tmpfs = if s_type_ptr != 0 {
96 let name_ptr: u64 = reader
98 .read_bytes(s_type_ptr, 8)
99 .ok()
100 .and_then(|b| b.try_into().ok())
101 .map_or(0, u64::from_le_bytes);
102 if name_ptr != 0 {
103 let name_bytes: Vec<u8> = reader.read_bytes(name_ptr, 8).unwrap_or_default();
104 let fs_name = std::str::from_utf8(&name_bytes)
105 .unwrap_or("")
106 .split('\0')
107 .next()
108 .unwrap_or("");
109 fs_name == "tmpfs" || fs_name == "ramfs"
110 } else {
111 false
112 }
113 } else {
114 false
115 };
116
117 if is_tmpfs {
118 let s_inodes_offset =
120 if let Some(off) = reader.symbols().field_offset("super_block", "s_inodes") {
121 off
122 } else {
123 sb_cursor = match reader.read_bytes(sb_cursor, 8) {
124 Ok(b) => u64::from_le_bytes(b.try_into().unwrap_or([0u8; 8])),
125 Err(_) => break,
126 };
127 sb_guard += 1;
128 continue;
129 };
130
131 let inode_sb_list_offset =
132 if let Some(off) = reader.symbols().field_offset("inode", "i_sb_list") {
133 off
134 } else {
135 sb_cursor = match reader.read_bytes(sb_cursor, 8) {
136 Ok(b) => u64::from_le_bytes(b.try_into().unwrap_or([0u8; 8])),
137 Err(_) => break,
138 };
139 sb_guard += 1;
140 continue;
141 };
142
143 let inode_list_head = sb_addr + s_inodes_offset;
144 let first_inode_list: u64 = reader
145 .read_bytes(inode_list_head, 8)
146 .ok()
147 .and_then(|b| b.try_into().ok())
148 .map_or(0, u64::from_le_bytes);
149
150 let mut inode_cursor = first_inode_list;
151 let mut inode_guard = 0usize;
152 loop {
153 if inode_cursor == 0 || inode_cursor == inode_list_head || inode_guard > 65536 {
154 break;
155 }
156 let inode_addr = inode_cursor.saturating_sub(inode_sb_list_offset);
157
158 let i_ino: u64 = reader.read_field(inode_addr, "inode", "i_ino").unwrap_or(0);
159 let i_size: u64 = reader
160 .read_field(inode_addr, "inode", "i_size")
161 .unwrap_or(0);
162 let i_uid: u32 = reader.read_field(inode_addr, "inode", "i_uid").unwrap_or(0);
163 let i_gid: u32 = reader.read_field(inode_addr, "inode", "i_gid").unwrap_or(0);
164 let i_mode: u32 = reader
165 .read_field(inode_addr, "inode", "i_mode")
166 .unwrap_or(0);
167 let atime_sec: u64 = reader
168 .read_field(inode_addr, "inode", "i_atime")
169 .unwrap_or(0);
170 let mtime_sec: u64 = reader
171 .read_field(inode_addr, "inode", "i_mtime")
172 .unwrap_or(0);
173 let ctime_sec: u64 = reader
174 .read_field(inode_addr, "inode", "i_ctime")
175 .unwrap_or(0);
176
177 let filename: String = (|| -> String {
184 let first: u64 = reader
186 .read_field(inode_addr, "inode", "i_dentry")
187 .unwrap_or(0);
188 if first == 0 {
189 return String::new();
190 }
191 let d_alias_offset = reader
193 .symbols()
194 .field_offset("dentry", "d_alias")
195 .unwrap_or(0);
196 let dentry_addr = first.saturating_sub(d_alias_offset);
197 if dentry_addr == 0 {
198 return String::new();
199 }
200 let name_ptr: u64 = reader
202 .read_field(dentry_addr, "dentry", "d_name_name")
203 .unwrap_or(0);
204 if name_ptr == 0 {
205 return String::new();
206 }
207 let bytes = reader.read_bytes(name_ptr, 256).unwrap_or_default();
209 let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
210 String::from_utf8_lossy(&bytes[..end]).into_owned()
211 })();
212 let is_suspicious = classify_tmpfs_file(&filename, i_mode);
213
214 results.push(TmpfsFileInfo {
215 inode_number: i_ino,
216 filename,
217 file_size: i_size,
218 uid: i_uid,
219 gid: i_gid,
220 mode: i_mode,
221 atime_sec,
222 mtime_sec,
223 ctime_sec,
224 is_suspicious,
225 });
226
227 inode_cursor = match reader.read_bytes(inode_cursor, 8) {
228 Ok(b) => u64::from_le_bytes(b.try_into().unwrap_or([0u8; 8])),
229 Err(_) => break,
230 };
231 inode_guard += 1;
232 }
233 }
234
235 sb_cursor = match reader.read_bytes(sb_cursor, 8) {
236 Ok(b) => u64::from_le_bytes(b.try_into().unwrap_or([0u8; 8])),
237 Err(_) => break,
238 };
239 sb_guard += 1;
240 }
241
242 Ok(results)
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use memf_core::object_reader::ObjectReader;
249 use memf_core::test_builders::{PageTableBuilder, SyntheticPhysMem};
250 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
251 use memf_symbols::isf::IsfResolver;
252 use memf_symbols::test_builders::IsfBuilder;
253
254 fn make_no_symbol_reader() -> ObjectReader<SyntheticPhysMem> {
255 let isf = IsfBuilder::new().build_json();
256 let resolver = IsfResolver::from_value(&isf).unwrap();
257 let (cr3, mem) = PageTableBuilder::new().build();
258 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
259 ObjectReader::new(vas, Box::new(resolver))
260 }
261
262 #[test]
263 fn classify_executable_tmpfs_file_suspicious() {
264 assert!(
265 classify_tmpfs_file("script.sh", 0o100_755),
266 "executable file must be suspicious"
267 );
268 }
269
270 #[test]
271 fn classify_hidden_file_suspicious() {
272 assert!(
273 classify_tmpfs_file(".hidden_file", 0o100_644),
274 "hidden file must be suspicious"
275 );
276 }
277
278 #[test]
279 fn classify_dot_alone_not_suspicious() {
280 assert!(
281 !classify_tmpfs_file(".", 0o040_755),
282 "bare '.' directory must not be suspicious"
283 );
284 }
285
286 #[test]
287 fn classify_normal_tmpfs_file_benign() {
288 assert!(
289 !classify_tmpfs_file("data.bin", 0o100_644),
290 "non-executable non-hidden file must not be suspicious"
291 );
292 }
293
294 #[test]
295 fn classify_executable_and_hidden_suspicious() {
296 assert!(
297 classify_tmpfs_file(".runme", 0o100_755),
298 "executable hidden file must be suspicious"
299 );
300 }
301
302 #[test]
303 fn walk_tmpfs_no_symbol_returns_empty() {
304 let reader = make_no_symbol_reader();
305 let result = walk_tmpfs_files(&reader).unwrap();
306 assert!(
307 result.is_empty(),
308 "no super_blocks symbol → empty vec expected"
309 );
310 }
311
312 #[test]
315 fn classify_empty_filename_not_suspicious() {
316 assert!(
318 !classify_tmpfs_file("", 0o100_644),
319 "empty filename non-executable must not be suspicious"
320 );
321 }
322
323 #[test]
324 fn classify_dot_with_exec_bit_not_suspicious_because_len_1() {
325 assert!(
328 !classify_tmpfs_file(".", 0o040_755),
329 "bare '.' must not be suspicious"
330 );
331 }
332
333 #[test]
334 fn classify_directory_with_exec_bits_not_suspicious() {
335 assert!(
337 !classify_tmpfs_file("mydir", 0o040_755),
338 "directory with exec bits must not be suspicious"
339 );
340 }
341
342 #[test]
343 fn classify_regular_file_no_exec_not_suspicious() {
344 assert!(
346 !classify_tmpfs_file("secret.dat", 0o100_600),
347 "regular non-executable non-hidden file must not be suspicious"
348 );
349 }
350
351 #[test]
352 fn classify_regular_file_group_exec_suspicious() {
353 assert!(
355 classify_tmpfs_file("grpexec", 0o100_610),
356 "regular file with group exec bit must be suspicious"
357 );
358 }
359
360 #[test]
361 fn classify_regular_file_other_exec_suspicious() {
362 assert!(
364 classify_tmpfs_file("otherexec", 0o100_601),
365 "regular file with other exec bit must be suspicious"
366 );
367 }
368
369 #[test]
370 fn classify_dotdot_not_suspicious() {
371 assert!(
375 classify_tmpfs_file("..", 0o040_755),
376 "'..' is two chars starting with '.'; hidden-check flags it"
377 );
378 }
379
380 #[test]
381 fn classify_non_regular_non_exec_file_benign() {
382 assert!(
384 !classify_tmpfs_file("mylink", 0o120_777),
385 "symlink with rwx bits must not be suspicious (not S_IFREG)"
386 );
387 }
388
389 #[test]
392 fn walk_tmpfs_missing_s_list_offset_returns_empty() {
393 let isf = IsfBuilder::new()
395 .add_symbol("super_blocks", 0xFFFF_8000_1234_0000)
396 .build_json();
397 let resolver = IsfResolver::from_value(&isf).unwrap();
398 let (cr3, mem) = PageTableBuilder::new().build();
399 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
400 let reader = ObjectReader::new(vas, Box::new(resolver));
401
402 let result = walk_tmpfs_files(&reader).unwrap();
403 assert!(
404 result.is_empty(),
405 "missing s_list field_offset → empty vec expected"
406 );
407 }
408
409 #[test]
412 fn walk_tmpfs_unreadable_first_sb_returns_empty() {
413 let isf = IsfBuilder::new()
415 .add_symbol("super_blocks", 0xDEAD_BEEF_0000_0000)
416 .add_struct("super_block", 512)
417 .add_field("super_block", "s_list", 0, "pointer")
418 .build_json();
419 let resolver = IsfResolver::from_value(&isf).unwrap();
420 let (cr3, mem) = PageTableBuilder::new().build();
421 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
422 let reader = ObjectReader::new(vas, Box::new(resolver));
423
424 let result = walk_tmpfs_files(&reader).unwrap();
425 assert!(
426 result.is_empty(),
427 "unreadable super_blocks address → empty vec expected"
428 );
429 }
430
431 #[test]
434 fn walk_tmpfs_symbol_present_self_pointing_list_returns_empty() {
435 use memf_core::test_builders::flags as ptf;
436
437 let sym_vaddr: u64 = 0xFFFF_8800_0010_0000;
440 let sym_paddr: u64 = 0x0030_0000; let isf = IsfBuilder::new()
443 .add_symbol("super_blocks", sym_vaddr)
444 .add_struct("super_block", 0x200)
445 .add_field("super_block", "s_list", 0x00, "pointer")
446 .add_field("super_block", "s_type", 0x08, "pointer")
447 .build_json();
448 let resolver = IsfResolver::from_value(&isf).unwrap();
449
450 let mut page = [0u8; 4096];
452 page[0..8].copy_from_slice(&sym_vaddr.to_le_bytes());
453
454 let (cr3, mem) = PageTableBuilder::new()
455 .map_4k(sym_vaddr, sym_paddr, ptf::WRITABLE)
456 .write_phys(sym_paddr, &page)
457 .build();
458
459 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
460 let reader = ObjectReader::new(vas, Box::new(resolver));
461
462 let result = walk_tmpfs_files(&reader).unwrap();
463 assert!(
464 result.is_empty(),
465 "self-pointing superblock list → no entries"
466 );
467 }
468
469 #[test]
474 fn walk_tmpfs_first_sb_nonzero_but_unreadable_sb_body() {
475 use memf_core::test_builders::flags as ptf;
476
477 let sym_vaddr: u64 = 0xFFFF_8800_0040_0000; let sym_paddr: u64 = 0x0040_0000;
487
488 let sb_list_vaddr: u64 = 0xFFFF_8800_0041_0000; let sb_list_paddr: u64 = 0x0041_0000;
492
493 let mut sym_page = [0u8; 4096];
499 sym_page[0..8].copy_from_slice(&sb_list_vaddr.to_le_bytes());
500
501 let mut sb_page = [0u8; 4096];
504 sb_page[0..8].copy_from_slice(&sym_vaddr.to_le_bytes());
505
506 let isf = IsfBuilder::new()
507 .add_symbol("super_blocks", sym_vaddr)
508 .add_struct("super_block", 0x200)
509 .add_field("super_block", "s_list", 0x10, "pointer")
510 .add_field("super_block", "s_type", 0x08, "pointer")
511 .build_json();
512 let resolver = IsfResolver::from_value(&isf).unwrap();
513
514 let (cr3, mem) = PageTableBuilder::new()
515 .map_4k(sym_vaddr, sym_paddr, ptf::WRITABLE)
516 .write_phys(sym_paddr, &sym_page)
517 .map_4k(sb_list_vaddr, sb_list_paddr, ptf::WRITABLE)
518 .write_phys(sb_list_paddr, &sb_page)
519 .build();
520
521 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
522 let reader = ObjectReader::new(vas, Box::new(resolver));
523
524 let result = walk_tmpfs_files(&reader).unwrap();
528 assert!(
529 result.is_empty(),
530 "loop body ran but s_type unreadable → no results"
531 );
532 }
533
534 #[test]
539 fn walk_tmpfs_non_tmpfs_superblock_skipped() {
540 use memf_core::test_builders::flags as ptf;
541
542 let sym_vaddr: u64 = 0xFFFF_8800_0042_0000; let sym_paddr: u64 = 0x0042_0000;
544
545 let sb_entry_vaddr: u64 = 0xFFFF_8800_0043_0000;
546 let sb_entry_paddr: u64 = 0x0043_0000;
547
548 let fs_type_vaddr: u64 = 0xFFFF_8800_0044_0000; let fs_type_paddr: u64 = 0x0044_0000;
550
551 let name_str_vaddr: u64 = 0xFFFF_8800_0045_0000; let name_str_paddr: u64 = 0x0045_0000;
553
554 let mut sym_page = [0u8; 4096];
557 sym_page[0..8].copy_from_slice(&sb_entry_vaddr.to_le_bytes());
558
559 let mut sb_page = [0u8; 4096];
563 sb_page[0x00..0x08].copy_from_slice(&sym_vaddr.to_le_bytes()); sb_page[0x08..0x10].copy_from_slice(&fs_type_vaddr.to_le_bytes()); let mut fs_type_page = [0u8; 4096];
568 fs_type_page[0..8].copy_from_slice(&name_str_vaddr.to_le_bytes());
569
570 let mut name_page = [0u8; 4096];
572 name_page[..5].copy_from_slice(b"ext4\0");
573
574 let isf = IsfBuilder::new()
575 .add_symbol("super_blocks", sym_vaddr)
576 .add_struct("super_block", 0x200)
577 .add_field("super_block", "s_list", 0x00, "pointer")
578 .add_field("super_block", "s_type", 0x08, "pointer")
579 .build_json();
580 let resolver = IsfResolver::from_value(&isf).unwrap();
581
582 let (cr3, mem) = PageTableBuilder::new()
583 .map_4k(sym_vaddr, sym_paddr, ptf::WRITABLE)
584 .write_phys(sym_paddr, &sym_page)
585 .map_4k(sb_entry_vaddr, sb_entry_paddr, ptf::WRITABLE)
586 .write_phys(sb_entry_paddr, &sb_page)
587 .map_4k(fs_type_vaddr, fs_type_paddr, ptf::WRITABLE)
588 .write_phys(fs_type_paddr, &fs_type_page)
589 .map_4k(name_str_vaddr, name_str_paddr, ptf::WRITABLE)
590 .write_phys(name_str_paddr, &name_page)
591 .build();
592
593 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
594 let reader = ObjectReader::new(vas, Box::new(resolver));
595
596 let result = walk_tmpfs_files(&reader).unwrap();
597 assert!(
598 result.is_empty(),
599 "non-tmpfs superblock must not produce entries"
600 );
601 }
602
603 #[test]
606 fn walk_tmpfs_null_s_type_ptr_skipped() {
607 use memf_core::test_builders::flags as ptf;
608
609 let sym_vaddr: u64 = 0xFFFF_8800_0046_0000;
610 let sym_paddr: u64 = 0x0046_0000;
611
612 let sb_entry_vaddr: u64 = 0xFFFF_8800_0047_0000;
613 let sb_entry_paddr: u64 = 0x0047_0000;
614
615 let mut sym_page = [0u8; 4096];
616 sym_page[0..8].copy_from_slice(&sb_entry_vaddr.to_le_bytes());
617
618 let mut sb_page = [0u8; 4096];
619 sb_page[0x00..0x08].copy_from_slice(&sym_vaddr.to_le_bytes());
621 sb_page[0x08..0x10].copy_from_slice(&0u64.to_le_bytes());
623
624 let isf = IsfBuilder::new()
625 .add_symbol("super_blocks", sym_vaddr)
626 .add_struct("super_block", 0x200)
627 .add_field("super_block", "s_list", 0x00, "pointer")
628 .add_field("super_block", "s_type", 0x08, "pointer")
629 .build_json();
630 let resolver = IsfResolver::from_value(&isf).unwrap();
631
632 let (cr3, mem) = PageTableBuilder::new()
633 .map_4k(sym_vaddr, sym_paddr, ptf::WRITABLE)
634 .write_phys(sym_paddr, &sym_page)
635 .map_4k(sb_entry_vaddr, sb_entry_paddr, ptf::WRITABLE)
636 .write_phys(sb_entry_paddr, &sb_page)
637 .build();
638
639 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
640 let reader = ObjectReader::new(vas, Box::new(resolver));
641
642 let result = walk_tmpfs_files(&reader).unwrap();
643 assert!(
644 result.is_empty(),
645 "null s_type ptr → is_tmpfs false → no entries"
646 );
647 }
648
649 #[test]
652 fn walk_tmpfs_tmpfs_sb_no_s_inodes_field_skips() {
653 use memf_core::test_builders::flags as ptf;
654
655 let sym_vaddr: u64 = 0xFFFF_8800_0048_0000;
656 let sym_paddr: u64 = 0x0048_0000;
657
658 let sb_entry_vaddr: u64 = 0xFFFF_8800_0049_0000;
659 let sb_entry_paddr: u64 = 0x0049_0000;
660
661 let fs_type_vaddr: u64 = 0xFFFF_8800_004A_0000;
662 let fs_type_paddr: u64 = 0x004A_0000;
663
664 let name_str_vaddr: u64 = 0xFFFF_8800_004B_0000;
665 let name_str_paddr: u64 = 0x004B_0000;
666
667 let mut sym_page = [0u8; 4096];
668 sym_page[0..8].copy_from_slice(&sb_entry_vaddr.to_le_bytes());
669
670 let mut sb_page = [0u8; 4096];
671 sb_page[0x00..0x08].copy_from_slice(&sym_vaddr.to_le_bytes()); sb_page[0x08..0x10].copy_from_slice(&fs_type_vaddr.to_le_bytes()); let mut fs_type_page = [0u8; 4096];
675 fs_type_page[0..8].copy_from_slice(&name_str_vaddr.to_le_bytes());
676
677 let mut name_page = [0u8; 4096];
678 name_page[..6].copy_from_slice(b"tmpfs\0");
679
680 let isf = IsfBuilder::new()
682 .add_symbol("super_blocks", sym_vaddr)
683 .add_struct("super_block", 0x200)
684 .add_field("super_block", "s_list", 0x00, "pointer")
685 .add_field("super_block", "s_type", 0x08, "pointer")
686 .build_json();
688 let resolver = IsfResolver::from_value(&isf).unwrap();
689
690 let (cr3, mem) = PageTableBuilder::new()
691 .map_4k(sym_vaddr, sym_paddr, ptf::WRITABLE)
692 .write_phys(sym_paddr, &sym_page)
693 .map_4k(sb_entry_vaddr, sb_entry_paddr, ptf::WRITABLE)
694 .write_phys(sb_entry_paddr, &sb_page)
695 .map_4k(fs_type_vaddr, fs_type_paddr, ptf::WRITABLE)
696 .write_phys(fs_type_paddr, &fs_type_page)
697 .map_4k(name_str_vaddr, name_str_paddr, ptf::WRITABLE)
698 .write_phys(name_str_paddr, &name_page)
699 .build();
700
701 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
702 let reader = ObjectReader::new(vas, Box::new(resolver));
703
704 let result = walk_tmpfs_files(&reader).unwrap();
706 assert!(
707 result.is_empty(),
708 "tmpfs sb without s_inodes offset → empty (graceful)"
709 );
710 }
711
712 #[test]
714 fn walk_tmpfs_tmpfs_sb_no_i_sb_list_field_skips() {
715 use memf_core::test_builders::flags as ptf;
716
717 let sym_vaddr: u64 = 0xFFFF_8800_004C_0000;
718 let sym_paddr: u64 = 0x004C_0000;
719
720 let sb_entry_vaddr: u64 = 0xFFFF_8800_004D_0000;
721 let sb_entry_paddr: u64 = 0x004D_0000;
722
723 let fs_type_vaddr: u64 = 0xFFFF_8800_004E_0000;
724 let fs_type_paddr: u64 = 0x004E_0000;
725
726 let name_str_vaddr: u64 = 0xFFFF_8800_004F_0000;
727 let name_str_paddr: u64 = 0x004F_0000;
728
729 let mut sym_page = [0u8; 4096];
730 sym_page[0..8].copy_from_slice(&sb_entry_vaddr.to_le_bytes());
731
732 let mut sb_page = [0u8; 4096];
733 sb_page[0x00..0x08].copy_from_slice(&sym_vaddr.to_le_bytes()); sb_page[0x08..0x10].copy_from_slice(&fs_type_vaddr.to_le_bytes()); let mut fs_type_page = [0u8; 4096];
737 fs_type_page[0..8].copy_from_slice(&name_str_vaddr.to_le_bytes());
738
739 let mut name_page = [0u8; 4096];
740 name_page[..6].copy_from_slice(b"tmpfs\0");
741
742 let isf = IsfBuilder::new()
744 .add_symbol("super_blocks", sym_vaddr)
745 .add_struct("super_block", 0x400)
746 .add_field("super_block", "s_list", 0x00, "pointer")
747 .add_field("super_block", "s_type", 0x08, "pointer")
748 .add_field("super_block", "s_inodes", 0x20, "pointer")
749 .build_json();
751 let resolver = IsfResolver::from_value(&isf).unwrap();
752
753 let (cr3, mem) = PageTableBuilder::new()
754 .map_4k(sym_vaddr, sym_paddr, ptf::WRITABLE)
755 .write_phys(sym_paddr, &sym_page)
756 .map_4k(sb_entry_vaddr, sb_entry_paddr, ptf::WRITABLE)
757 .write_phys(sb_entry_paddr, &sb_page)
758 .map_4k(fs_type_vaddr, fs_type_paddr, ptf::WRITABLE)
759 .write_phys(fs_type_paddr, &fs_type_page)
760 .map_4k(name_str_vaddr, name_str_paddr, ptf::WRITABLE)
761 .write_phys(name_str_paddr, &name_page)
762 .build();
763
764 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
765 let reader = ObjectReader::new(vas, Box::new(resolver));
766
767 let result = walk_tmpfs_files(&reader).unwrap();
768 assert!(
769 result.is_empty(),
770 "tmpfs sb with s_inodes but no i_sb_list → empty (graceful)"
771 );
772 }
773
774 #[test]
777 fn walk_tmpfs_null_name_ptr_is_not_tmpfs() {
778 use memf_core::test_builders::flags as ptf;
779
780 let sym_vaddr: u64 = 0xFFFF_8800_0054_0000;
781 let sym_paddr: u64 = 0x0054_0000;
782 let sb_entry_vaddr: u64 = 0xFFFF_8800_0055_0000;
783 let sb_entry_paddr: u64 = 0x0055_0000;
784 let fs_type_vaddr: u64 = 0xFFFF_8800_0056_0000;
785 let fs_type_paddr: u64 = 0x0056_0000;
786
787 let mut sym_page = [0u8; 4096];
788 sym_page[0..8].copy_from_slice(&sb_entry_vaddr.to_le_bytes());
789
790 let mut sb_page = [0u8; 4096];
791 sb_page[0x00..0x08].copy_from_slice(&sym_vaddr.to_le_bytes()); sb_page[0x08..0x10].copy_from_slice(&fs_type_vaddr.to_le_bytes()); let fs_type_page = [0u8; 4096];
796
797 let isf = IsfBuilder::new()
798 .add_symbol("super_blocks", sym_vaddr)
799 .add_struct("super_block", 0x200)
800 .add_field("super_block", "s_list", 0x00, "pointer")
801 .add_field("super_block", "s_type", 0x08, "pointer")
802 .build_json();
803 let resolver = IsfResolver::from_value(&isf).unwrap();
804
805 let (cr3, mem) = PageTableBuilder::new()
806 .map_4k(sym_vaddr, sym_paddr, ptf::WRITABLE)
807 .write_phys(sym_paddr, &sym_page)
808 .map_4k(sb_entry_vaddr, sb_entry_paddr, ptf::WRITABLE)
809 .write_phys(sb_entry_paddr, &sb_page)
810 .map_4k(fs_type_vaddr, fs_type_paddr, ptf::WRITABLE)
811 .write_phys(fs_type_paddr, &fs_type_page)
812 .build();
813
814 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
815 let reader = ObjectReader::new(vas, Box::new(resolver));
816
817 let result = walk_tmpfs_files(&reader).unwrap();
818 assert!(
819 result.is_empty(),
820 "null name_ptr → is_tmpfs false → no entries"
821 );
822 }
823
824 #[test]
828 fn walk_tmpfs_tmpfs_sb_with_one_inode_produces_result() {
829 use memf_core::test_builders::flags as ptf;
830
831 let sym_vaddr: u64 = 0xFFFF_8800_0057_0000;
849 let sym_paddr: u64 = 0x0057_0000;
850 let sb_vaddr: u64 = 0xFFFF_8800_0058_0000;
851 let sb_paddr: u64 = 0x0058_0000;
852 let fstype_vaddr: u64 = 0xFFFF_8800_0059_0000;
853 let fstype_paddr: u64 = 0x0059_0000;
854 let name_vaddr: u64 = 0xFFFF_8800_005A_0000;
855 let name_paddr: u64 = 0x005A_0000;
856 let inode_vaddr: u64 = 0xFFFF_8800_005B_0000;
857 let inode_paddr: u64 = 0x005B_0000;
858
859 let s_list_offset: u64 = 0x00;
861 let s_type_offset: u64 = 0x08;
862 let s_inodes_offset: u64 = 0x20;
863
864 let i_sb_list_offset: u64 = 0x08;
866 let i_ino_offset: u64 = 0x10;
867 let i_size_offset: u64 = 0x18;
868 let i_uid_offset: u64 = 0x20;
869 let i_gid_offset: u64 = 0x24;
870 let i_mode_offset: u64 = 0x28;
871 let i_atime_offset: u64 = 0x30;
872 let i_mtime_offset: u64 = 0x38;
873 let i_ctime_offset: u64 = 0x40;
874
875 let inode_list_head = sb_vaddr + s_inodes_offset;
877 let inode_list_node = inode_vaddr + i_sb_list_offset;
879
880 let mut sym_page = [0u8; 4096];
882 sym_page[0..8].copy_from_slice(&sb_vaddr.to_le_bytes());
883
884 let mut sb_page = [0u8; 4096];
889 sb_page[s_list_offset as usize..s_list_offset as usize + 8]
890 .copy_from_slice(&sym_vaddr.to_le_bytes());
891 sb_page[s_type_offset as usize..s_type_offset as usize + 8]
892 .copy_from_slice(&fstype_vaddr.to_le_bytes());
893 sb_page[s_inodes_offset as usize..s_inodes_offset as usize + 8]
894 .copy_from_slice(&inode_list_node.to_le_bytes());
895
896 let mut fstype_page = [0u8; 4096];
898 fstype_page[0..8].copy_from_slice(&name_vaddr.to_le_bytes());
899
900 let mut name_page = [0u8; 4096];
902 name_page[..6].copy_from_slice(b"tmpfs\0");
903
904 let mut inode_page = [0u8; 4096];
915 inode_page[i_sb_list_offset as usize..i_sb_list_offset as usize + 8]
916 .copy_from_slice(&inode_list_head.to_le_bytes());
917 inode_page[i_ino_offset as usize..i_ino_offset as usize + 8]
918 .copy_from_slice(&1234u64.to_le_bytes());
919 inode_page[i_size_offset as usize..i_size_offset as usize + 8]
920 .copy_from_slice(&4096u64.to_le_bytes());
921 inode_page[i_uid_offset as usize..i_uid_offset as usize + 4]
922 .copy_from_slice(&500u32.to_le_bytes());
923 inode_page[i_gid_offset as usize..i_gid_offset as usize + 4]
924 .copy_from_slice(&501u32.to_le_bytes());
925 inode_page[i_mode_offset as usize..i_mode_offset as usize + 4]
926 .copy_from_slice(&0o100_755u32.to_le_bytes());
927 inode_page[i_atime_offset as usize..i_atime_offset as usize + 8]
928 .copy_from_slice(&1000u64.to_le_bytes());
929 inode_page[i_mtime_offset as usize..i_mtime_offset as usize + 8]
930 .copy_from_slice(&2000u64.to_le_bytes());
931 inode_page[i_ctime_offset as usize..i_ctime_offset as usize + 8]
932 .copy_from_slice(&3000u64.to_le_bytes());
933
934 let isf = IsfBuilder::new()
935 .add_symbol("super_blocks", sym_vaddr)
936 .add_struct("super_block", 0x400)
937 .add_field("super_block", "s_list", s_list_offset, "pointer")
938 .add_field("super_block", "s_type", s_type_offset, "pointer")
939 .add_field("super_block", "s_inodes", s_inodes_offset, "pointer")
940 .add_struct("inode", 0x200)
941 .add_field("inode", "i_sb_list", i_sb_list_offset, "pointer")
942 .add_field("inode", "i_ino", i_ino_offset, "unsigned long")
943 .add_field("inode", "i_size", i_size_offset, "long long")
944 .add_field("inode", "i_uid", i_uid_offset, "unsigned int")
945 .add_field("inode", "i_gid", i_gid_offset, "unsigned int")
946 .add_field("inode", "i_mode", i_mode_offset, "unsigned int")
947 .add_field("inode", "i_atime", i_atime_offset, "long long")
948 .add_field("inode", "i_mtime", i_mtime_offset, "long long")
949 .add_field("inode", "i_ctime", i_ctime_offset, "long long")
950 .build_json();
951 let resolver = IsfResolver::from_value(&isf).unwrap();
952
953 let (cr3, mem) = PageTableBuilder::new()
954 .map_4k(sym_vaddr, sym_paddr, ptf::WRITABLE)
955 .write_phys(sym_paddr, &sym_page)
956 .map_4k(sb_vaddr, sb_paddr, ptf::WRITABLE)
957 .write_phys(sb_paddr, &sb_page)
958 .map_4k(fstype_vaddr, fstype_paddr, ptf::WRITABLE)
959 .write_phys(fstype_paddr, &fstype_page)
960 .map_4k(name_vaddr, name_paddr, ptf::WRITABLE)
961 .write_phys(name_paddr, &name_page)
962 .map_4k(inode_vaddr, inode_paddr, ptf::WRITABLE)
963 .write_phys(inode_paddr, &inode_page)
964 .build();
965
966 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
967 let reader = ObjectReader::new(vas, Box::new(resolver));
968
969 let result = walk_tmpfs_files(&reader).unwrap();
970 assert_eq!(result.len(), 1, "should find exactly one inode");
971 let fi = &result[0];
972 assert_eq!(fi.inode_number, 1234);
973 assert_eq!(fi.file_size, 4096);
974 assert_eq!(fi.uid, 500);
975 assert_eq!(fi.gid, 501);
976 assert_eq!(fi.mode, 0o100_755);
977 assert_eq!(fi.atime_sec, 1000);
978 assert_eq!(fi.mtime_sec, 2000);
979 assert_eq!(fi.ctime_sec, 3000);
980 assert!(
981 fi.is_suspicious,
982 "executable regular file must be suspicious"
983 );
984 }
985
986 #[test]
992 fn inode_with_dentry_returns_filename() {
993 use memf_core::test_builders::flags as ptf;
994
995 let sym_vaddr: u64 = 0xFFFF_8800_0060_0000;
1005 let sym_paddr: u64 = 0x0060_0000;
1006 let sb_vaddr: u64 = 0xFFFF_8800_0061_0000;
1007 let sb_paddr: u64 = 0x0061_0000;
1008 let fstype_vaddr: u64 = 0xFFFF_8800_0062_0000;
1009 let fstype_paddr: u64 = 0x0062_0000;
1010 let fsname_vaddr: u64 = 0xFFFF_8800_0063_0000;
1011 let fsname_paddr: u64 = 0x0063_0000;
1012 let inode_vaddr: u64 = 0xFFFF_8800_0064_0000;
1013 let inode_paddr: u64 = 0x0064_0000;
1014 let dentry_vaddr: u64 = 0xFFFF_8800_0065_0000;
1015 let dentry_paddr: u64 = 0x0065_0000;
1016 let dname_str_vaddr: u64 = 0xFFFF_8800_0066_0000;
1017 let dname_str_paddr: u64 = 0x0066_0000;
1018
1019 let s_list_off: u64 = 0x00;
1021 let s_type_off: u64 = 0x08;
1022 let s_inodes_off: u64 = 0x20;
1023
1024 let i_sb_list_off: u64 = 0x08;
1026 let i_ino_off: u64 = 0x10;
1027 let i_size_off: u64 = 0x18;
1028 let i_uid_off: u64 = 0x20;
1029 let i_gid_off: u64 = 0x24;
1030 let i_mode_off: u64 = 0x28;
1031 let i_atime_off: u64 = 0x30;
1032 let i_mtime_off: u64 = 0x38;
1033 let i_ctime_off: u64 = 0x40;
1034 let i_dentry_off: u64 = 0x48; let d_alias_off: u64 = 0x00; let d_name_off: u64 = 0x20; let d_name_name_off: u64 = d_name_off + 0x08; let hlist_node_ptr = dentry_vaddr + d_alias_off;
1044
1045 let inode_list_head = sb_vaddr + s_inodes_off;
1047 let inode_list_node = inode_vaddr + i_sb_list_off;
1048
1049 let mut sym_page = [0u8; 4096];
1051 sym_page[0..8].copy_from_slice(&sb_vaddr.to_le_bytes());
1052
1053 let mut sb_page = [0u8; 4096];
1055 sb_page[s_list_off as usize..s_list_off as usize + 8]
1056 .copy_from_slice(&sym_vaddr.to_le_bytes()); sb_page[s_type_off as usize..s_type_off as usize + 8]
1058 .copy_from_slice(&fstype_vaddr.to_le_bytes());
1059 sb_page[s_inodes_off as usize..s_inodes_off as usize + 8]
1060 .copy_from_slice(&inode_list_node.to_le_bytes());
1061
1062 let mut fstype_page = [0u8; 4096];
1064 fstype_page[0..8].copy_from_slice(&fsname_vaddr.to_le_bytes());
1065
1066 let mut fsname_page = [0u8; 4096];
1068 fsname_page[..6].copy_from_slice(b"tmpfs\0");
1069
1070 let mut inode_page = [0u8; 4096];
1072 inode_page[i_sb_list_off as usize..i_sb_list_off as usize + 8]
1073 .copy_from_slice(&inode_list_head.to_le_bytes()); inode_page[i_ino_off as usize..i_ino_off as usize + 8]
1075 .copy_from_slice(&42u64.to_le_bytes());
1076 inode_page[i_size_off as usize..i_size_off as usize + 8]
1077 .copy_from_slice(&512u64.to_le_bytes());
1078 inode_page[i_uid_off as usize..i_uid_off as usize + 4]
1079 .copy_from_slice(&1000u32.to_le_bytes());
1080 inode_page[i_gid_off as usize..i_gid_off as usize + 4]
1081 .copy_from_slice(&1000u32.to_le_bytes());
1082 inode_page[i_mode_off as usize..i_mode_off as usize + 4]
1083 .copy_from_slice(&0o100_644u32.to_le_bytes());
1084 inode_page[i_atime_off as usize..i_atime_off as usize + 8]
1085 .copy_from_slice(&100u64.to_le_bytes());
1086 inode_page[i_mtime_off as usize..i_mtime_off as usize + 8]
1087 .copy_from_slice(&200u64.to_le_bytes());
1088 inode_page[i_ctime_off as usize..i_ctime_off as usize + 8]
1089 .copy_from_slice(&300u64.to_le_bytes());
1090 inode_page[i_dentry_off as usize..i_dentry_off as usize + 8]
1092 .copy_from_slice(&hlist_node_ptr.to_le_bytes());
1093
1094 let mut dentry_page = [0u8; 4096];
1096 dentry_page[d_name_name_off as usize..d_name_name_off as usize + 8]
1098 .copy_from_slice(&dname_str_vaddr.to_le_bytes());
1099
1100 let mut dname_str_page = [0u8; 4096];
1102 dname_str_page[..11].copy_from_slice(b"secret.txt\0");
1103
1104 let isf = IsfBuilder::new()
1105 .add_symbol("super_blocks", sym_vaddr)
1106 .add_struct("super_block", 0x400)
1107 .add_field("super_block", "s_list", s_list_off, "pointer")
1108 .add_field("super_block", "s_type", s_type_off, "pointer")
1109 .add_field("super_block", "s_inodes", s_inodes_off, "pointer")
1110 .add_struct("inode", 0x200)
1111 .add_field("inode", "i_sb_list", i_sb_list_off, "pointer")
1112 .add_field("inode", "i_ino", i_ino_off, "unsigned long")
1113 .add_field("inode", "i_size", i_size_off, "long long")
1114 .add_field("inode", "i_uid", i_uid_off, "unsigned int")
1115 .add_field("inode", "i_gid", i_gid_off, "unsigned int")
1116 .add_field("inode", "i_mode", i_mode_off, "unsigned int")
1117 .add_field("inode", "i_atime", i_atime_off, "long long")
1118 .add_field("inode", "i_mtime", i_mtime_off, "long long")
1119 .add_field("inode", "i_ctime", i_ctime_off, "long long")
1120 .add_field("inode", "i_dentry", i_dentry_off, "pointer")
1121 .add_struct("dentry", 0x200)
1122 .add_field("dentry", "d_alias", d_alias_off, "pointer")
1123 .add_field("dentry", "d_name_name", d_name_name_off, "pointer")
1124 .build_json();
1125 let resolver = IsfResolver::from_value(&isf).unwrap();
1126
1127 let (cr3, mem) = PageTableBuilder::new()
1128 .map_4k(sym_vaddr, sym_paddr, ptf::WRITABLE)
1129 .write_phys(sym_paddr, &sym_page)
1130 .map_4k(sb_vaddr, sb_paddr, ptf::WRITABLE)
1131 .write_phys(sb_paddr, &sb_page)
1132 .map_4k(fstype_vaddr, fstype_paddr, ptf::WRITABLE)
1133 .write_phys(fstype_paddr, &fstype_page)
1134 .map_4k(fsname_vaddr, fsname_paddr, ptf::WRITABLE)
1135 .write_phys(fsname_paddr, &fsname_page)
1136 .map_4k(inode_vaddr, inode_paddr, ptf::WRITABLE)
1137 .write_phys(inode_paddr, &inode_page)
1138 .map_4k(dentry_vaddr, dentry_paddr, ptf::WRITABLE)
1139 .write_phys(dentry_paddr, &dentry_page)
1140 .map_4k(dname_str_vaddr, dname_str_paddr, ptf::WRITABLE)
1141 .write_phys(dname_str_paddr, &dname_str_page)
1142 .build();
1143
1144 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
1145 let reader = ObjectReader::new(vas, Box::new(resolver));
1146
1147 let result = walk_tmpfs_files(&reader).unwrap();
1148 assert_eq!(result.len(), 1, "should find exactly one inode");
1149 assert_eq!(
1150 result[0].filename, "secret.txt",
1151 "filename must be resolved from dentry d_name.name"
1152 );
1153 }
1154
1155 #[test]
1157 fn inode_without_dentry_returns_empty_filename() {
1158 use memf_core::test_builders::flags as ptf;
1159
1160 let sym_vaddr: u64 = 0xFFFF_8800_0067_0000;
1161 let sym_paddr: u64 = 0x0067_0000;
1162 let sb_vaddr: u64 = 0xFFFF_8800_0068_0000;
1163 let sb_paddr: u64 = 0x0068_0000;
1164 let fstype_vaddr: u64 = 0xFFFF_8800_0069_0000;
1165 let fstype_paddr: u64 = 0x0069_0000;
1166 let fsname_vaddr: u64 = 0xFFFF_8800_006A_0000;
1167 let fsname_paddr: u64 = 0x006A_0000;
1168 let inode_vaddr: u64 = 0xFFFF_8800_006B_0000;
1169 let inode_paddr: u64 = 0x006B_0000;
1170
1171 let s_list_off: u64 = 0x00;
1172 let s_type_off: u64 = 0x08;
1173 let s_inodes_off: u64 = 0x20;
1174 let i_sb_list_off: u64 = 0x08;
1175 let i_ino_off: u64 = 0x10;
1176 let i_size_off: u64 = 0x18;
1177 let i_uid_off: u64 = 0x20;
1178 let i_gid_off: u64 = 0x24;
1179 let i_mode_off: u64 = 0x28;
1180 let i_atime_off: u64 = 0x30;
1181 let i_mtime_off: u64 = 0x38;
1182 let i_ctime_off: u64 = 0x40;
1183 let i_dentry_off: u64 = 0x48;
1184
1185 let inode_list_head = sb_vaddr + s_inodes_off;
1186 let inode_list_node = inode_vaddr + i_sb_list_off;
1187
1188 let mut sym_page = [0u8; 4096];
1189 sym_page[0..8].copy_from_slice(&sb_vaddr.to_le_bytes());
1190
1191 let mut sb_page = [0u8; 4096];
1192 sb_page[s_list_off as usize..s_list_off as usize + 8]
1193 .copy_from_slice(&sym_vaddr.to_le_bytes());
1194 sb_page[s_type_off as usize..s_type_off as usize + 8]
1195 .copy_from_slice(&fstype_vaddr.to_le_bytes());
1196 sb_page[s_inodes_off as usize..s_inodes_off as usize + 8]
1197 .copy_from_slice(&inode_list_node.to_le_bytes());
1198
1199 let mut fstype_page = [0u8; 4096];
1200 fstype_page[0..8].copy_from_slice(&fsname_vaddr.to_le_bytes());
1201
1202 let mut fsname_page = [0u8; 4096];
1203 fsname_page[..6].copy_from_slice(b"tmpfs\0");
1204
1205 let mut inode_page = [0u8; 4096];
1206 inode_page[i_sb_list_off as usize..i_sb_list_off as usize + 8]
1207 .copy_from_slice(&inode_list_head.to_le_bytes());
1208 inode_page[i_ino_off as usize..i_ino_off as usize + 8]
1209 .copy_from_slice(&99u64.to_le_bytes());
1210 inode_page[i_size_off as usize..i_size_off as usize + 8]
1211 .copy_from_slice(&0u64.to_le_bytes());
1212 inode_page[i_uid_off as usize..i_uid_off as usize + 4].copy_from_slice(&0u32.to_le_bytes());
1213 inode_page[i_gid_off as usize..i_gid_off as usize + 4].copy_from_slice(&0u32.to_le_bytes());
1214 inode_page[i_mode_off as usize..i_mode_off as usize + 4]
1215 .copy_from_slice(&0o100_644u32.to_le_bytes());
1216 inode_page[i_atime_off as usize..i_atime_off as usize + 8]
1217 .copy_from_slice(&0u64.to_le_bytes());
1218 inode_page[i_mtime_off as usize..i_mtime_off as usize + 8]
1219 .copy_from_slice(&0u64.to_le_bytes());
1220 inode_page[i_ctime_off as usize..i_ctime_off as usize + 8]
1221 .copy_from_slice(&0u64.to_le_bytes());
1222 inode_page[i_dentry_off as usize..i_dentry_off as usize + 8]
1224 .copy_from_slice(&0u64.to_le_bytes());
1225
1226 let isf = IsfBuilder::new()
1227 .add_symbol("super_blocks", sym_vaddr)
1228 .add_struct("super_block", 0x400)
1229 .add_field("super_block", "s_list", s_list_off, "pointer")
1230 .add_field("super_block", "s_type", s_type_off, "pointer")
1231 .add_field("super_block", "s_inodes", s_inodes_off, "pointer")
1232 .add_struct("inode", 0x200)
1233 .add_field("inode", "i_sb_list", i_sb_list_off, "pointer")
1234 .add_field("inode", "i_ino", i_ino_off, "unsigned long")
1235 .add_field("inode", "i_size", i_size_off, "long long")
1236 .add_field("inode", "i_uid", i_uid_off, "unsigned int")
1237 .add_field("inode", "i_gid", i_gid_off, "unsigned int")
1238 .add_field("inode", "i_mode", i_mode_off, "unsigned int")
1239 .add_field("inode", "i_atime", i_atime_off, "long long")
1240 .add_field("inode", "i_mtime", i_mtime_off, "long long")
1241 .add_field("inode", "i_ctime", i_ctime_off, "long long")
1242 .add_field("inode", "i_dentry", i_dentry_off, "pointer")
1243 .add_struct("dentry", 0x200)
1244 .add_field("dentry", "d_alias", 0x00, "pointer")
1245 .add_field("dentry", "d_name_name", 0x28, "pointer")
1246 .build_json();
1247 let resolver = IsfResolver::from_value(&isf).unwrap();
1248
1249 let (cr3, mem) = PageTableBuilder::new()
1250 .map_4k(sym_vaddr, sym_paddr, ptf::WRITABLE)
1251 .write_phys(sym_paddr, &sym_page)
1252 .map_4k(sb_vaddr, sb_paddr, ptf::WRITABLE)
1253 .write_phys(sb_paddr, &sb_page)
1254 .map_4k(fstype_vaddr, fstype_paddr, ptf::WRITABLE)
1255 .write_phys(fstype_paddr, &fstype_page)
1256 .map_4k(fsname_vaddr, fsname_paddr, ptf::WRITABLE)
1257 .write_phys(fsname_paddr, &fsname_page)
1258 .map_4k(inode_vaddr, inode_paddr, ptf::WRITABLE)
1259 .write_phys(inode_paddr, &inode_page)
1260 .build();
1261
1262 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
1263 let reader = ObjectReader::new(vas, Box::new(resolver));
1264
1265 let result = walk_tmpfs_files(&reader).unwrap();
1266 assert_eq!(result.len(), 1, "should find exactly one inode");
1267 assert_eq!(
1268 result[0].filename, "",
1269 "null i_dentry.first → filename must be empty string"
1270 );
1271 }
1272
1273 #[test]
1275 fn tmpfs_file_info_clone_debug_serialize() {
1276 let info = TmpfsFileInfo {
1277 inode_number: 42,
1278 filename: ".evil".to_string(),
1279 file_size: 1024,
1280 uid: 0,
1281 gid: 0,
1282 mode: 0o100_755,
1283 atime_sec: 1000,
1284 mtime_sec: 2000,
1285 ctime_sec: 3000,
1286 is_suspicious: true,
1287 };
1288 let cloned = info.clone();
1289 assert_eq!(cloned.inode_number, 42);
1290 let dbg = format!("{cloned:?}");
1291 assert!(dbg.contains("evil"));
1292 let json = serde_json::to_string(&cloned).unwrap();
1293 assert!(json.contains("\"inode_number\":42"));
1294 assert!(json.contains("\"is_suspicious\":true"));
1295 }
1296
1297 #[test]
1301 fn walk_tmpfs_tmpfs_sb_self_pointing_inode_list_returns_empty() {
1302 use memf_core::test_builders::flags as ptf;
1303
1304 let sym_vaddr: u64 = 0xFFFF_8800_0050_0000;
1310 let sym_paddr: u64 = 0x0050_0000;
1311 let sb_entry_vaddr: u64 = 0xFFFF_8800_0051_0000;
1312 let sb_entry_paddr: u64 = 0x0051_0000;
1313 let fs_type_vaddr: u64 = 0xFFFF_8800_0052_0000;
1314 let fs_type_paddr: u64 = 0x0052_0000;
1315 let name_str_vaddr: u64 = 0xFFFF_8800_0053_0000;
1316 let name_str_paddr: u64 = 0x0053_0000;
1317
1318 let s_inodes_offset: u64 = 0x20;
1323
1324 let inode_list_head = sb_entry_vaddr + s_inodes_offset;
1327
1328 let mut sym_page = [0u8; 4096];
1330 sym_page[0..8].copy_from_slice(&sb_entry_vaddr.to_le_bytes());
1331
1332 let mut sb_page = [0u8; 4096];
1334 sb_page[0x00..0x08].copy_from_slice(&sym_vaddr.to_le_bytes()); sb_page[0x08..0x10].copy_from_slice(&fs_type_vaddr.to_le_bytes()); sb_page[s_inodes_offset as usize..s_inodes_offset as usize + 8]
1338 .copy_from_slice(&inode_list_head.to_le_bytes());
1339
1340 let mut fs_type_page = [0u8; 4096];
1341 fs_type_page[0..8].copy_from_slice(&name_str_vaddr.to_le_bytes());
1342
1343 let mut name_page = [0u8; 4096];
1344 name_page[..6].copy_from_slice(b"tmpfs\0");
1345
1346 let isf = IsfBuilder::new()
1348 .add_symbol("super_blocks", sym_vaddr)
1349 .add_struct("super_block", 0x400)
1350 .add_field("super_block", "s_list", 0x00, "pointer")
1351 .add_field("super_block", "s_type", 0x08, "pointer")
1352 .add_field("super_block", "s_inodes", s_inodes_offset, "pointer")
1353 .add_struct("inode", 0x400)
1354 .add_field("inode", "i_sb_list", 0x08, "pointer")
1355 .add_field("inode", "i_ino", 0x10, "unsigned long")
1356 .add_field("inode", "i_size", 0x18, "long long")
1357 .add_field("inode", "i_uid", 0x20, "unsigned int")
1358 .add_field("inode", "i_gid", 0x24, "unsigned int")
1359 .add_field("inode", "i_mode", 0x28, "unsigned int")
1360 .add_field("inode", "i_atime", 0x30, "long long")
1361 .add_field("inode", "i_mtime", 0x38, "long long")
1362 .add_field("inode", "i_ctime", 0x40, "long long")
1363 .build_json();
1364 let resolver = IsfResolver::from_value(&isf).unwrap();
1365
1366 let (cr3, mem) = PageTableBuilder::new()
1367 .map_4k(sym_vaddr, sym_paddr, ptf::WRITABLE)
1368 .write_phys(sym_paddr, &sym_page)
1369 .map_4k(sb_entry_vaddr, sb_entry_paddr, ptf::WRITABLE)
1370 .write_phys(sb_entry_paddr, &sb_page)
1371 .map_4k(fs_type_vaddr, fs_type_paddr, ptf::WRITABLE)
1372 .write_phys(fs_type_paddr, &fs_type_page)
1373 .map_4k(name_str_vaddr, name_str_paddr, ptf::WRITABLE)
1374 .write_phys(name_str_paddr, &name_page)
1375 .build();
1376
1377 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
1378 let reader = ObjectReader::new(vas, Box::new(resolver));
1379
1380 let result = walk_tmpfs_files(&reader).unwrap();
1381 assert!(
1382 result.is_empty(),
1383 "tmpfs sb with self-pointing inode list → 0 inodes"
1384 );
1385 }
1386}