mirror of
https://github.com/uutils/coreutils.git
synced 2026-06-10 15:48:22 -07:00
df: fallback when proc masked (#10417)
This commit is contained in:
+34
-3
@@ -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);
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user