Skip to content

Commit 5df49ac

Browse files
authored
df: fallback when proc masked (#10417)
1 parent 2a2cafd commit 5df49ac

3 files changed

Lines changed: 165 additions & 4 deletions

File tree

src/uu/df/src/df.rs

Lines changed: 34 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,38 @@ 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+
#[allow(unused_variables)]
371+
let (mounts, use_fallback) = match mounts_result {
372+
Ok(m) => (m, false),
373+
Err(e) => {
374+
if opt.requires_mount_table() {
375+
return Err(e);
376+
}
377+
show_warning!(
378+
"{}",
379+
translate!("df-error-cannot-read-table-of-mounted-filesystems")
380+
);
381+
(vec![], true)
382+
}
383+
};
362384

363385
let mut result = vec![];
364386

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

src/uu/df/src/filesystem.rs

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,17 @@
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+
#[cfg(unix)]
12+
use std::io;
13+
#[cfg(unix)]
14+
use std::path::PathBuf;
1115
use std::{ffi::OsString, path::Path};
1216

1317
#[cfg(unix)]
14-
use uucore::fsext::statfs;
18+
use std::os::unix::fs::MetadataExt;
19+
20+
#[cfg(unix)]
21+
use uucore::fsext::{FsMeta, pretty_fstype, statfs};
1522
use uucore::fsext::{FsUsage, MountInfo};
1623

1724
/// Summary representation of a filesystem.
@@ -61,6 +68,31 @@ fn is_over_mounted(mounts: &[MountInfo], mount: &MountInfo) -> bool {
6168
}
6269
}
6370

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

200269
#[cfg(test)]

tests/by-util/test_df.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ 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+
#[cfg(target_os = "linux")]
19+
use uutests::util::TestScenario;
1820

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

0 commit comments

Comments
 (0)