Skip to content

Commit 7faf1db

Browse files
committed
df: Add a mount_dev_id to use id's instead of paths
1 parent 7b5cdc8 commit 7faf1db

File tree

4 files changed

+160
-5
lines changed

4 files changed

+160
-5
lines changed

src/uu/df/src/df.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,8 @@ mod tests {
638638
mount_root: mount_root.into(),
639639
remote: false,
640640
dummy: false,
641+
#[cfg(any(target_os = "linux", target_os = "android"))]
642+
mount_dev_id: String::new(),
641643
}
642644
}
643645

@@ -688,6 +690,8 @@ mod tests {
688690
mount_root: "/".into(),
689691
remote: false,
690692
dummy: false,
693+
#[cfg(any(target_os = "linux", target_os = "android"))]
694+
mount_dev_id: String::new(),
691695
}
692696
}
693697

@@ -733,6 +737,8 @@ mod tests {
733737
mount_root: "/".into(),
734738
remote,
735739
dummy,
740+
#[cfg(any(target_os = "linux", target_os = "android"))]
741+
mount_dev_id: String::new(),
736742
}
737743
}
738744

src/uu/df/src/filesystem.rs

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,32 @@ pub(crate) enum FsError {
4545
MountMissing,
4646
}
4747

48+
// spell-checker:ignore (fs) autofs binfmt
49+
4850
/// Check whether `mount` has been over-mounted.
4951
///
50-
/// `mount` is considered over-mounted if it there is an element in
51-
/// `mounts` after mount that has the same `mount_dir`.
52-
#[cfg(not(windows))]
52+
/// `mount` is considered over-mounted if there is a later element in
53+
/// `mounts` that has the same `mount_dir` AND the same `mount_dev_id`.
54+
/// Mounts with different mountinfo device IDs at the same path are considered
55+
/// separate filesystems (e.g., autofs trigger mounts like systemd-1 + binfmt_misc
56+
/// at /proc/sys/fs/binfmt_misc) and both should be shown.
57+
#[cfg(all(not(windows), any(target_os = "linux", target_os = "android")))]
58+
fn is_over_mounted(mounts: &[MountInfo], mount: &MountInfo) -> bool {
59+
let last_mount_for_dir = mounts
60+
.iter()
61+
.rfind(|m| m.mount_dir == mount.mount_dir && m.mount_dev_id == mount.mount_dev_id);
62+
63+
if let Some(lmi) = last_mount_for_dir {
64+
lmi.dev_name != mount.dev_name
65+
} else {
66+
// No mount with same dir AND same mount_dev_id found after this one,
67+
// so it's not over-mounted
68+
false
69+
}
70+
}
71+
72+
/// Check whether `mount` has been over-mounted (non-Linux platforms).
73+
#[cfg(all(not(windows), not(any(target_os = "linux", target_os = "android"))))]
5374
fn is_over_mounted(mounts: &[MountInfo], mount: &MountInfo) -> bool {
5475
let last_mount_for_dir = mounts.iter().rfind(|m| m.mount_dir == mount.mount_dir);
5576

@@ -219,6 +240,8 @@ mod tests {
219240
mount_root: OsString::default(),
220241
remote: Default::default(),
221242
dummy: Default::default(),
243+
#[cfg(any(target_os = "linux", target_os = "android"))]
244+
mount_dev_id: String::default(),
222245
}
223246
}
224247

@@ -324,6 +347,27 @@ mod tests {
324347
mount_root: OsString::default(),
325348
remote: Default::default(),
326349
dummy: Default::default(),
350+
#[cfg(any(target_os = "linux", target_os = "android"))]
351+
mount_dev_id: String::default(),
352+
}
353+
}
354+
355+
#[cfg(any(target_os = "linux", target_os = "android"))]
356+
fn mount_info_full(
357+
mount_dir: &str,
358+
dev_name: Option<&str>,
359+
mount_dev_id: Option<&str>,
360+
) -> MountInfo {
361+
MountInfo {
362+
dev_id: String::default(),
363+
dev_name: dev_name.map(String::from).unwrap_or_default(),
364+
fs_type: String::default(),
365+
mount_dir: OsString::from(mount_dir),
366+
mount_option: String::default(),
367+
mount_root: OsString::default(),
368+
remote: Default::default(),
369+
dummy: Default::default(),
370+
mount_dev_id: mount_dev_id.map(String::from).unwrap_or_default(),
327371
}
328372
}
329373

@@ -355,5 +399,35 @@ mod tests {
355399
FsError::OverMounted
356400
);
357401
}
402+
403+
/// Test for issue #9952: Two mounts at the same path with different device IDs
404+
/// (like autofs + binfmt_misc at /proc/sys/fs/binfmt_misc) should NOT be
405+
/// considered over-mounted. They are separate filesystems that happen to be
406+
/// stacked at the same mount point.
407+
#[test]
408+
#[cfg(any(target_os = "linux", target_os = "android"))]
409+
fn test_different_dev_ids_not_over_mounted() {
410+
// Simulates: systemd-1 (autofs, dev 0:33) and binfmt_misc (dev 0:46)
411+
// both at /proc/sys/fs/binfmt_misc
412+
let mount_info1 =
413+
mount_info_full("/proc/sys/fs/binfmt_misc", Some("systemd-1"), Some("0:33"));
414+
let mount_info2 = mount_info_full(
415+
"/proc/sys/fs/binfmt_misc",
416+
Some("binfmt_misc"),
417+
Some("0:46"),
418+
);
419+
let mounts = [mount_info1, mount_info2];
420+
421+
// With different device IDs, the first mount should NOT be over-mounted
422+
assert!(
423+
!is_over_mounted(&mounts, &mounts[0]),
424+
"Mount with different dev_id should not be considered over-mounted"
425+
);
426+
// And neither should the second one
427+
assert!(
428+
!is_over_mounted(&mounts, &mounts[1]),
429+
"Last mount at path should not be considered over-mounted"
430+
);
431+
}
358432
}
359433
}

src/uu/df/src/table.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,8 @@ mod tests {
950950
mount_root: "/".into(),
951951
remote: false,
952952
dummy: false,
953+
#[cfg(any(target_os = "linux", target_os = "android"))]
954+
mount_dev_id: String::new(),
953955
},
954956
usage: crate::table::FsUsage {
955957
blocksize: 4096,
@@ -981,6 +983,8 @@ mod tests {
981983
mount_root: "/".into(),
982984
remote: false,
983985
dummy: false,
986+
#[cfg(any(target_os = "linux", target_os = "android"))]
987+
mount_dev_id: String::new(),
984988
},
985989
usage: crate::table::FsUsage {
986990
blocksize: 4096,
@@ -1052,6 +1056,8 @@ mod tests {
10521056
mount_root: "/".into(),
10531057
remote: false,
10541058
dummy: false,
1059+
#[cfg(any(target_os = "linux", target_os = "android"))]
1060+
mount_dev_id: String::new(),
10551061
},
10561062
usage: crate::table::FsUsage {
10571063
blocksize: 4096,

src/uucore/src/lib/features/fsext.rs

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
//! Set of functions to manage file systems
77
8-
// spell-checker:ignore DATETIME getmntinfo subsecond (fs) cifs smbfs
8+
// spell-checker:ignore DATETIME getmntinfo subsecond (fs) cifs smbfs autofs binfmt
99

1010
#[cfg(any(target_os = "linux", target_os = "android", target_os = "cygwin"))]
1111
const LINUX_MTAB: &str = "/etc/mtab";
@@ -189,6 +189,12 @@ pub struct MountInfo {
189189
pub mount_option: String,
190190
pub remote: bool,
191191
pub dummy: bool,
192+
/// The device ID from mountinfo (major:minor format like "0:33").
193+
/// This preserves the actual device ID from /proc/self/mountinfo,
194+
/// which can differ from the stat-based dev_id for stacked mounts
195+
/// (e.g., autofs + binfmt_misc at the same mount point).
196+
#[cfg(any(target_os = "linux", target_os = "android"))]
197+
pub mount_dev_id: String,
192198
}
193199

194200
#[cfg(any(target_os = "linux", target_os = "android", target_os = "cygwin"))]
@@ -207,7 +213,70 @@ fn replace_special_chars(s: &[u8]) -> Vec<u8> {
207213
}
208214

209215
impl MountInfo {
210-
#[cfg(any(target_os = "linux", target_os = "android", target_os = "cygwin"))]
216+
#[cfg(any(target_os = "linux", target_os = "android"))]
217+
fn new(file_name: &str, raw: &[&[u8]]) -> Option<Self> {
218+
use std::ffi::OsStr;
219+
use std::os::unix::ffi::OsStrExt;
220+
use std::os::unix::ffi::OsStringExt;
221+
222+
let dev_name;
223+
let fs_type;
224+
let mount_root;
225+
let mount_dir;
226+
let mount_option;
227+
let mountinfo_dev_id;
228+
229+
match file_name {
230+
// spell-checker:ignore (word) noatime
231+
// Format: 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
232+
// "man proc" for more details
233+
LINUX_MOUNTINFO => {
234+
const FIELDS_OFFSET: usize = 6;
235+
let after_fields = raw[FIELDS_OFFSET..]
236+
.iter()
237+
.position(|c| *c == b"-")
238+
.unwrap()
239+
+ FIELDS_OFFSET
240+
+ 1;
241+
dev_name = String::from_utf8_lossy(raw[after_fields + 1]).to_string();
242+
fs_type = String::from_utf8_lossy(raw[after_fields]).to_string();
243+
mount_root = OsStr::from_bytes(raw[3]).to_owned();
244+
mount_dir = OsString::from_vec(replace_special_chars(raw[4]));
245+
mount_option = String::from_utf8_lossy(raw[5]).to_string();
246+
// Store the device ID from mountinfo (major:minor format like "0:33")
247+
// This is used to distinguish different filesystems at the same mount point
248+
mountinfo_dev_id = String::from_utf8_lossy(raw[2]).to_string();
249+
}
250+
LINUX_MTAB => {
251+
dev_name = String::from_utf8_lossy(raw[0]).to_string();
252+
fs_type = String::from_utf8_lossy(raw[2]).to_string();
253+
mount_root = OsString::new();
254+
mount_dir = OsString::from_vec(replace_special_chars(raw[1]));
255+
mount_option = String::from_utf8_lossy(raw[3]).to_string();
256+
// mtab doesn't have the mountinfo device ID, use empty string
257+
mountinfo_dev_id = String::new();
258+
}
259+
_ => return None,
260+
}
261+
262+
let dev_id = mount_dev_id(&mount_dir);
263+
let dummy = is_dummy_filesystem(&fs_type, &mount_option);
264+
let remote = is_remote_filesystem(&dev_name, &fs_type);
265+
266+
Some(Self {
267+
dev_id,
268+
dev_name,
269+
fs_type,
270+
mount_root,
271+
mount_dir,
272+
mount_option,
273+
remote,
274+
dummy,
275+
mount_dev_id: mountinfo_dev_id,
276+
})
277+
}
278+
279+
#[cfg(target_os = "cygwin")]
211280
fn new(file_name: &str, raw: &[&[u8]]) -> Option<Self> {
212281
use std::ffi::OsStr;
213282
use std::os::unix::ffi::OsStrExt;

0 commit comments

Comments
 (0)