1#![allow(clippy::expect_used)]
15use std::collections::HashMap;
16use std::sync::OnceLock;
17use tracing::{debug, warn};
18
19#[derive(Debug, Clone)]
25pub struct MountEntry {
26 pub mount_id: u64,
28 pub parent_id: u64,
30 pub dev_major: u32,
32 pub dev_minor: u32,
34 pub root: String,
36 pub mount_point: String,
38 pub mount_options: String,
40 pub fs_type: String,
42 pub source: String,
44 pub super_options: String,
46}
47
48#[cfg(any(
54 target_arch = "x86_64",
55 target_arch = "aarch64",
56 target_arch = "s390x",
57 target_arch = "powerpc64"
58))]
59const SYS_STATMOUNT: libc::c_long = 457;
60
61#[cfg(any(
63 target_arch = "x86_64",
64 target_arch = "aarch64",
65 target_arch = "s390x",
66 target_arch = "powerpc64"
67))]
68const SYS_LISTMOUNT: libc::c_long = 458;
69
70const STATMOUNT_SB_BASIC: u64 = 0x0000_0001;
76const STATMOUNT_MNT_BASIC: u64 = 0x0000_0002;
78const STATMOUNT_MNT_ROOT: u64 = 0x0000_0008;
80const STATMOUNT_MNT_POINT: u64 = 0x0000_0010;
82const STATMOUNT_FS_TYPE: u64 = 0x0000_0020;
84const STATMOUNT_MNT_OPTS: u64 = 0x0000_0080;
86const STATMOUNT_SB_SOURCE: u64 = 0x0000_0200;
88
89const LSMT_ROOT: u64 = 0xffff_ffff_ffff_ffff;
91
92const MNT_ID_REQ_SIZE_VER0: u32 = 24;
94
95const OFF_SIZE: usize = 0; const OFF_MNT_OPTS_STR: usize = 4; const OFF_MASK: usize = 8; const OFF_SB_DEV_MAJOR: usize = 16; const OFF_SB_DEV_MINOR: usize = 20; const OFF_FS_TYPE_STR: usize = 36; const _OFF_MNT_ID: usize = 40; const _OFF_MNT_PARENT_ID: usize = 48; const OFF_MNT_ID_OLD: usize = 56; const OFF_MNT_PARENT_ID_OLD: usize = 60; const OFF_MNT_ROOT_STR: usize = 104; const OFF_MNT_POINT_STR: usize = 108; const OFF_SB_SOURCE_STR: usize = 124; const OFF_STR: usize = 512;
116
117#[repr(C)]
126struct MntIdReq {
127 size: u32,
128 spare: u32,
129 mnt_id: u64,
130 param: u64,
131}
132
133static STATMOUNT_AVAILABLE: OnceLock<bool> = OnceLock::new();
138
139#[cfg(any(
140 target_arch = "x86_64",
141 target_arch = "aarch64",
142 target_arch = "s390x",
143 target_arch = "powerpc64"
144))]
145fn probe_statmount() -> bool {
146 *STATMOUNT_AVAILABLE.get_or_init(|| {
147 let ret = unsafe {
150 libc::syscall(
151 SYS_STATMOUNT,
152 std::ptr::null::<u8>(),
153 std::ptr::null::<u8>(),
154 0usize,
155 0u32,
156 )
157 };
158 let available = if ret == -1 {
159 let errno = unsafe { *libc::__errno_location() };
160 errno != libc::ENOSYS
161 } else {
162 true };
164 if available {
165 debug!("statmount(2)/listmount(2) available -- using kernel mount API");
166 } else {
167 debug!("statmount(2) not available (ENOSYS) -- using /proc/self/mountinfo");
168 }
169 available
170 })
171}
172
173#[cfg(not(any(
174 target_arch = "x86_64",
175 target_arch = "aarch64",
176 target_arch = "s390x",
177 target_arch = "powerpc64"
178)))]
179fn probe_statmount() -> bool {
180 false
181}
182
183#[inline]
189fn read_u32(buf: &[u8], off: usize) -> u32 {
190 let bytes: [u8; 4] = buf[off..off + 4].try_into().expect("slice too short for u32");
191 u32::from_ne_bytes(bytes)
192}
193
194#[inline]
196fn read_u64(buf: &[u8], off: usize) -> u64 {
197 let bytes: [u8; 8] = buf[off..off + 8].try_into().expect("slice too short for u64");
198 u64::from_ne_bytes(bytes)
199}
200
201fn read_statmount_str(buf: &[u8], str_off: u32) -> String {
203 let start = OFF_STR + str_off as usize;
204 if start >= buf.len() {
205 return String::new();
206 }
207 let remaining = &buf[start..];
208 let end = remaining
209 .iter()
210 .position(|&b| b == 0)
211 .unwrap_or(remaining.len());
212 String::from_utf8_lossy(&remaining[..end]).into_owned()
213}
214
215#[cfg(any(
220 target_arch = "x86_64",
221 target_arch = "aarch64",
222 target_arch = "s390x",
223 target_arch = "powerpc64"
224))]
225fn listmount_ids() -> Option<Vec<u64>> {
226 let mut all_ids = Vec::new();
227 let mut last_id: u64 = 0;
228
229 loop {
230 let req = MntIdReq {
231 size: MNT_ID_REQ_SIZE_VER0,
232 spare: 0,
233 mnt_id: LSMT_ROOT,
234 param: last_id,
235 };
236 let mut ids = [0u64; 1024];
237
238 let ret = unsafe {
241 libc::syscall(
242 SYS_LISTMOUNT,
243 &req as *const MntIdReq,
244 ids.as_mut_ptr(),
245 ids.len(),
246 0u32,
247 )
248 };
249
250 if ret < 0 {
251 let errno = unsafe { *libc::__errno_location() };
252 warn!("listmount(2) failed: errno={errno}");
253 return None;
254 }
255
256 let count = ret as usize;
257 if count == 0 {
258 break;
259 }
260
261 all_ids.extend_from_slice(&ids[..count]);
262
263 if count < ids.len() {
264 break; }
266
267 last_id = ids[count - 1];
269 }
270
271 Some(all_ids)
272}
273
274#[cfg(any(
275 target_arch = "x86_64",
276 target_arch = "aarch64",
277 target_arch = "s390x",
278 target_arch = "powerpc64"
279))]
280fn statmount_entry(mnt_id: u64) -> Option<MountEntry> {
281 let mask = STATMOUNT_SB_BASIC
282 | STATMOUNT_MNT_BASIC
283 | STATMOUNT_MNT_ROOT
284 | STATMOUNT_MNT_POINT
285 | STATMOUNT_FS_TYPE
286 | STATMOUNT_MNT_OPTS
287 | STATMOUNT_SB_SOURCE; let req = MntIdReq {
290 size: MNT_ID_REQ_SIZE_VER0,
291 spare: 0,
292 mnt_id,
293 param: mask,
294 };
295
296 let mut buf = vec![0u8; 4096];
298
299 let ret = unsafe {
302 libc::syscall(
303 SYS_STATMOUNT,
304 &req as *const MntIdReq,
305 buf.as_mut_ptr(),
306 buf.len(),
307 0u32,
308 )
309 };
310
311 if ret < 0 {
312 let errno = unsafe { *libc::__errno_location() };
313 if errno == libc::EOVERFLOW {
314 let needed = read_u32(&buf, OFF_SIZE) as usize;
316 if needed > 0 && needed <= 1 << 20 {
317 buf.resize(needed, 0);
318 let ret2 = unsafe {
319 libc::syscall(
320 SYS_STATMOUNT,
321 &req as *const MntIdReq,
322 buf.as_mut_ptr(),
323 buf.len(),
324 0u32,
325 )
326 };
327 if ret2 < 0 {
328 return None;
329 }
330 } else {
331 return None;
332 }
333 } else {
334 return None;
336 }
337 }
338
339 if buf.len() < OFF_STR {
340 return None;
341 }
342
343 let filled = read_u64(&buf, OFF_MASK);
344
345 let dev_major = if filled & STATMOUNT_SB_BASIC != 0 {
347 read_u32(&buf, OFF_SB_DEV_MAJOR)
348 } else {
349 0
350 };
351 let dev_minor = if filled & STATMOUNT_SB_BASIC != 0 {
352 read_u32(&buf, OFF_SB_DEV_MINOR)
353 } else {
354 0
355 };
356
357 let mount_id = if filled & STATMOUNT_MNT_BASIC != 0 {
359 read_u32(&buf, OFF_MNT_ID_OLD) as u64
360 } else {
361 0
362 };
363 let parent_id = if filled & STATMOUNT_MNT_BASIC != 0 {
364 read_u32(&buf, OFF_MNT_PARENT_ID_OLD) as u64
365 } else {
366 0
367 };
368
369 let root = if filled & STATMOUNT_MNT_ROOT != 0 {
371 read_statmount_str(&buf, read_u32(&buf, OFF_MNT_ROOT_STR))
372 } else {
373 String::new()
374 };
375 let mount_point = if filled & STATMOUNT_MNT_POINT != 0 {
376 read_statmount_str(&buf, read_u32(&buf, OFF_MNT_POINT_STR))
377 } else {
378 String::new()
379 };
380 let fs_type = if filled & STATMOUNT_FS_TYPE != 0 {
381 read_statmount_str(&buf, read_u32(&buf, OFF_FS_TYPE_STR))
382 } else {
383 String::new()
384 };
385
386 let mnt_opts = if filled & STATMOUNT_MNT_OPTS != 0 {
390 read_statmount_str(&buf, read_u32(&buf, OFF_MNT_OPTS_STR))
391 } else {
392 String::new()
393 };
394
395 let source = if filled & STATMOUNT_SB_SOURCE != 0 {
400 read_statmount_str(&buf, read_u32(&buf, OFF_SB_SOURCE_STR))
401 } else {
402 String::new()
403 };
404
405 Some(MountEntry {
406 mount_id,
407 parent_id,
408 dev_major,
409 dev_minor,
410 root,
411 mount_point,
412 mount_options: String::new(), fs_type,
414 source,
415 super_options: mnt_opts, })
417}
418
419#[cfg(any(
420 target_arch = "x86_64",
421 target_arch = "aarch64",
422 target_arch = "s390x",
423 target_arch = "powerpc64"
424))]
425fn enrich_from_proc(entries: &mut [MountEntry]) {
426 let proc_entries = list_mounts_proc();
427 let proc_map: HashMap<u64, &MountEntry> = proc_entries
428 .iter()
429 .map(|e| (e.mount_id, e))
430 .collect();
431
432 for entry in entries.iter_mut() {
433 if let Some(pe) = proc_map.get(&entry.mount_id) {
434 if entry.source.is_empty() {
435 entry.source.clone_from(&pe.source);
436 }
437 if entry.mount_options.is_empty() {
438 entry.mount_options.clone_from(&pe.mount_options);
439 }
440 if entry.super_options.is_empty() {
441 entry.super_options.clone_from(&pe.super_options);
442 }
443 }
444 }
445}
446
447#[cfg(any(
448 target_arch = "x86_64",
449 target_arch = "aarch64",
450 target_arch = "s390x",
451 target_arch = "powerpc64"
452))]
453fn list_mounts_statmount() -> Option<Vec<MountEntry>> {
454 let ids = listmount_ids()?;
455 let mut entries = Vec::with_capacity(ids.len());
456
457 for id in ids {
458 if let Some(entry) = statmount_entry(id) {
459 entries.push(entry);
460 }
461 }
462
463 let needs_enrichment = entries
465 .iter()
466 .any(|e| e.source.is_empty() || e.mount_options.is_empty());
467 if needs_enrichment {
468 enrich_from_proc(&mut entries);
469 }
470
471 Some(entries)
472}
473
474pub fn list_mounts_proc() -> Vec<MountEntry> {
480 parse_mountinfo_text(
481 &std::fs::read_to_string("/proc/self/mountinfo").unwrap_or_default(),
482 )
483}
484
485fn parse_mountinfo_text(text: &str) -> Vec<MountEntry> {
492 let mut entries = Vec::new();
493
494 for line in text.lines() {
495 let fields: Vec<&str> = line.split_whitespace().collect();
496 if fields.len() < 10 {
497 continue;
498 }
499
500 let mount_id: u64 = match fields[0].parse() {
501 Ok(v) => v,
502 Err(_) => continue,
503 };
504 let parent_id: u64 = match fields[1].parse() {
505 Ok(v) => v,
506 Err(_) => continue,
507 };
508
509 let (dev_major, dev_minor) = match fields[2].split_once(':') {
511 Some((maj, min)) => match (maj.parse::<u32>(), min.parse::<u32>()) {
512 (Ok(ma), Ok(mi)) => (ma, mi),
513 _ => continue,
514 },
515 None => continue,
516 };
517
518 let root = fields[3].to_string();
519 let mount_point = fields[4].to_string();
520 let mount_options = fields[5].to_string();
521
522 let sep_pos = match fields.iter().position(|&f| f == "-") {
524 Some(p) => p,
525 None => continue,
526 };
527 if sep_pos + 3 > fields.len() {
528 continue;
529 }
530
531 let fs_type = fields[sep_pos + 1].to_string();
532 let source = fields[sep_pos + 2].to_string();
533 let super_options = if sep_pos + 3 < fields.len() {
534 fields[sep_pos + 3].to_string()
535 } else {
536 String::new()
537 };
538
539 entries.push(MountEntry {
540 mount_id,
541 parent_id,
542 dev_major,
543 dev_minor,
544 root,
545 mount_point,
546 mount_options,
547 fs_type,
548 source,
549 super_options,
550 });
551 }
552
553 entries
554}
555
556pub fn list_mounts() -> Vec<MountEntry> {
565 #[cfg(any(
566 target_arch = "x86_64",
567 target_arch = "aarch64",
568 target_arch = "s390x",
569 target_arch = "powerpc64"
570 ))]
571 if probe_statmount() {
572 if let Some(entries) = list_mounts_statmount() {
573 return entries;
574 }
575 warn!("statmount enumeration failed -- falling back to /proc/self/mountinfo");
576 }
577 list_mounts_proc()
578}
579
580pub fn find_mount_for_path(path: &std::path::Path) -> Option<MountEntry> {
585 let canonical = path.canonicalize().ok()?;
586 let canonical_str = canonical.to_string_lossy();
587 let entries = list_mounts();
588
589 entries
590 .into_iter()
591 .filter(|e| canonical_str.starts_with(&e.mount_point) || e.mount_point == "/")
592 .max_by_key(|e| {
593 if e.mount_point == "/" {
594 0
595 } else {
596 e.mount_point.len()
597 }
598 })
599}
600
601#[cfg(test)]
606mod tests {
607 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
608 use super::*;
609
610 #[test]
611 fn test_probe_statmount_does_not_panic() {
612 let available = probe_statmount();
613 println!("statmount(2) available: {available}");
614 }
615
616 #[test]
617 fn test_list_mounts_returns_entries() {
618 let entries = list_mounts();
619 assert!(
620 !entries.is_empty(),
621 "list_mounts() should return at least one mount"
622 );
623
624 let has_root = entries.iter().any(|e| e.mount_point == "/");
626 assert!(has_root, "root mount (/) must be present");
627
628 for e in &entries {
630 assert!(!e.mount_point.is_empty(), "mount_point empty: {e:?}");
631 assert!(!e.fs_type.is_empty(), "fs_type empty: {e:?}");
632 }
633
634 println!("list_mounts() returned {} entries", entries.len());
635 }
636
637 #[test]
638 fn test_list_mounts_proc_returns_entries() {
639 let entries = list_mounts_proc();
640 assert!(
641 !entries.is_empty(),
642 "proc fallback should return entries"
643 );
644 assert!(entries.iter().any(|e| e.mount_point == "/"));
645 }
646
647 #[test]
648 fn test_statmount_vs_proc_field_counts() {
649 let proc_entries = list_mounts_proc();
651 let proc_count = proc_entries.len();
652
653 if probe_statmount() {
654 if let Some(sm_entries) = list_mounts_statmount() {
655 let sm_count = sm_entries.len();
656 println!(
657 "statmount: {sm_count} entries, /proc/self/mountinfo: {proc_count} entries"
658 );
659 let diff = (sm_count as i64 - proc_count as i64).unsigned_abs();
661 assert!(
662 diff <= 5,
663 "statmount ({sm_count}) vs proc ({proc_count}) differ by more than 5"
664 );
665 } else {
666 println!("statmount probe succeeded but enumeration failed -- skipping comparison");
667 }
668 } else {
669 println!("statmount not available -- skipping comparison test");
670 }
671 }
672
673 #[test]
674 fn test_parse_mountinfo_text() {
675 let text = "\
67622 1 8:1 / / rw,relatime shared:1 - ext4 /dev/sda1 rw,errors=continue
67729 22 0:25 / /sys rw,nosuid,nodev,noexec,relatime shared:7 - sysfs sysfs rw
67830 22 0:5 / /dev rw,nosuid shared:2 - devtmpfs devtmpfs rw,size=4096k
679100 22 0:50 / /mnt/nfs rw,relatime shared:50 - nfs4 server:/export rw,vers=4.2,addr=10.0.0.1
680";
681 let entries = parse_mountinfo_text(text);
682 assert_eq!(entries.len(), 4);
683
684 assert_eq!(entries[0].mount_id, 22);
685 assert_eq!(entries[0].parent_id, 1);
686 assert_eq!(entries[0].dev_major, 8);
687 assert_eq!(entries[0].dev_minor, 1);
688 assert_eq!(entries[0].root, "/");
689 assert_eq!(entries[0].mount_point, "/");
690 assert_eq!(entries[0].mount_options, "rw,relatime");
691 assert_eq!(entries[0].fs_type, "ext4");
692 assert_eq!(entries[0].source, "/dev/sda1");
693 assert_eq!(entries[0].super_options, "rw,errors=continue");
694
695 assert_eq!(entries[3].fs_type, "nfs4");
697 assert_eq!(entries[3].source, "server:/export");
698 assert!(entries[3].super_options.contains("vers=4.2"));
699 }
700
701 #[test]
702 fn test_find_mount_for_path() {
703 let entry = find_mount_for_path(std::path::Path::new("/"));
704 assert!(entry.is_some(), "should find mount for /");
705 assert_eq!(entry.unwrap().mount_point, "/");
706 }
707}