diff --git a/src/uu/rm/locales/en-US.ftl b/src/uu/rm/locales/en-US.ftl index 8a9a3513eaa..de39e583fac 100644 --- a/src/uu/rm/locales/en-US.ftl +++ b/src/uu/rm/locales/en-US.ftl @@ -45,6 +45,7 @@ rm-error-use-no-preserve-root = use --no-preserve-root to override this failsafe rm-error-refusing-to-remove-directory = refusing to remove '.' or '..' directory: skipping {$path} rm-error-cannot-remove = cannot remove {$file} rm-error-may-not-abbreviate-no-preserve-root = you may not abbreviate the --no-preserve-root option +rm-error-traversal-failed = traversal failed: {$path} # Verbose messages rm-verbose-removed = removed {$file} diff --git a/src/uu/rm/src/platform/unix.rs b/src/uu/rm/src/platform/unix.rs index e890ab15823..72e720f98df 100644 --- a/src/uu/rm/src/platform/unix.rs +++ b/src/uu/rm/src/platform/unix.rs @@ -16,7 +16,7 @@ use std::path::Path; use uucore::display::Quotable; use uucore::error::FromIo; use uucore::prompt_yes; -use uucore::safe_traversal::DirFd; +use uucore::safe_traversal::{DirFd, clear_errno, take_errno}; use uucore::show_error; use uucore::translate; @@ -348,6 +348,7 @@ pub fn safe_remove_dir_recursive( #[cfg(not(target_os = "redox"))] pub fn safe_remove_dir_recursive_impl(path: &Path, dir_fd: &DirFd, options: &Options) -> bool { // Read directory entries using safe traversal + clear_errno(); let entries = match dir_fd.read_dir() { Ok(entries) => entries, Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => { @@ -361,6 +362,18 @@ pub fn safe_remove_dir_recursive_impl(path: &Path, dir_fd: &DirFd, options: &Opt } }; + // Check if readdir failed partway through (partial read) + if let Some(err) = take_errno() { + if !entries.is_empty() { + show_error!( + "{}: {}", + translate!("rm-error-traversal-failed", "path" => path.display()), + err + ); + return true; + } + } + let mut error = false; // Process each entry diff --git a/src/uucore/src/lib/features/safe_traversal.rs b/src/uucore/src/lib/features/safe_traversal.rs index 05c456ad4ce..21eaec0717f 100644 --- a/src/uucore/src/lib/features/safe_traversal.rs +++ b/src/uucore/src/lib/features/safe_traversal.rs @@ -21,6 +21,7 @@ use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd}; use std::path::{Path, PathBuf}; use nix::dir::Dir; +use nix::errno::Errno; use nix::fcntl::{OFlag, openat}; use nix::libc; use nix::sys::stat::{FchmodatFlags, FileStat, Mode, fchmodat, fstatat}; @@ -79,6 +80,19 @@ impl From for io::Error { } } +/// Clear errno and return any error that was set after an operation +/// This is used because the nix library does not propagate folder reading errors correctly +pub fn take_errno() -> Option { + let errno = Errno::last(); + Errno::clear(); + (errno != Errno::from_raw(0)).then(|| io::Error::from_raw_os_error(errno as i32)) +} + +/// Clear errno before an operation, required to read error messages not propagated by nix from reading folders +pub fn clear_errno() { + Errno::clear(); +} + // Helper function to read directory entries using nix fn read_dir_entries(fd: &OwnedFd) -> io::Result> { let mut entries = Vec::new(); diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 9b1477f7bec..4e014d7c422 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -5,6 +5,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) greadlink gsed multihardlink texinfo CARGOFLAGS # spell-checker:ignore openat TOCTOU CFLAGS tmpfs gnproc +# spell-checker:ignore hfsplus casefold chattr dirp memcpy set -e @@ -357,4 +358,15 @@ sed -i 's/echo "changing security context/echo "chcon: changing security context # Disable this test, it is not relevant for us: # * the selinux crate is handling errors # * the test says "maybe we should not fail when no context available" -sed -i -e "s|returns_ 1||g" tests/cp/no-ctx.sh +"${SED}" -i -e "s|returns_ 1||g" tests/cp/no-ctx.sh + +# uutils rm uses nix which calls readdir64_r, so add a wrapper that delegates to the readdir hook +"${SED}" -i '/^struct dirent \*readdir/i\ +int readdir64_r(DIR *dirp, struct dirent64 *entry, struct dirent64 **result) {\ + struct dirent *d = readdir(dirp);\ + if (!d) { *result = NULL; return errno ? EIO : 0; }\ + memcpy(entry, d, sizeof(*d));\ + *result = entry;\ + return 0;\ +} +' tests/rm/rm-readdir-fail.sh