Skip to content

Commit 61fd18b

Browse files
committed
Add integration test for df fallback when /proc is masked
1 parent a3febdb commit 61fd18b

File tree

3 files changed

+68
-41
lines changed

3 files changed

+68
-41
lines changed

src/uu/df/src/df.rs

Lines changed: 5 additions & 15 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

@@ -112,10 +112,7 @@ impl Default for Options {
112112
}
113113

114114
impl Options {
115-
/// Check if the mount table is required for these options.
116-
///
117-
/// Options like -a (show all), -l (local only), -t (include type),
118-
/// and -x (exclude type) require the mount table to filter filesystems.
115+
/// Whether -a, -l, -t, or -x options require the mount table.
119116
fn requires_mount_table(&self) -> bool {
120117
self.show_all_fs || self.show_local_fs || self.include.is_some() || self.exclude.is_some()
121118
}
@@ -367,23 +364,17 @@ fn get_named_filesystems<P>(paths: &[P], opt: &Options) -> UResult<Vec<Filesyste
367364
where
368365
P: AsRef<Path>,
369366
{
370-
// Try to read the list of all mounted filesystems.
371-
// If this fails and we need the mount table (for -a, -l, -t, -x options),
372-
// we must return an error. Otherwise, we can fall back to using statfs directly.
367+
// The list of all mounted filesystems.
373368
let mounts_result = read_fs_list();
374369

375370
let (mounts, use_fallback) = match mounts_result {
376371
Ok(m) => (m, false),
377372
Err(e) => {
378373
if opt.requires_mount_table() {
379-
// Options like -a, -l, -t, -x require the mount table
380374
return Err(e);
381375
}
382-
// Warn about the missing mount table but continue with fallback.
383-
// Don't set exit code here - we'll still try to produce output.
384-
eprintln!(
385-
"{}: {}",
386-
uucore::util_name(),
376+
show_warning!(
377+
"{}",
387378
translate!("df-error-cannot-read-table-of-mounted-filesystems")
388379
);
389380
(vec![], true)
@@ -397,7 +388,6 @@ where
397388
for path in paths {
398389
#[cfg(unix)]
399390
let fs_result = if use_fallback {
400-
// Use statfs directly when mount table is unavailable
401391
Filesystem::from_path_direct(path)
402392
} else {
403393
Filesystem::from_path(&mounts, path)

src/uu/df/src/filesystem.rs

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ use std::{ffi::OsString, path::Path};
1515
#[cfg(unix)]
1616
use std::os::unix::fs::MetadataExt;
1717

18-
use uucore::fsext::{FsUsage, MountInfo};
1918
#[cfg(unix)]
2019
use uucore::fsext::{FsMeta, pretty_fstype, statfs};
20+
use uucore::fsext::{FsUsage, MountInfo};
2121

2222
/// Summary representation of a filesystem.
2323
///
@@ -66,14 +66,7 @@ fn is_over_mounted(mounts: &[MountInfo], mount: &MountInfo) -> bool {
6666
}
6767
}
6868

69-
/// Find the mount point for a given path by walking up the directory tree.
70-
///
71-
/// This function walks up the directory tree from `path` until it finds a
72-
/// directory that is on a different device (different `st_dev`), which indicates
73-
/// a mount point boundary. This is used as a fallback when the mount table is
74-
/// unavailable.
75-
///
76-
/// Returns the path to the mount point directory.
69+
/// Find mount point by walking up the directory tree until device ID changes.
7770
#[cfg(unix)]
7871
pub(crate) fn find_mount_point<P: AsRef<Path>>(path: P) -> io::Result<PathBuf> {
7972
let mut current = path.as_ref().canonicalize()?;
@@ -87,11 +80,9 @@ pub(crate) fn find_mount_point<P: AsRef<Path>>(path: P) -> io::Result<PathBuf> {
8780

8881
let parent_dev = parent.metadata()?.dev();
8982
if parent_dev != current_dev {
90-
// We crossed a mount point boundary
9183
return Ok(current);
9284
}
9385

94-
// Check if we've reached the root (parent equals current)
9586
if parent == current {
9687
return Ok(current);
9788
}
@@ -235,37 +226,23 @@ impl Filesystem {
235226
return result.and_then(|mount_info| Self::from_mount(mounts, mount_info, Some(file)));
236227
}
237228

238-
/// Create a filesystem directly from a path using statfs, without needing the mount table.
239-
///
240-
/// This is a fallback method used when the mount table (e.g., /proc/self/mountinfo)
241-
/// is unavailable. It uses `statfs()` to get filesystem usage and `find_mount_point()`
242-
/// to determine the mount directory by walking up the directory tree.
243-
///
244-
/// Note: Some mount info fields (like device name) may be incomplete when using this method.
229+
/// Fallback using statfs when mount table is unavailable.
245230
#[cfg(unix)]
246231
pub(crate) fn from_path_direct<P>(path: P) -> Result<Self, FsError>
247232
where
248233
P: AsRef<Path>,
249234
{
250235
let file = path.as_ref().as_os_str().to_owned();
251236

252-
// Get the canonical path first
253237
let canonical_path = path
254238
.as_ref()
255239
.canonicalize()
256240
.map_err(|_| FsError::InvalidPath)?;
257241

258-
// Get filesystem info using statfs
259242
let stat_result = statfs(canonical_path.as_os_str()).map_err(|_| FsError::MountMissing)?;
260-
261-
// Find the mount point by walking up the directory tree
262243
let mount_dir = find_mount_point(&canonical_path).map_err(|_| FsError::MountMissing)?;
263-
264-
// Get filesystem type from statfs f_type using the FsMeta trait
265244
let fs_type = pretty_fstype(stat_result.fs_type()).into_owned();
266245

267-
// Create a MountInfo with the information we have
268-
// Note: dev_name will be "-" since we can't determine it without the mount table
269246
let mount_info = MountInfo {
270247
dev_id: String::new(),
271248
dev_name: "-".to_string(),

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)