diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index c782ad429a4..15b608af6b2 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -522,9 +522,24 @@ impl Chmoder { .safe_chmod_file(&entry_path, dir_fd, &entry_name, meta.mode() & 0o7777) .and(r); - // Recurse into subdirectories + // Recurse into subdirectories using the existing directory fd if meta.is_dir() { - r = self.walk_dir_with_context(&entry_path, false).and(r); + match dir_fd.open_subdir(&entry_name) { + Ok(child_dir_fd) => { + r = self.safe_traverse_dir(&child_dir_fd, &entry_path).and(r); + } + Err(err) => { + let error = if err.kind() == std::io::ErrorKind::PermissionDenied { + ChmodError::PermissionDenied( + entry_path.to_string_lossy().to_string(), + ) + .into() + } else { + err.into() + }; + r = r.and(Err(error)); + } + } } } } diff --git a/tests/by-util/test_chmod.rs b/tests/by-util/test_chmod.rs index 1378aab00d2..e4d4b028474 100644 --- a/tests/by-util/test_chmod.rs +++ b/tests/by-util/test_chmod.rs @@ -2,6 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +// spell-checker:ignore (words) dirfd subdirs openat FDCWD use std::fs::{OpenOptions, Permissions, metadata, set_permissions}; use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; @@ -1280,6 +1281,51 @@ fn test_chmod_non_utf8_paths() { ); } +#[cfg(all(target_os = "linux", feature = "chmod"))] +#[test] +#[ignore = "covered by util/check-safe-traversal.sh"] +fn test_chmod_recursive_uses_dirfd_for_subdirs() { + use std::process::Command; + use uutests::get_tests_binary; + + // strace is required; fail fast if it is missing or not runnable + let output = Command::new("strace") + .arg("-V") + .output() + .expect("strace not found; install strace to run this test"); + assert!( + output.status.success(), + "strace -V failed; ensure strace is installed and usable" + ); + + let (at, _ucmd) = at_and_ucmd!(); + at.mkdir("x"); + at.mkdir("x/y"); + at.mkdir("x/y/z"); + + let log_path = at.plus_as_string("strace.log"); + + let status = Command::new("strace") + .arg("-e") + .arg("openat") + .arg("-o") + .arg(&log_path) + .arg(get_tests_binary!()) + .args(["chmod", "-R", "+x", "x"]) + .current_dir(&at.subdir) + .status() + .expect("failed to run strace"); + assert!(status.success(), "strace run failed"); + + let log = at.read("strace.log"); + + // Regression guard: ensure recursion uses dirfd-relative openat instead of AT_FDCWD with a multi-component path + assert!( + !log.contains("openat(AT_FDCWD, \"x/y"), + "chmod recursed using AT_FDCWD with a multi-component path; expected dirfd-relative openat" + ); +} + #[test] fn test_chmod_colored_output() { // Test colored help message diff --git a/util/check-safe-traversal.sh b/util/check-safe-traversal.sh index ed3c5a78eff..8dc9b04cf52 100755 --- a/util/check-safe-traversal.sh +++ b/util/check-safe-traversal.sh @@ -173,6 +173,11 @@ fi if echo "$AVAILABLE_UTILS" | grep -q "chmod"; then cp -r test_dir test_chmod check_utility "chmod" "openat,fchmodat,newfstatat,chmod" "openat fchmodat" "-R 755 test_chmod" "recursive_chmod" + + # Additional regression guard: ensure recursion uses dirfd-relative openat, not AT_FDCWD with a multi-component path + if grep -q 'openat(AT_FDCWD, "test_chmod/' strace_chmod_recursive_chmod.log; then + fail_immediately "chmod recursed using AT_FDCWD with a multi-component path; expected dirfd-relative openat" + fi fi # Test chown - should use openat, fchownat, newfstatat