Skip to content

Commit 3c16e36

Browse files
committed
df: use statfs fallback when mount table is unavailable
1 parent cbbff30 commit 3c16e36

File tree

3 files changed

+161
-4
lines changed

3 files changed

+161
-4
lines changed

src/uu/df/src/df.rs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use uucore::error::{UError, UResult, USimpleError, get_exit_code};
1616
use uucore::fsext::{MountInfo, read_fs_list};
1717
use uucore::parser::parse_size::ParseSizeError;
1818
use uucore::translate;
19-
use uucore::{format_usage, show};
19+
use uucore::{format_usage, show, show_warning};
2020

2121
use clap::{Arg, ArgAction, ArgMatches, Command, parser::ValueSource};
2222

@@ -111,6 +111,13 @@ impl Default for Options {
111111
}
112112
}
113113

114+
impl Options {
115+
/// Whether -a, -l, -t, or -x options require the mount table.
116+
fn requires_mount_table(&self) -> bool {
117+
self.show_all_fs || self.show_local_fs || self.include.is_some() || self.exclude.is_some()
118+
}
119+
}
120+
114121
#[derive(Debug, Error)]
115122
enum OptionsError {
116123
// TODO This needs to vary based on whether `--block-size`
@@ -358,14 +365,37 @@ where
358365
P: AsRef<Path>,
359366
{
360367
// The list of all mounted filesystems.
361-
let mounts: Vec<MountInfo> = read_fs_list()?;
368+
let mounts_result = read_fs_list();
369+
370+
let (mounts, use_fallback) = match mounts_result {
371+
Ok(m) => (m, false),
372+
Err(e) => {
373+
if opt.requires_mount_table() {
374+
return Err(e);
375+
}
376+
show_warning!(
377+
"{}",
378+
translate!("df-error-cannot-read-table-of-mounted-filesystems")
379+
);
380+
(vec![], true)
381+
}
382+
};
362383

363384
let mut result = vec![];
364385

365386
// Convert each path into a `Filesystem`, which contains
366387
// both the mount information and usage information.
367388
for path in paths {
368-
match Filesystem::from_path(&mounts, path) {
389+
#[cfg(unix)]
390+
let fs_result = if use_fallback {
391+
Filesystem::from_path_direct(path)
392+
} else {
393+
Filesystem::from_path(&mounts, path)
394+
};
395+
#[cfg(not(unix))]
396+
let fs_result = Filesystem::from_path(&mounts, path);
397+
398+
match fs_result {
369399
Ok(fs) => {
370400
if is_included(&fs.mount_info, opt) {
371401
result.push(fs);

src/uu/df/src/filesystem.rs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,15 @@
88
//! filesystem mounted at a particular directory. It also includes
99
//! information on amount of space available and amount of space used.
1010
// spell-checker:ignore canonicalized
11+
use std::io;
12+
use std::path::PathBuf;
1113
use std::{ffi::OsString, path::Path};
1214

1315
#[cfg(unix)]
14-
use uucore::fsext::statfs;
16+
use std::os::unix::fs::MetadataExt;
17+
18+
#[cfg(unix)]
19+
use uucore::fsext::{FsMeta, pretty_fstype, statfs};
1520
use uucore::fsext::{FsUsage, MountInfo};
1621

1722
/// Summary representation of a filesystem.
@@ -61,6 +66,31 @@ fn is_over_mounted(mounts: &[MountInfo], mount: &MountInfo) -> bool {
6166
}
6267
}
6368

69+
/// Find mount point by walking up the directory tree until device ID changes.
70+
#[cfg(unix)]
71+
pub(crate) fn find_mount_point<P: AsRef<Path>>(path: P) -> io::Result<PathBuf> {
72+
let mut current = path.as_ref().canonicalize()?;
73+
let current_dev = current.metadata()?.dev();
74+
75+
loop {
76+
let parent = match current.parent() {
77+
Some(p) if !p.as_os_str().is_empty() => p,
78+
_ => return Ok(current),
79+
};
80+
81+
let parent_dev = parent.metadata()?.dev();
82+
if parent_dev != current_dev {
83+
return Ok(current);
84+
}
85+
86+
if parent == current {
87+
return Ok(current);
88+
}
89+
90+
current = parent.to_path_buf();
91+
}
92+
}
93+
6494
/// Find the mount info that best matches a given filesystem path.
6595
///
6696
/// This function returns the element of `mounts` on which `path` is
@@ -195,6 +225,43 @@ impl Filesystem {
195225
#[cfg(not(windows))]
196226
return result.and_then(|mount_info| Self::from_mount(mounts, mount_info, Some(file)));
197227
}
228+
229+
/// Fallback using statfs when mount table is unavailable.
230+
#[cfg(unix)]
231+
pub(crate) fn from_path_direct<P>(path: P) -> Result<Self, FsError>
232+
where
233+
P: AsRef<Path>,
234+
{
235+
let file = path.as_ref().as_os_str().to_owned();
236+
237+
let canonical_path = path
238+
.as_ref()
239+
.canonicalize()
240+
.map_err(|_| FsError::InvalidPath)?;
241+
242+
let stat_result = statfs(canonical_path.as_os_str()).map_err(|_| FsError::MountMissing)?;
243+
let mount_dir = find_mount_point(&canonical_path).map_err(|_| FsError::MountMissing)?;
244+
let fs_type = pretty_fstype(stat_result.fs_type()).into_owned();
245+
246+
let mount_info = MountInfo {
247+
dev_id: String::new(),
248+
dev_name: "-".to_string(),
249+
fs_type,
250+
mount_dir: mount_dir.into_os_string(),
251+
mount_option: String::new(),
252+
mount_root: OsString::new(),
253+
remote: false,
254+
dummy: false,
255+
};
256+
257+
let usage = FsUsage::new(stat_result);
258+
259+
Ok(Self {
260+
file: Some(file),
261+
mount_info,
262+
usage,
263+
})
264+
}
198265
}
199266

200267
#[cfg(test)]

tests/by-util/test_df.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use std::collections::HashSet;
1515
#[cfg(not(any(target_os = "freebsd", target_os = "windows")))]
1616
use uutests::at_and_ucmd;
1717
use uutests::new_ucmd;
18+
use uutests::util::TestScenario;
1819

1920
#[test]
2021
fn test_invalid_arg() {
@@ -1091,3 +1092,62 @@ fn test_df_hides_binfmt_misc_by_default() {
10911092
}
10921093
// If binfmt_misc is not mounted, skip the test silently
10931094
}
1095+
1096+
/// Run df inside a mount namespace where /proc is masked with tmpfs.
1097+
/// Returns (success, stdout, stderr).
1098+
#[cfg(target_os = "linux")]
1099+
fn run_df_with_masked_proc(args: &str) -> Option<(bool, String, String)> {
1100+
use std::process::Command;
1101+
1102+
// Check if user namespaces are available
1103+
if !Command::new("unshare")
1104+
.args(["-rm", "true"])
1105+
.status()
1106+
.is_ok_and(|s| s.success())
1107+
{
1108+
return None;
1109+
}
1110+
1111+
let df_path = TestScenario::new("df").bin_path.clone();
1112+
let output = Command::new("unshare")
1113+
.args(["-rm", "sh", "-c"])
1114+
.arg(format!(
1115+
"mount -t tmpfs tmpfs /proc && {} df {args}",
1116+
df_path.display()
1117+
))
1118+
.output()
1119+
.ok()?;
1120+
1121+
Some((
1122+
output.status.success(),
1123+
String::from_utf8_lossy(&output.stdout).to_string(),
1124+
String::from_utf8_lossy(&output.stderr).to_string(),
1125+
))
1126+
}
1127+
1128+
/// Test df fallback when /proc is masked - should work with path, fail without or with filters.
1129+
#[test]
1130+
#[cfg(target_os = "linux")]
1131+
fn test_df_masked_proc_fallback() {
1132+
if let Some((ok, stdout, stderr)) = run_df_with_masked_proc(".") {
1133+
assert!(ok, "df . should succeed: {stderr}");
1134+
assert!(stderr.contains("cannot read table of mounted file systems"));
1135+
assert!(stdout.contains("Filesystem"));
1136+
}
1137+
1138+
if let Some((ok, _, _)) = run_df_with_masked_proc("") {
1139+
assert!(!ok, "df without args should fail when /proc is masked");
1140+
}
1141+
1142+
for args in ["-a .", "-l .", "-t ext4 .", "-x tmpfs ."] {
1143+
if let Some((ok, _, _)) = run_df_with_masked_proc(args) {
1144+
assert!(!ok, "df {args} should fail when /proc is masked");
1145+
}
1146+
}
1147+
1148+
for args in ["-i .", "-T .", "--total ."] {
1149+
if let Some((ok, _, stderr)) = run_df_with_masked_proc(args) {
1150+
assert!(ok, "df {args} should succeed: {stderr}");
1151+
}
1152+
}
1153+
}

0 commit comments

Comments
 (0)