mirror of
https://github.com/uutils/coreutils.git
synced 2026-06-10 15:48:22 -07:00
du: port to use the safe io functions
This commit is contained in:
@@ -21,7 +21,13 @@ path = "src/du.rs"
|
||||
# For the --exclude & --exclude-from options
|
||||
glob = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
uucore = { workspace = true, features = ["format", "fsext", "parser", "time"] }
|
||||
uucore = { workspace = true, features = [
|
||||
"format",
|
||||
"fsext",
|
||||
"parser",
|
||||
"time",
|
||||
"safe-traversal",
|
||||
] }
|
||||
thiserror = { workspace = true }
|
||||
fluent = { workspace = true }
|
||||
|
||||
|
||||
+466
-23
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,8 @@
|
||||
// Safe directory traversal using openat() and related syscalls
|
||||
// This module provides TOCTOU-safe filesystem operations for recursive traversal
|
||||
// Only available on Linux
|
||||
// spell-checker:ignore CLOEXEC RDONLY TOCTOU closedir dirp fdopendir fstatat openat
|
||||
// spell-checker:ignore CLOEXEC RDONLY TOCTOU closedir dirp fdopendir fstatat openat REMOVEDIR unlinkat
|
||||
// spell-checker:ignore RAII dirfd
|
||||
|
||||
#![cfg(target_os = "linux")]
|
||||
|
||||
@@ -291,7 +292,6 @@ impl FileInfo {
|
||||
ino: stat.st_ino as u64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create FileInfo from device and inode numbers
|
||||
pub fn new(dev: u64, ino: u64) -> Self {
|
||||
@@ -384,6 +384,144 @@ impl Metadata {
|
||||
pub fn as_raw_stat(&self) -> &libc::stat {
|
||||
&self.stat
|
||||
}
|
||||
|
||||
/// Compatibility methods to match std::fs::Metadata interface
|
||||
pub fn is_dir(&self) -> bool {
|
||||
self.file_type().is_directory()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> u64 {
|
||||
self.size()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
}
|
||||
|
||||
// Add MetadataExt trait implementation for compatibility
|
||||
#[cfg(not(windows))]
|
||||
impl std::os::unix::fs::MetadataExt for Metadata {
|
||||
fn dev(&self) -> u64 {
|
||||
self.stat.st_dev
|
||||
}
|
||||
|
||||
fn ino(&self) -> u64 {
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
{
|
||||
self.stat.st_ino.into()
|
||||
}
|
||||
#[cfg(not(target_pointer_width = "32"))]
|
||||
{
|
||||
self.stat.st_ino
|
||||
}
|
||||
}
|
||||
|
||||
fn mode(&self) -> u32 {
|
||||
self.stat.st_mode
|
||||
}
|
||||
|
||||
fn nlink(&self) -> u64 {
|
||||
// st_nlink is u32 on most platforms except x86_64
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
self.stat.st_nlink
|
||||
}
|
||||
#[cfg(not(target_arch = "x86_64"))]
|
||||
{
|
||||
self.stat.st_nlink.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn uid(&self) -> u32 {
|
||||
self.stat.st_uid
|
||||
}
|
||||
|
||||
fn gid(&self) -> u32 {
|
||||
self.stat.st_gid
|
||||
}
|
||||
|
||||
fn rdev(&self) -> u64 {
|
||||
self.stat.st_rdev
|
||||
}
|
||||
|
||||
fn size(&self) -> u64 {
|
||||
self.stat.st_size as u64
|
||||
}
|
||||
|
||||
fn atime(&self) -> i64 {
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
{
|
||||
self.stat.st_atime.into()
|
||||
}
|
||||
#[cfg(not(target_pointer_width = "32"))]
|
||||
{
|
||||
self.stat.st_atime
|
||||
}
|
||||
}
|
||||
|
||||
fn atime_nsec(&self) -> i64 {
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
{
|
||||
self.stat.st_atime_nsec.into()
|
||||
}
|
||||
#[cfg(not(target_pointer_width = "32"))]
|
||||
{
|
||||
self.stat.st_atime_nsec
|
||||
}
|
||||
}
|
||||
|
||||
fn mtime(&self) -> i64 {
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
{
|
||||
self.stat.st_mtime.into()
|
||||
}
|
||||
#[cfg(not(target_pointer_width = "32"))]
|
||||
{
|
||||
self.stat.st_mtime
|
||||
}
|
||||
}
|
||||
|
||||
fn mtime_nsec(&self) -> i64 {
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
{
|
||||
self.stat.st_mtime_nsec.into()
|
||||
}
|
||||
#[cfg(not(target_pointer_width = "32"))]
|
||||
{
|
||||
self.stat.st_mtime_nsec
|
||||
}
|
||||
}
|
||||
|
||||
fn ctime(&self) -> i64 {
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
{
|
||||
self.stat.st_ctime.into()
|
||||
}
|
||||
#[cfg(not(target_pointer_width = "32"))]
|
||||
{
|
||||
self.stat.st_ctime
|
||||
}
|
||||
}
|
||||
|
||||
fn ctime_nsec(&self) -> i64 {
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
{
|
||||
self.stat.st_ctime_nsec.into()
|
||||
}
|
||||
#[cfg(not(target_pointer_width = "32"))]
|
||||
{
|
||||
self.stat.st_ctime_nsec
|
||||
}
|
||||
}
|
||||
|
||||
fn blksize(&self) -> u64 {
|
||||
self.stat.st_blksize as u64
|
||||
}
|
||||
|
||||
fn blocks(&self) -> u64 {
|
||||
self.stat.st_blocks as u64
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -647,3 +785,4 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// file that was distributed with this source code.
|
||||
|
||||
// spell-checker:ignore (paths) atim sublink subwords azerty azeaze xcwww azeaz amaz azea qzerty tazerty tsublink testfile1 testfile2 filelist fpath testdir testfile
|
||||
// spell-checker:ignore selfref ELOOP
|
||||
#[cfg(not(windows))]
|
||||
use regex::Regex;
|
||||
|
||||
@@ -1439,3 +1440,235 @@ fn test_du_threshold_no_suggested_values() {
|
||||
let result = ts.ucmd().arg("--threshold").fails();
|
||||
assert!(!result.stderr_str().contains("[possible values: ]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "linux")]
|
||||
fn test_du_long_path_safe_traversal() {
|
||||
let ts = TestScenario::new(util_name!());
|
||||
let at = &ts.fixtures;
|
||||
|
||||
let mut deep_path = String::from("long_path_test");
|
||||
at.mkdir(&deep_path);
|
||||
|
||||
for i in 0..15 {
|
||||
let long_dir_name = format!("{}{}", "a".repeat(100), i);
|
||||
deep_path = format!("{deep_path}/{long_dir_name}");
|
||||
at.mkdir_all(&deep_path);
|
||||
}
|
||||
|
||||
let test_file = format!("{deep_path}/test.txt");
|
||||
at.write(&test_file, "test content");
|
||||
|
||||
let result = ts.ucmd().arg("-s").arg("long_path_test").succeeds();
|
||||
assert!(result.stdout_str().contains("long_path_test"));
|
||||
|
||||
let result = ts.ucmd().arg("long_path_test").succeeds();
|
||||
let lines: Vec<&str> = result.stdout_str().trim().lines().collect();
|
||||
assert!(lines.len() >= 15);
|
||||
}
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_du_very_deep_directory() {
|
||||
let ts = TestScenario::new(util_name!());
|
||||
let at = &ts.fixtures;
|
||||
|
||||
let mut current_path = String::from("x");
|
||||
at.mkdir(¤t_path);
|
||||
|
||||
for _ in 0..10 {
|
||||
current_path = format!("{current_path}/x");
|
||||
at.mkdir_all(¤t_path);
|
||||
}
|
||||
|
||||
at.write(&format!("{current_path}/file.txt"), "deep file");
|
||||
|
||||
let result = ts.ucmd().arg("-s").arg("x").succeeds();
|
||||
assert!(result.stdout_str().contains('x'));
|
||||
|
||||
let result = ts.ucmd().arg("-a").arg("x").succeeds();
|
||||
let output = result.stdout_str();
|
||||
assert!(output.contains("file.txt"));
|
||||
}
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_du_safe_traversal_with_symlinks() {
|
||||
let ts = TestScenario::new(util_name!());
|
||||
let at = &ts.fixtures;
|
||||
|
||||
let mut deep_path = String::from("symlink_test");
|
||||
at.mkdir(&deep_path);
|
||||
|
||||
for i in 0..8 {
|
||||
let dir_name = format!("{}{}", "b".repeat(50), i);
|
||||
deep_path = format!("{deep_path}/{dir_name}");
|
||||
at.mkdir_all(&deep_path);
|
||||
}
|
||||
|
||||
at.write(&format!("{deep_path}/target.txt"), "target content");
|
||||
|
||||
at.symlink_file(&format!("{deep_path}/target.txt"), "shallow_link.txt");
|
||||
|
||||
let result = ts.ucmd().arg("-L").arg("shallow_link.txt").succeeds();
|
||||
assert!(!result.stdout_str().is_empty());
|
||||
|
||||
let result = ts.ucmd().arg("shallow_link.txt").succeeds();
|
||||
assert!(!result.stdout_str().is_empty());
|
||||
}
|
||||
#[test]
|
||||
#[cfg(target_os = "linux")]
|
||||
fn test_du_inaccessible_directory() {
|
||||
// tested by tests/du/no-x
|
||||
let ts = TestScenario::new(util_name!());
|
||||
let at = ts.fixtures.clone();
|
||||
|
||||
at.mkdir("d");
|
||||
at.mkdir("d/no-x");
|
||||
at.mkdir("d/no-x/y");
|
||||
|
||||
at.set_mode("d/no-x", 0o600);
|
||||
|
||||
let result = ts.ucmd().arg("d").fails();
|
||||
result.stderr_contains("du: cannot access 'd/no-x/y': Permission denied");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_du_symlink_self_reference() {
|
||||
// Test symlink that points to its own directory
|
||||
let ts = TestScenario::new(util_name!());
|
||||
let at = &ts.fixtures;
|
||||
|
||||
at.mkdir("selfref");
|
||||
at.symlink_dir("selfref", "selfref/self");
|
||||
|
||||
let result = ts.ucmd().arg("-L").arg("selfref").succeeds();
|
||||
|
||||
result.stdout_contains("selfref");
|
||||
// Should not show the self-referencing symlink to avoid infinite recursion
|
||||
result.stdout_does_not_contain("selfref/self");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_du_long_symlink_chain() {
|
||||
// Test that very long symlink chains are handled gracefully
|
||||
let ts = TestScenario::new(util_name!());
|
||||
let at = &ts.fixtures;
|
||||
|
||||
// Create a simple structure that tests symlink depth limits
|
||||
// Instead of trying to create a chain that causes ELOOP, test that reasonable chains work
|
||||
at.mkdir_all("deep/level1/level2/level3/level4/level5");
|
||||
at.write(
|
||||
"deep/level1/level2/level3/level4/level5/file.txt",
|
||||
"content",
|
||||
);
|
||||
|
||||
at.symlink_dir("deep/level1", "link1");
|
||||
at.symlink_dir("link1/level2", "link2");
|
||||
at.symlink_dir("link2/level3", "link3");
|
||||
|
||||
let result = ts.ucmd().arg("-L").arg("link3").succeeds();
|
||||
result.stdout_contains("link3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
fn test_du_bind_mount_simulation() {
|
||||
// Simulate bind mount scenario using hard links where possible
|
||||
// Note: This test simulates what bind mounts do - making the same directory
|
||||
// appear in multiple places with the same inode
|
||||
let ts = TestScenario::new(util_name!());
|
||||
let at = &ts.fixtures;
|
||||
|
||||
at.mkdir_all("mount_test/subdir");
|
||||
at.write("mount_test/file1.txt", "content1");
|
||||
at.write("mount_test/subdir/file2.txt", "content2");
|
||||
|
||||
// On systems where we can't create actual bind mounts,
|
||||
// we test that cycle detection works with symlinks that would create similar cycles
|
||||
at.symlink_dir("../mount_test", "mount_test/subdir/cycle_link");
|
||||
|
||||
let result = ts.ucmd().arg("mount_test").succeeds();
|
||||
|
||||
result.stdout_contains("mount_test/subdir");
|
||||
result.stdout_contains("mount_test");
|
||||
|
||||
result.stdout_does_not_contain("mount_test/subdir/cycle_link");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_du_symlink_depth_tracking() {
|
||||
// Test that du can handle reasonable symlink chains without hitting depth limits
|
||||
let ts = TestScenario::new(util_name!());
|
||||
let at = &ts.fixtures;
|
||||
|
||||
at.mkdir_all("chain/dir1/dir2/dir3");
|
||||
at.write("chain/dir1/dir2/dir3/file.txt", "content");
|
||||
|
||||
at.symlink_dir("chain/dir1/dir2", "shortcut");
|
||||
|
||||
let result = ts.ucmd().arg("-L").arg("shortcut").succeeds();
|
||||
result.stdout_contains("shortcut/dir3");
|
||||
result.stdout_contains("shortcut");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "linux")]
|
||||
fn test_du_long_path_from_unreadable() {
|
||||
// Test the specific scenario from GNU's long-from-unreadable.sh test
|
||||
// This verifies that du can handle very long paths when the current directory is unreadable
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let ts = TestScenario::new(util_name!());
|
||||
let at = &ts.fixtures;
|
||||
|
||||
// Create a deep hierarchy similar to the GNU test
|
||||
// Use a more reasonable depth for unit tests
|
||||
let dir_name = "x".repeat(200);
|
||||
let mut current_path = String::new();
|
||||
|
||||
for i in 0..20 {
|
||||
if i == 0 {
|
||||
current_path = dir_name.clone();
|
||||
} else {
|
||||
current_path = format!("{current_path}/{dir_name}");
|
||||
}
|
||||
at.mkdir_all(¤t_path);
|
||||
}
|
||||
|
||||
at.write(&format!("{current_path}/test.txt"), "test content");
|
||||
|
||||
at.mkdir("inaccessible");
|
||||
|
||||
let original_cwd = env::current_dir().unwrap();
|
||||
|
||||
let inaccessible_path = at.plus("inaccessible");
|
||||
env::set_current_dir(&inaccessible_path).unwrap();
|
||||
|
||||
// Remove read permission from the directory
|
||||
let mut perms = fs::metadata(&inaccessible_path).unwrap().permissions();
|
||||
perms.set_mode(0o000);
|
||||
fs::set_permissions(&inaccessible_path, perms).unwrap();
|
||||
|
||||
// Try to run du on the long path from the unreadable directory
|
||||
let target_path = at.plus(&dir_name);
|
||||
let result = ts.ucmd().arg("-s").arg(&target_path).succeeds(); // Should succeed with safe traversal
|
||||
|
||||
assert!(!result.stdout_str().is_empty());
|
||||
let output = result.stdout_str().trim();
|
||||
let parts: Vec<&str> = output.split_whitespace().collect();
|
||||
assert_eq!(parts.len(), 2);
|
||||
|
||||
assert!(parts[0].parse::<u64>().is_ok());
|
||||
assert!(parts[1].contains(&dir_name[..50])); // Check first part of the long name
|
||||
|
||||
env::set_current_dir(&original_cwd).unwrap();
|
||||
|
||||
// Restore permissions so the directory can be cleaned up
|
||||
let mut perms = fs::metadata(&inaccessible_path).unwrap().permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(&inaccessible_path, perms).unwrap();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
# spell-checker:ignore (paths) abmon deref discrim eacces getlimits getopt ginstall inacc infloop inotify reflink ; (misc) INT_OFLOW OFLOW
|
||||
# spell-checker:ignore baddecode submodules xstrtol distros ; (vars/env) SRCDIR vdir rcexp xpart dired OSTYPE ; (utils) gnproc greadlink gsed multihardlink texinfo CARGOFLAGS
|
||||
# spell-checker:ignore openat TOCTOU
|
||||
|
||||
set -e
|
||||
|
||||
|
||||
Reference in New Issue
Block a user