Skip to main content

memf_linux/
fs.rs

1//! Linux mounted filesystem walker.
2//!
3//! Enumerates mounted filesystems by walking the `mount` linked list
4//! from `init_task.nsproxy → mnt_namespace → list`. Each `mount`
5//! struct provides the device name, mount point dentry, and filesystem
6//! type from the super_block.
7
8use memf_core::object_reader::ObjectReader;
9use memf_format::PhysicalMemoryProvider;
10
11use crate::{Error, MountInfo, Result};
12
13/// Walk all mounted filesystems visible from init's mount namespace.
14///
15/// Follows `init_task.nsproxy → mnt_namespace.list` to enumerate
16/// `struct mount` entries, reading dev_name, mountpoint, and fs type.
17pub fn walk_filesystems<P: PhysicalMemoryProvider>(
18    reader: &ObjectReader<P>,
19) -> Result<Vec<MountInfo>> {
20    let init_task_addr = reader
21        .symbols()
22        .symbol_address("init_task")
23        .ok_or_else(|| Error::MissingKernelSymbol {
24            name: "init_task".into(),
25        })?;
26
27    let nsproxy_ptr: u64 = reader.read_field(init_task_addr, "task_struct", "nsproxy")?;
28    if nsproxy_ptr == 0 {
29        return Err(Error::WalkFailed {
30            walker: "walk_filesystems",
31            reason: "init_task has NULL nsproxy".into(),
32        });
33    }
34
35    let mnt_ns_ptr: u64 = reader.read_field(nsproxy_ptr, "nsproxy", "mnt_ns")?;
36    if mnt_ns_ptr == 0 {
37        return Err(Error::WalkFailed {
38            walker: "walk_filesystems",
39            reason: "nsproxy has NULL mnt_ns".into(),
40        });
41    }
42
43    // mnt_namespace.list is the head of a circular list of mount.mnt_list
44    let mount_addrs = reader.walk_list(mnt_ns_ptr, "mount", "mnt_list")?;
45
46    let d_name_offset = reader
47        .symbols()
48        .field_offset("dentry", "d_name")
49        .ok_or_else(|| Error::MissingField {
50            struct_name: "dentry".into(),
51            field_name: "d_name".into(),
52        })?;
53
54    let name_in_qstr_offset = reader
55        .symbols()
56        .field_offset("qstr", "name")
57        .ok_or_else(|| Error::MissingField {
58            struct_name: "qstr".into(),
59            field_name: "name".into(),
60        })?;
61
62    let mnt_offset = reader
63        .symbols()
64        .field_offset("mount", "mnt")
65        .ok_or_else(|| Error::MissingField {
66            struct_name: "mount".into(),
67            field_name: "mnt".into(),
68        })?;
69
70    let mut mounts = Vec::new();
71
72    for &mount_addr in &mount_addrs {
73        // Read device name string
74        let devname_ptr: u64 = reader.read_field(mount_addr, "mount", "mnt_devname")?;
75        let dev_name = if devname_ptr != 0 {
76            reader.read_string(devname_ptr, 256).unwrap_or_default()
77        } else {
78            String::new()
79        };
80
81        // Read mount point from dentry → d_name.name
82        let mountpoint_ptr: u64 = reader.read_field(mount_addr, "mount", "mnt_mountpoint")?;
83        let mount_point = if mountpoint_ptr != 0 {
84            read_dentry_name(reader, mountpoint_ptr, d_name_offset, name_in_qstr_offset)
85                .unwrap_or_default()
86        } else {
87            String::new()
88        };
89
90        // Read filesystem type: mount.mnt.mnt_sb → super_block.s_type → file_system_type.name
91        let sb_ptr: u64 = reader.read_field(mount_addr + mnt_offset, "vfsmount", "mnt_sb")?;
92        let fs_type = if sb_ptr != 0 {
93            read_fs_type_name(reader, sb_ptr).unwrap_or_default()
94        } else {
95            String::new()
96        };
97
98        mounts.push(MountInfo {
99            dev_name,
100            mount_point,
101            fs_type,
102        });
103    }
104
105    Ok(mounts)
106}
107
108/// Read the name from a dentry's embedded d_name (qstr).
109fn read_dentry_name<P: PhysicalMemoryProvider>(
110    reader: &ObjectReader<P>,
111    dentry_ptr: u64,
112    d_name_offset: u64,
113    name_in_qstr_offset: u64,
114) -> Result<String> {
115    let name_addr = dentry_ptr + d_name_offset + name_in_qstr_offset;
116    let name_raw = reader.read_bytes(name_addr, 8)?;
117    let name_ptr = name_raw.try_into().map_or(0, u64::from_le_bytes);
118    if name_ptr != 0 {
119        Ok(reader.read_string(name_ptr, 256)?)
120    } else {
121        Ok(String::new())
122    }
123}
124
125/// Read the filesystem type name from a super_block.
126fn read_fs_type_name<P: PhysicalMemoryProvider>(
127    reader: &ObjectReader<P>,
128    sb_ptr: u64,
129) -> Result<String> {
130    let s_type_ptr: u64 = reader.read_field(sb_ptr, "super_block", "s_type")?;
131    if s_type_ptr == 0 {
132        return Ok(String::new());
133    }
134    let name_ptr: u64 = reader.read_field(s_type_ptr, "file_system_type", "name")?;
135    if name_ptr == 0 {
136        return Ok(String::new());
137    }
138    Ok(reader.read_string(name_ptr, 64)?)
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use memf_core::test_builders::{flags, PageTableBuilder, SyntheticPhysMem};
145    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
146    use memf_symbols::isf::IsfResolver;
147    use memf_symbols::test_builders::IsfBuilder;
148
149    fn make_test_reader(data: &[u8], vaddr: u64, paddr: u64) -> ObjectReader<SyntheticPhysMem> {
150        let isf = IsfBuilder::new()
151            // task_struct
152            .add_struct("task_struct", 128)
153            .add_field("task_struct", "pid", 0, "int")
154            .add_field("task_struct", "tasks", 16, "list_head")
155            .add_field("task_struct", "nsproxy", 64, "pointer")
156            // list_head
157            .add_struct("list_head", 16)
158            .add_field("list_head", "next", 0, "pointer")
159            .add_field("list_head", "prev", 8, "pointer")
160            // nsproxy
161            .add_struct("nsproxy", 48)
162            .add_field("nsproxy", "mnt_ns", 16, "pointer")
163            // mnt_namespace
164            .add_struct("mnt_namespace", 32)
165            .add_field("mnt_namespace", "list", 0, "list_head")
166            // mount
167            .add_struct("mount", 256)
168            .add_field("mount", "mnt_list", 0, "list_head")
169            .add_field("mount", "mnt_devname", 16, "pointer")
170            .add_field("mount", "mnt_mountpoint", 24, "pointer")
171            .add_field("mount", "mnt", 32, "vfsmount")
172            // vfsmount (embedded in mount)
173            .add_struct("vfsmount", 32)
174            .add_field("vfsmount", "mnt_sb", 0, "pointer")
175            // super_block
176            .add_struct("super_block", 64)
177            .add_field("super_block", "s_type", 0, "pointer")
178            // file_system_type
179            .add_struct("file_system_type", 64)
180            .add_field("file_system_type", "name", 0, "pointer")
181            // dentry
182            .add_struct("dentry", 64)
183            .add_field("dentry", "d_name", 0, "qstr")
184            // qstr
185            .add_struct("qstr", 16)
186            .add_field("qstr", "name", 8, "pointer")
187            // symbol
188            .add_symbol("init_task", vaddr)
189            .build_json();
190
191        let resolver = IsfResolver::from_value(&isf).unwrap();
192        let (cr3, mem) = PageTableBuilder::new()
193            .map_4k(vaddr, paddr, flags::WRITABLE)
194            .write_phys(paddr, data)
195            .build();
196        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
197        ObjectReader::new(vas, Box::new(resolver))
198    }
199
200    #[test]
201    fn walk_two_mounts() {
202        let vaddr: u64 = 0xFFFF_8000_0010_0000;
203        let paddr: u64 = 0x0080_0000;
204        let mut data = vec![0u8; 4096];
205
206        // init_task: nsproxy at +64
207        let nsproxy_addr = vaddr + 0x100;
208        data[64..72].copy_from_slice(&nsproxy_addr.to_le_bytes());
209
210        // nsproxy at +0x100: mnt_ns at offset 16
211        let mnt_ns_addr = vaddr + 0x180;
212        data[0x110..0x118].copy_from_slice(&mnt_ns_addr.to_le_bytes());
213
214        // mnt_namespace at +0x180: list head
215        // list.next → mount1.mnt_list, list.prev → mount2.mnt_list
216        let ns_list_addr = vaddr + 0x180; // list_head at offset 0 in mnt_namespace
217        let mount1_addr = vaddr + 0x200;
218        let mount2_addr = vaddr + 0x400;
219        let mount1_list = mount1_addr; // mnt_list at offset 0 in mount
220        let mount2_list = mount2_addr;
221        data[0x180..0x188].copy_from_slice(&mount1_list.to_le_bytes()); // list.next → mount1
222        data[0x188..0x190].copy_from_slice(&mount2_list.to_le_bytes()); // list.prev → mount2
223
224        // mount1 at +0x200: rootfs on /
225        data[0x200..0x208].copy_from_slice(&mount2_list.to_le_bytes()); // mnt_list.next → mount2
226        data[0x208..0x210].copy_from_slice(&ns_list_addr.to_le_bytes()); // mnt_list.prev → ns head
227        let devname1_addr = vaddr + 0x300;
228        data[0x210..0x218].copy_from_slice(&devname1_addr.to_le_bytes()); // mnt_devname
229        let dentry1_addr = vaddr + 0x340;
230        data[0x218..0x220].copy_from_slice(&dentry1_addr.to_le_bytes()); // mnt_mountpoint
231                                                                         // mnt.mnt_sb at offset 32
232        let sb1_addr = vaddr + 0x380;
233        data[0x220..0x228].copy_from_slice(&sb1_addr.to_le_bytes()); // mnt.mnt_sb
234
235        // devname1 string at +0x300
236        data[0x300..0x308].copy_from_slice(b"/dev/sda");
237
238        // dentry1 at +0x340: d_name.name at qstr offset 8
239        let dname1_addr = vaddr + 0x360;
240        data[0x348..0x350].copy_from_slice(&dname1_addr.to_le_bytes()); // d_name.name
241        data[0x360..0x361].copy_from_slice(b"/");
242
243        // super_block1 at +0x380: s_type pointer
244        let fstype1_addr = vaddr + 0x3C0;
245        data[0x380..0x388].copy_from_slice(&fstype1_addr.to_le_bytes());
246
247        // file_system_type1 at +0x3C0: name pointer
248        let typename1_addr = vaddr + 0x3E0;
249        data[0x3C0..0x3C8].copy_from_slice(&typename1_addr.to_le_bytes());
250        data[0x3E0..0x3E4].copy_from_slice(b"ext4");
251
252        // mount2 at +0x400: tmpfs on /tmp
253        data[0x400..0x408].copy_from_slice(&ns_list_addr.to_le_bytes()); // mnt_list.next → ns head
254        data[0x408..0x410].copy_from_slice(&mount1_list.to_le_bytes()); // mnt_list.prev → mount1
255        let devname2_addr = vaddr + 0x500;
256        data[0x410..0x418].copy_from_slice(&devname2_addr.to_le_bytes()); // mnt_devname
257        let dentry2_addr = vaddr + 0x540;
258        data[0x418..0x420].copy_from_slice(&dentry2_addr.to_le_bytes()); // mnt_mountpoint
259        let sb2_addr = vaddr + 0x580;
260        data[0x420..0x428].copy_from_slice(&sb2_addr.to_le_bytes()); // mnt.mnt_sb
261
262        // devname2 string at +0x500
263        data[0x500..0x505].copy_from_slice(b"tmpfs");
264
265        // dentry2 at +0x540
266        let dname2_addr = vaddr + 0x560;
267        data[0x548..0x550].copy_from_slice(&dname2_addr.to_le_bytes());
268        data[0x560..0x564].copy_from_slice(b"/tmp");
269
270        // super_block2 at +0x580
271        let fstype2_addr = vaddr + 0x5C0;
272        data[0x580..0x588].copy_from_slice(&fstype2_addr.to_le_bytes());
273
274        // file_system_type2 at +0x5C0
275        let typename2_addr = vaddr + 0x5E0;
276        data[0x5C0..0x5C8].copy_from_slice(&typename2_addr.to_le_bytes());
277        data[0x5E0..0x5E5].copy_from_slice(b"tmpfs");
278
279        let reader = make_test_reader(&data, vaddr, paddr);
280        let mounts = walk_filesystems(&reader).unwrap();
281
282        assert_eq!(mounts.len(), 2);
283
284        assert_eq!(mounts[0].dev_name, "/dev/sda");
285        assert_eq!(mounts[0].mount_point, "/");
286        assert_eq!(mounts[0].fs_type, "ext4");
287
288        assert_eq!(mounts[1].dev_name, "tmpfs");
289        assert_eq!(mounts[1].mount_point, "/tmp");
290        assert_eq!(mounts[1].fs_type, "tmpfs");
291    }
292
293    #[test]
294    fn walk_filesystems_null_nsproxy() {
295        let vaddr: u64 = 0xFFFF_8000_0010_0000;
296        let paddr: u64 = 0x0080_0000;
297        let mut data = vec![0u8; 4096];
298
299        // init_task with nsproxy = NULL
300        data[64..72].copy_from_slice(&0u64.to_le_bytes());
301
302        let reader = make_test_reader(&data, vaddr, paddr);
303        let result = walk_filesystems(&reader);
304        assert!(result.is_err());
305    }
306
307    #[test]
308    fn missing_init_task_symbol() {
309        let isf = IsfBuilder::new()
310            .add_struct("task_struct", 64)
311            .add_field("task_struct", "pid", 0, "int")
312            .add_field("task_struct", "tasks", 8, "list_head")
313            .add_struct("list_head", 16)
314            .add_field("list_head", "next", 0, "pointer")
315            .add_field("list_head", "prev", 8, "pointer")
316            .build_json();
317
318        let resolver = IsfResolver::from_value(&isf).unwrap();
319        let (cr3, mem) = PageTableBuilder::new().build();
320        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
321        let reader = ObjectReader::new(vas, Box::new(resolver));
322
323        let result = walk_filesystems(&reader);
324        assert!(result.is_err());
325    }
326
327    // walk_filesystems: mnt_ns_ptr == 0 → Err (exercises line 31-33).
328    #[test]
329    fn walk_filesystems_null_mnt_ns_returns_error() {
330        let vaddr: u64 = 0xFFFF_8000_0010_0000;
331        let paddr: u64 = 0x0080_0000;
332        let mut data = vec![0u8; 4096];
333
334        // nsproxy at +0x100
335        let nsproxy_addr = vaddr + 0x100;
336        data[64..72].copy_from_slice(&nsproxy_addr.to_le_bytes());
337        // nsproxy.mnt_ns at offset 16 = 0 (null)
338        data[0x110..0x118].copy_from_slice(&0u64.to_le_bytes());
339
340        let reader = make_test_reader(&data, vaddr, paddr);
341        let result = walk_filesystems(&reader);
342        assert!(result.is_err(), "null mnt_ns must return Err");
343    }
344
345    // walk_filesystems: mount with devname_ptr=0 and mountpoint_ptr=0 and sb_ptr=0
346    // → dev_name="", mount_point="", fs_type="" (exercises null-ptr branches in loop body).
347    #[test]
348    fn walk_filesystems_all_null_ptrs_in_mount() {
349        let vaddr: u64 = 0xFFFF_8000_0010_0000;
350        let paddr: u64 = 0x0080_0000;
351        let mut data = vec![0u8; 4096];
352
353        // init_task: nsproxy at +0x100
354        let nsproxy_addr = vaddr + 0x100;
355        data[64..72].copy_from_slice(&nsproxy_addr.to_le_bytes());
356
357        // nsproxy: mnt_ns at offset 16 → mnt_ns at +0x180
358        let mnt_ns_addr = vaddr + 0x180;
359        data[0x110..0x118].copy_from_slice(&mnt_ns_addr.to_le_bytes());
360
361        // mnt_namespace: list at offset 0 → head
362        // list.next → mount1, list.prev → mount1
363        let ns_list_addr = vaddr + 0x180;
364        let mount1_addr = vaddr + 0x200;
365        data[0x180..0x188].copy_from_slice(&mount1_addr.to_le_bytes()); // list.next
366        data[0x188..0x190].copy_from_slice(&mount1_addr.to_le_bytes()); // list.prev
367
368        // mount1: mnt_list.next → ns_head, mnt_list.prev → ns_head (single-entry loop)
369        data[0x200..0x208].copy_from_slice(&ns_list_addr.to_le_bytes()); // mnt_list.next
370        data[0x208..0x210].copy_from_slice(&ns_list_addr.to_le_bytes()); // mnt_list.prev
371                                                                         // mnt_devname at offset 16 = 0 (null)
372        data[0x210..0x218].copy_from_slice(&0u64.to_le_bytes());
373        // mnt_mountpoint at offset 24 = 0 (null)
374        data[0x218..0x220].copy_from_slice(&0u64.to_le_bytes());
375        // mnt.mnt_sb at offset 32 (= mount.mnt at offset 32 + vfsmount.mnt_sb at offset 0) = 0
376        data[0x220..0x228].copy_from_slice(&0u64.to_le_bytes());
377
378        let reader = make_test_reader(&data, vaddr, paddr);
379        let mounts = walk_filesystems(&reader).unwrap();
380
381        assert_eq!(mounts.len(), 1, "one mount entry expected");
382        assert_eq!(mounts[0].dev_name, "", "null devname_ptr → empty string");
383        assert_eq!(
384            mounts[0].mount_point, "",
385            "null mountpoint_ptr → empty string"
386        );
387        assert_eq!(mounts[0].fs_type, "", "null sb_ptr → empty string");
388    }
389
390    // read_fs_type_name: s_type_ptr == 0 → fs_type = "" (exercises line 114-115).
391    #[test]
392    fn walk_filesystems_null_s_type_gives_empty_fs_type() {
393        let vaddr: u64 = 0xFFFF_8000_0010_0000;
394        let paddr: u64 = 0x0080_0000;
395        let mut data = vec![0u8; 4096];
396
397        let nsproxy_addr = vaddr + 0x100;
398        data[64..72].copy_from_slice(&nsproxy_addr.to_le_bytes());
399
400        let mnt_ns_addr = vaddr + 0x180;
401        data[0x110..0x118].copy_from_slice(&mnt_ns_addr.to_le_bytes());
402
403        let ns_list_addr = vaddr + 0x180;
404        let mount1_addr = vaddr + 0x200;
405        data[0x180..0x188].copy_from_slice(&mount1_addr.to_le_bytes());
406        data[0x188..0x190].copy_from_slice(&mount1_addr.to_le_bytes());
407
408        data[0x200..0x208].copy_from_slice(&ns_list_addr.to_le_bytes());
409        data[0x208..0x210].copy_from_slice(&ns_list_addr.to_le_bytes());
410        // devname_ptr = 0, mountpoint_ptr = 0
411        data[0x210..0x218].copy_from_slice(&0u64.to_le_bytes());
412        data[0x218..0x220].copy_from_slice(&0u64.to_le_bytes());
413        // mnt_sb points to a super_block at +0x380; super_block.s_type = 0
414        let sb_addr = vaddr + 0x380;
415        data[0x220..0x228].copy_from_slice(&sb_addr.to_le_bytes());
416        // super_block.s_type at offset 0 = 0 (null)
417        data[0x380..0x388].copy_from_slice(&0u64.to_le_bytes());
418
419        let reader = make_test_reader(&data, vaddr, paddr);
420        let mounts = walk_filesystems(&reader).unwrap();
421
422        assert_eq!(mounts.len(), 1);
423        assert_eq!(mounts[0].fs_type, "", "null s_type_ptr → empty fs_type");
424    }
425
426    // read_fs_type_name: name_ptr == 0 → fs_type = "" (exercises line 117-119).
427    #[test]
428    fn walk_filesystems_null_name_ptr_gives_empty_fs_type() {
429        let vaddr: u64 = 0xFFFF_8000_0010_0000;
430        let paddr: u64 = 0x0080_0000;
431        let mut data = vec![0u8; 4096];
432
433        let nsproxy_addr = vaddr + 0x100;
434        data[64..72].copy_from_slice(&nsproxy_addr.to_le_bytes());
435
436        let mnt_ns_addr = vaddr + 0x180;
437        data[0x110..0x118].copy_from_slice(&mnt_ns_addr.to_le_bytes());
438
439        let ns_list_addr = vaddr + 0x180;
440        let mount1_addr = vaddr + 0x200;
441        data[0x180..0x188].copy_from_slice(&mount1_addr.to_le_bytes());
442        data[0x188..0x190].copy_from_slice(&mount1_addr.to_le_bytes());
443
444        data[0x200..0x208].copy_from_slice(&ns_list_addr.to_le_bytes());
445        data[0x208..0x210].copy_from_slice(&ns_list_addr.to_le_bytes());
446        data[0x210..0x218].copy_from_slice(&0u64.to_le_bytes()); // devname = null
447        data[0x218..0x220].copy_from_slice(&0u64.to_le_bytes()); // mountpoint = null
448                                                                 // sb at +0x380 with valid s_type ptr → file_system_type at +0x3C0 with name_ptr = 0
449        let sb_addr = vaddr + 0x380;
450        data[0x220..0x228].copy_from_slice(&sb_addr.to_le_bytes());
451        let fstype_addr = vaddr + 0x3C0;
452        data[0x380..0x388].copy_from_slice(&fstype_addr.to_le_bytes()); // s_type
453                                                                        // file_system_type.name at offset 0 = 0 (null)
454        data[0x3C0..0x3C8].copy_from_slice(&0u64.to_le_bytes());
455
456        let reader = make_test_reader(&data, vaddr, paddr);
457        let mounts = walk_filesystems(&reader).unwrap();
458
459        assert_eq!(mounts.len(), 1);
460        assert_eq!(
461            mounts[0].fs_type, "",
462            "null name_ptr in file_system_type → empty fs_type"
463        );
464    }
465
466    // MountInfo struct: Debug + Clone.
467    #[test]
468    fn mount_info_debug_clone() {
469        let m = MountInfo {
470            dev_name: "/dev/sda".to_string(),
471            mount_point: "/".to_string(),
472            fs_type: "ext4".to_string(),
473        };
474        let cloned = m.clone();
475        let dbg = format!("{cloned:?}");
476        assert!(dbg.contains("ext4"));
477    }
478}