df: fallback when proc masked (#10417)

This commit is contained in:
Chris Dryden
2026-02-03 22:10:11 +01:00
committed by GitHub
parent 2a2cafdbd4
commit 5df49ac4fd
3 changed files with 165 additions and 4 deletions
+34 -3
View File
@@ -16,7 +16,7 @@ use uucore::error::{UError, UResult, USimpleError, get_exit_code};
use uucore::fsext::{MountInfo, read_fs_list};
use uucore::parser::parse_size::ParseSizeError;
use uucore::translate;
use uucore::{format_usage, show};
use uucore::{format_usage, show, show_warning};
use clap::{Arg, ArgAction, ArgMatches, Command, parser::ValueSource};
@@ -111,6 +111,13 @@ impl Default for Options {
}
}
impl Options {
/// Whether -a, -l, -t, or -x options require the mount table.
fn requires_mount_table(&self) -> bool {
self.show_all_fs || self.show_local_fs || self.include.is_some() || self.exclude.is_some()
}
}
#[derive(Debug, Error)]
enum OptionsError {
// TODO This needs to vary based on whether `--block-size`
@@ -358,14 +365,38 @@ where
P: AsRef<Path>,
{
// The list of all mounted filesystems.
let mounts: Vec<MountInfo> = read_fs_list()?;
let mounts_result = read_fs_list();
#[allow(unused_variables)]
let (mounts, use_fallback) = match mounts_result {
Ok(m) => (m, false),
Err(e) => {
if opt.requires_mount_table() {
return Err(e);
}
show_warning!(
"{}",
translate!("df-error-cannot-read-table-of-mounted-filesystems")
);
(vec![], true)
}
};
let mut result = vec![];
// Convert each path into a `Filesystem`, which contains
// both the mount information and usage information.
for path in paths {
match Filesystem::from_path(&mounts, path) {
#[cfg(unix)]
let fs_result = if use_fallback {
Filesystem::from_path_direct(path)
} else {
Filesystem::from_path(&mounts, path)
};
#[cfg(not(unix))]
let fs_result = Filesystem::from_path(&mounts, path);
match fs_result {
Ok(fs) => {
if is_included(&fs.mount_info, opt) {
result.push(fs);
+70 -1
View File
@@ -8,10 +8,17 @@
//! filesystem mounted at a particular directory. It also includes
//! information on amount of space available and amount of space used.
// spell-checker:ignore canonicalized
#[cfg(unix)]
use std::io;
#[cfg(unix)]
use std::path::PathBuf;
use std::{ffi::OsString, path::Path};
#[cfg(unix)]
use uucore::fsext::statfs;
use std::os::unix::fs::MetadataExt;
#[cfg(unix)]
use uucore::fsext::{FsMeta, pretty_fstype, statfs};
use uucore::fsext::{FsUsage, MountInfo};
/// Summary representation of a filesystem.
@@ -61,6 +68,31 @@ fn is_over_mounted(mounts: &[MountInfo], mount: &MountInfo) -> bool {
}
}
/// Find mount point by walking up the directory tree until device ID changes.
#[cfg(unix)]
pub(crate) fn find_mount_point<P: AsRef<Path>>(path: P) -> io::Result<PathBuf> {
let mut current = path.as_ref().canonicalize()?;
let current_dev = current.metadata()?.dev();
loop {
let parent = match current.parent() {
Some(p) if !p.as_os_str().is_empty() => p,
_ => return Ok(current),
};
let parent_dev = parent.metadata()?.dev();
if parent_dev != current_dev {
return Ok(current);
}
if parent == current {
return Ok(current);
}
current = parent.to_path_buf();
}
}
/// Find the mount info that best matches a given filesystem path.
///
/// This function returns the element of `mounts` on which `path` is
@@ -195,6 +227,43 @@ impl Filesystem {
#[cfg(not(windows))]
return result.and_then(|mount_info| Self::from_mount(mounts, mount_info, Some(file)));
}
/// Fallback using statfs when mount table is unavailable.
#[cfg(unix)]
pub(crate) fn from_path_direct<P>(path: P) -> Result<Self, FsError>
where
P: AsRef<Path>,
{
let file = path.as_ref().as_os_str().to_owned();
let canonical_path = path
.as_ref()
.canonicalize()
.map_err(|_| FsError::InvalidPath)?;
let stat_result = statfs(canonical_path.as_os_str()).map_err(|_| FsError::MountMissing)?;
let mount_dir = find_mount_point(&canonical_path).map_err(|_| FsError::MountMissing)?;
let fs_type = pretty_fstype(stat_result.fs_type()).into_owned();
let mount_info = MountInfo {
dev_id: String::new(),
dev_name: "-".to_string(),
fs_type,
mount_dir: mount_dir.into_os_string(),
mount_option: String::new(),
mount_root: OsString::new(),
remote: false,
dummy: false,
};
let usage = FsUsage::new(stat_result);
Ok(Self {
file: Some(file),
mount_info,
usage,
})
}
}
#[cfg(test)]
+61
View File
@@ -15,6 +15,8 @@ use std::collections::HashSet;
#[cfg(not(any(target_os = "freebsd", target_os = "windows")))]
use uutests::at_and_ucmd;
use uutests::new_ucmd;
#[cfg(target_os = "linux")]
use uutests::util::TestScenario;
#[test]
fn test_invalid_arg() {
@@ -1091,3 +1093,62 @@ fn test_df_hides_binfmt_misc_by_default() {
}
// If binfmt_misc is not mounted, skip the test silently
}
/// Run df inside a mount namespace where /proc is masked with tmpfs.
/// Returns (success, stdout, stderr).
#[cfg(target_os = "linux")]
fn run_df_with_masked_proc(args: &str) -> Option<(bool, String, String)> {
use std::process::Command;
// Check if user namespaces are available
if !Command::new("unshare")
.args(["-rm", "true"])
.status()
.is_ok_and(|s| s.success())
{
return None;
}
let df_path = TestScenario::new("df").bin_path.clone();
let output = Command::new("unshare")
.args(["-rm", "sh", "-c"])
.arg(format!(
"mount -t tmpfs tmpfs /proc && {} df {args}",
df_path.display()
))
.output()
.ok()?;
Some((
output.status.success(),
String::from_utf8_lossy(&output.stdout).to_string(),
String::from_utf8_lossy(&output.stderr).to_string(),
))
}
/// Test df fallback when /proc is masked - should work with path, fail without or with filters.
#[test]
#[cfg(target_os = "linux")]
fn test_df_masked_proc_fallback() {
if let Some((ok, stdout, stderr)) = run_df_with_masked_proc(".") {
assert!(ok, "df . should succeed: {stderr}");
assert!(stderr.contains("cannot read table of mounted file systems"));
assert!(stdout.contains("Filesystem"));
}
if let Some((ok, _, _)) = run_df_with_masked_proc("") {
assert!(!ok, "df without args should fail when /proc is masked");
}
for args in ["-a .", "-l .", "-t ext4 .", "-x tmpfs ."] {
if let Some((ok, _, _)) = run_df_with_masked_proc(args) {
assert!(!ok, "df {args} should fail when /proc is masked");
}
}
for args in ["-i .", "-T .", "--total ."] {
if let Some((ok, _, stderr)) = run_df_with_masked_proc(args) {
assert!(ok, "df {args} should succeed: {stderr}");
}
}
}