Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@shskwmt
Copy link
Contributor

@shskwmt shskwmt commented Mar 25, 2025

Fixes #7011

Implement --one-file-system and --preserve-root=all options for the rm command.

Comment on lines 565 to 580
match validate_single_filesystem(path) {
Ok(()) => true,
Err(additional_reason) => {
if !additional_reason.is_empty() {
show_error!("{}", additional_reason);
}
show_error!(
"skipping {}, since it's on a different device",
path.quote()
);
if options.preserve_root == PreserveRoot::YesAll {
show_error!("and --preserve-root=all is in effect");
}
false
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can probably be simplified with something like:

Suggested change
match validate_single_filesystem(path) {
Ok(()) => true,
Err(additional_reason) => {
if !additional_reason.is_empty() {
show_error!("{}", additional_reason);
}
show_error!(
"skipping {}, since it's on a different device",
path.quote()
);
if options.preserve_root == PreserveRoot::YesAll {
show_error!("and --preserve-root=all is in effect");
}
false
}
}
let result = validate_single_filesystem(path);
if result.is_ok() {
return true;
}
if let Err(additional_reason) = result {
if !additional_reason.is_empty() {
show_error!("{}", additional_reason);
}
}
show_error!(
"skipping {}, since it's on a different device",
path.quote()
);
if options.preserve_root == PreserveRoot::YesAll {
show_error!("and --preserve-root=all is in effect");
}
false

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for pointing that out! I've implemented the suggested simplification.

c091369

@github-actions
Copy link

GNU testsuite comparison:

Skip an intermittent issue tests/misc/stdbuf (fails in this run but passes in the 'main' branch)
Skipping an intermittent issue tests/timeout/timeout (passes in this run but fails in the 'main' branch)

@sylvestre
Copy link
Contributor

some jobs are failing :)

@github-actions
Copy link

GNU testsuite comparison:

Skip an intermittent issue tests/timeout/timeout (fails in this run but passes in the 'main' branch)
Skipping an intermittent issue tests/misc/stdbuf (passes in this run but fails in the 'main' branch)

@github-actions
Copy link

GNU testsuite comparison:

Skipping an intermittent issue tests/misc/stdbuf (passes in this run but fails in the 'main' branch)

@sylvestre
Copy link
Contributor

does
bash -v util/run-gnu-test.sh tests/rm/one-file-system.sh
passes on your system ?

@github-actions
Copy link

GNU testsuite comparison:

Skip an intermittent issue tests/misc/stdbuf (fails in this run but passes in the 'main' branch)

@shskwmt
Copy link
Contributor Author

shskwmt commented Mar 29, 2025

The following is the test result. It has passed.

PASS: tests/rm/one-file-system.sh
============================================================================
Testsuite summary for GNU coreutils 9.6-dirty
============================================================================
# TOTAL: 1
# PASS:  1
# SKIP:  0
# XFAIL: 0
# FAIL:  0
# XPASS: 0
# ERROR: 0
============================================================================

@shskwmt
Copy link
Contributor Author

shskwmt commented Mar 30, 2025

I tried again and it failed, so I will fix it.

FAIL: tests/rm/one-file-system
==============================

--- exp 2025-03-31 00:11:32.336030079 +0900
+++ out 2025-03-31 00:11:32.336030079 +0900
@@ -1 +1 @@
-rm: skipping 'a', since it's on a different device
+rm: cannot remove 'a/b': Device or resource busy
FAIL tests/rm/one-file-system.sh (exit status: 1)

============================================================================
Testsuite summary for GNU coreutils 9.6-dirty
============================================================================
# TOTAL: 1
# PASS:  0
# SKIP:  0
# XFAIL: 0
# FAIL:  1
# XPASS: 0
# ERROR: 0
============================================================================
See ./tests/test-suite.log for debugging.
Some test(s) failed.  Please report this to [email protected],
together with the test-suite.log file (gzipped) and your system
information.  Thanks.
============================================================================

@github-actions
Copy link

github-actions bot commented Apr 3, 2025

GNU testsuite comparison:

GNU test failed: tests/chgrp/default-no-deref. tests/chgrp/default-no-deref is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/chgrp/recurse. tests/chgrp/recurse is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/chmod/thru-dangling. tests/chmod/thru-dangling is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/chown/deref. tests/chown/deref is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/chown/preserve-root. tests/chown/preserve-root is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/abuse. tests/cp/abuse is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/attr-existing. tests/cp/attr-existing is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/cp-HL. tests/cp/cp-HL is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/cross-dev-symlink. tests/cp/cross-dev-symlink is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/keep-directory-symlink. tests/cp/keep-directory-symlink is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/link-deref. tests/cp/link-deref is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/link-no-deref. tests/cp/link-no-deref is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/link-preserve. tests/cp/link-preserve is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/link-symlink. tests/cp/link-symlink is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/no-deref-link1. tests/cp/no-deref-link1 is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/no-deref-link2. tests/cp/no-deref-link2 is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/no-deref-link3. tests/cp/no-deref-link3 is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/preserve-slink-time. tests/cp/preserve-slink-time is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/r-vs-symlink. tests/cp/r-vs-symlink is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/same-file. tests/cp/same-file is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/slink-2-slink. tests/cp/slink-2-slink is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/thru-dangling. tests/cp/thru-dangling is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/dd/misc. tests/dd/misc is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/du/deref. tests/du/deref is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/du/deref-args. tests/du/deref-args is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/du/long-sloop. tests/du/long-sloop is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/du/no-deref. tests/du/no-deref is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/du/one-file-system. tests/du/one-file-system is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/du/trailing-slash. tests/du/trailing-slash is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/env/env. tests/env/env is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/ln/hard-to-sym. tests/ln/hard-to-sym is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/ln/relative. tests/ln/relative is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/ln/sf-1. tests/ln/sf-1 is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/ln/target-1. tests/ln/target-1 is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/ls/classify. tests/ls/classify is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/ls/dangle. tests/ls/dangle is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/ls/dired. tests/ls/dired is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/ls/file-type. tests/ls/file-type is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/ls/follow-slink. tests/ls/follow-slink is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/ls/group-dirs. tests/ls/group-dirs is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/ls/hyperlink. tests/ls/hyperlink is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/ls/no-arg. tests/ls/no-arg is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/ls/selinux-segfault. tests/ls/selinux-segfault is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/ls/slink-acl. tests/ls/slink-acl is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/ls/stat-dtype. tests/ls/stat-dtype is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/ls/stat-failed. tests/ls/stat-failed is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/ls/stat-free-color. tests/ls/stat-free-color is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/ls/symlink-loop. tests/ls/symlink-loop is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/ls/symlink-quote. tests/ls/symlink-quote is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/misc/realpath. tests/misc/realpath is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/mv/atomic. tests/mv/atomic is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/mv/part-symlink. tests/mv/part-symlink is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/mv/symlink-onto-hardlink-to-self. tests/mv/symlink-onto-hardlink-to-self is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/pwd/pwd-option. tests/pwd/pwd-option is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/readlink/can-e. tests/readlink/can-e is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/readlink/can-f. tests/readlink/can-f is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/readlink/can-m. tests/readlink/can-m is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/readlink/multi. tests/readlink/multi is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/readlink/readlink-fp-loop. tests/readlink/readlink-fp-loop is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/readlink/readlink-root. tests/readlink/readlink-root is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/readlink/rl-1. tests/readlink/rl-1 is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/rm/ext3-perf. tests/rm/ext3-perf is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/rm/fail-eacces. tests/rm/fail-eacces is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/rm/rm2. tests/rm/rm2 is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/rm/rm3. tests/rm/rm3 is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/rmdir/symlink-errors. tests/rmdir/symlink-errors is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/stat/stat-slash. tests/stat/stat-slash is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/touch/no-dereference. tests/touch/no-dereference is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/touch/trailing-slash. tests/touch/trailing-slash is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/truncate/truncate-dangling-symlink. tests/truncate/truncate-dangling-symlink is passing on 'main'. Maybe you have to rebase?
Skipping an intermittent issue tests/timeout/timeout (passes in this run but fails in the 'main' branch)
Congrats! The gnu test tests/rm/one-file-system is no longer failing!

@github-actions
Copy link

github-actions bot commented Apr 3, 2025

GNU testsuite comparison:

Skipping an intermittent issue tests/timeout/timeout (passes in this run but fails in the 'main' branch)
Congrats! The gnu test tests/rm/one-file-system is no longer failing!

@sylvestre sylvestre force-pushed the rm-one-file-system branch from 2b9e697 to 3882f38 Compare April 4, 2025 16:49
@github-actions
Copy link

github-actions bot commented Apr 4, 2025

GNU testsuite comparison:

Skipping an intermittent issue tests/misc/stdbuf (passes in this run but fails in the 'main' branch)
Skipping an intermittent issue tests/timeout/timeout (passes in this run but fails in the 'main' branch)
Congrats! The gnu test tests/rm/one-file-system is no longer failing!

@sylvestre
Copy link
Contributor

Sorry, i think i made a mistake with the conflict resolution :( could you please fix it? thanks

@shskwmt
Copy link
Contributor Author

shskwmt commented May 4, 2025

No warries, I'll fix it.
Thanks for reaching out!

@shskwmt shskwmt force-pushed the rm-one-file-system branch from 5e09b83 to 42a24fb Compare May 7, 2025 14:43
@github-actions
Copy link

github-actions bot commented May 7, 2025

GNU testsuite comparison:

Skip an intermittent issue tests/timeout/timeout (fails in this run but passes in the 'main' branch)
Skipping an intermittent issue tests/misc/tee (passes in this run but fails in the 'main' branch)
Congrats! The gnu test tests/rm/one-file-system is no longer failing!

@sylvestre
Copy link
Contributor

sorry, i missed it :(
it needs to be rebased, could you please do this? sorry :(

@shskwmt shskwmt force-pushed the rm-one-file-system branch from 42a24fb to f4adf86 Compare December 6, 2025 09:54
@github-actions
Copy link

github-actions bot commented Dec 6, 2025

GNU testsuite comparison:

Congrats! The gnu test tests/du/files0-from is no longer failing!

@shskwmt shskwmt force-pushed the rm-one-file-system branch from f4adf86 to 5dd9c19 Compare December 6, 2025 10:25
This comment enhances the safety of recursive deletion in `rm` by introducing two key features to
prevent accidental data loss.

`--one-file-system`: When specified, `rm` will not traverse into directories that are on a
different file system from the one on which the traversal began, this prevents `rm -r` from
accidentally deleting data on mounted volumes.

`--preserve-root=all`: The `--preserve-root` option is enhanced to accept an optional argument,
`all`. When set to `all`, `rm` will refuse to remove any directory that is a mount point. The
default behavior (`--preserve-root` without an argument) remains, protecting only the root
directory (`/`).
@shskwmt shskwmt force-pushed the rm-one-file-system branch from 5dd9c19 to 8d0a839 Compare December 6, 2025 10:36
@github-actions
Copy link

github-actions bot commented Dec 6, 2025

GNU testsuite comparison:

Skipping an intermittent issue tests/tail/overlay-headers (passes in this run but fails in the 'main' branch)

@shskwmt
Copy link
Contributor Author

shskwmt commented Dec 6, 2025

I've finished rebasing the branch.
Could you please review it?

} else {
match matches
.get_one::<String>(OPT_PRESERVE_ROOT)
.unwrap()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please remove the unwrap()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed:
a0df78a

.map_err(|err| format!("cannot canonicalize {}: {err}", path.quote()))?;

// Get parent path, handling root case
let parent_canon = child_canon.parent().ok_or("")?.to_path_buf();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we avoid the empty strings in ok_or(") ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed:
2617af7

fn handle_dir(path: &Path, options: &Options, progress_bar: Option<&ProgressBar>) -> bool {
let mut had_err = false;

if let Err(additional_reason) = check_one_fs(path, options) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same code as in line 621, please dedup

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed:
f1c3436

Introduce `check_and_report_one_fs` to encapsulate the logic for
verifying filesystem boundaries (`-x`) and root preservation.

This removes code duplication between `remove_dir_recursive` and
`handle_dir`, ensuring consistent error reporting for "different device"
skips and `--preserve-root=all` violations.
@github-actions
Copy link

GNU testsuite comparison:

Skip an intermittent issue tests/tail/follow-name (fails in this run but passes in the 'main' branch)
Congrats! The gnu test tests/dd/stderr is no longer failing!
Congrats! The gnu test tests/tac/tac-2-nonseekable is no longer failing!
Congrats! The gnu test tests/tail/follow-stdin is no longer failing!

Split the initialization of `stat_path` in `Filesystem::new` into distinct
`#[cfg(unix)]` and `#[cfg(windows)]` blocks. This improves readability and
ensures that Windows builds consistently use the volume ID (`dev_id`).
@github-actions
Copy link

GNU testsuite comparison:

Skip an intermittent issue tests/tail/follow-name (fails in this run but passes in the 'main' branch)
Congrats! The gnu test tests/dd/stderr is no longer failing!
Congrats! The gnu test tests/tac/tac-2-nonseekable is no longer failing!
Congrats! The gnu test tests/tail/follow-stdin is no longer failing!

@github-actions
Copy link

GNU testsuite comparison:

Skipping an intermittent issue tests/shuf/shuf-reservoir (passes in this run but fails in the 'main' branch)
Skipping an intermittent issue tests/sort/sort-stale-thread-mem (passes in this run but fails in the 'main' branch)

@sylvestre
Copy link
Contributor

@shskwmt

any idea why we don't get "Congrats! The gnu test tests/rm/one-file-system is no longer failing!" anymore? thanks

@ChrisDryden
Copy link
Collaborator

It appears that one of the error messages is returning differently in the test:

--- exp	2026-01-21 10:59:43.543481609 +0000
+++ out	2026-01-21 10:59:43.546481572 +0000
@@ -1 +1 @@
-rm: skipping 'a/b', since it's on a different device
+rm: cannot remove 'a/b': Device or resource busy
FAIL tests/rm/one-file-system.sh (exit status: 1)

@ChrisDryden
Copy link
Collaborator

I'm taking a deeper look into the implementation and I'm curious about the decision to use the mount tables. The when using stat on the files you are able to get the device id from the st_dev value.

When reading from the mount tables it brings in the potential for a race condition and that appears to be whats causing the GNU tests to fail.

@ChrisDryden
Copy link
Collaborator

Would you be interested if I cherry-pick your PR and just modify the mount part to swap with the stat? Sorry that the feedback is getting there so much later than when you made the PR, but was wondering if this was something you were still interested in following up on?

@shskwmt
Copy link
Contributor Author

shskwmt commented Jan 28, 2026

@ChrisDryden
Hi, sorry for the late reply.

I actually have a fix ready locally, but I haven't been able to push it yet as I'm still in the middle of testing. It's a bit tough for me to find time during the week, so I'll likely get to it over the weekend.

For reference, here is the current diff I'm working on.

If this is urgent, please feel free to cherry-pick and modify it as you suggested!

diff --git a/src/uu/rm/src/platform/unix.rs b/src/uu/rm/src/platform/unix.rs
index e890ab158..a0092e742 100644
--- a/src/uu/rm/src/platform/unix.rs
+++ b/src/uu/rm/src/platform/unix.rs
@@ -11,6 +11,7 @@ use indicatif::ProgressBar;
 use std::ffi::OsStr;
 use std::fs;
 use std::io::{IsTerminal, stdin};
+use std::os::unix::fs::MetadataExt;
 use std::os::unix::fs::PermissionsExt;
 use std::path::Path;
 use uucore::display::Quotable;
@@ -21,9 +22,9 @@ use uucore::show_error;
 use uucore::translate;
 
 use super::super::{
-    InteractiveMode, Options, is_dir_empty, is_readable_metadata, prompt_descend, remove_file,
-    show_permission_denied_error, show_removal_error, verbose_removed_directory,
-    verbose_removed_file,
+    InteractiveMode, Options, PreserveRoot, is_dir_empty, is_readable_metadata, prompt_descend,
+    remove_file, show_one_fs_error, show_permission_denied_error, show_removal_error,
+    verbose_removed_directory, verbose_removed_file,
 };
 
 #[inline]
@@ -279,11 +280,11 @@ pub fn safe_remove_dir_recursive(
 ) -> bool {
     // Base case 1: this is a file or a symbolic link.
     // Use lstat to avoid race condition between check and use
-    let initial_mode = match fs::symlink_metadata(path) {
+    let (initial_mode, initial_dev) = match fs::symlink_metadata(path) {
         Ok(metadata) if !metadata.is_dir() => {
             return remove_file(path, options, progress_bar);
         }
-        Ok(metadata) => metadata.permissions().mode(),
+        Ok(metadata) => (metadata.permissions().mode(), metadata.dev()),
         Err(e) => {
             return show_removal_error(e, path);
         }
@@ -309,7 +310,7 @@ pub fn safe_remove_dir_recursive(
         }
     };
 
-    let error = safe_remove_dir_recursive_impl(path, &dir_fd, options);
+    let error = safe_remove_dir_recursive_impl(path, &dir_fd, options, Some(initial_dev));
 
     // After processing all children, remove the directory itself
     if error {
@@ -346,7 +347,12 @@ 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 {
+pub fn safe_remove_dir_recursive_impl(
+    path: &Path,
+    dir_fd: &DirFd,
+    options: &Options,
+    parent_dev: Option<u64>,
+) -> bool {
     // Read directory entries using safe traversal
     let entries = match dir_fd.read_dir() {
         Ok(entries) => entries,
@@ -380,6 +386,15 @@ pub fn safe_remove_dir_recursive_impl(path: &Path, dir_fd: &DirFd, options: &Opt
         let is_dir = ((entry_stat.st_mode as libc::mode_t) & libc::S_IFMT) == libc::S_IFDIR;
 
         if is_dir {
+            if options.one_fs || options.preserve_root == PreserveRoot::YesAll {
+                if let Some(p_dev) = parent_dev {
+                    if entry_stat.st_dev as u64 != p_dev {
+                        show_one_fs_error(&entry_path, options);
+                        continue;
+                    }
+                }
+            }
+
             // Ask user if they want to descend into this directory
             if options.interactive == InteractiveMode::Always
                 && !is_dir_empty(&entry_path)
@@ -408,8 +423,12 @@ pub fn safe_remove_dir_recursive_impl(path: &Path, dir_fd: &DirFd, options: &Opt
                 }
             };
 
-            let child_error = safe_remove_dir_recursive_impl(&entry_path, &child_dir_fd, options);
-            error |= child_error;
+            let child_error = safe_remove_dir_recursive_impl(
+                &entry_path,
+                &child_dir_fd,
+                options,
+                Some(entry_stat.st_dev as u64)
+            );
 
             // Ask user permission if needed for this subdirectory
             if !child_error
@@ -435,7 +454,12 @@ pub fn safe_remove_dir_recursive_impl(path: &Path, dir_fd: &DirFd, options: &Opt
 }
 
 #[cfg(target_os = "redox")]
-pub fn safe_remove_dir_recursive_impl(_path: &Path, _dir_fd: &DirFd, _options: &Options) -> bool {
+pub fn safe_remove_dir_recursive_impl(
+    _path: &Path,
+    _dir_fd: &DirFd,
+    _options: &Options,
+    _parent_dev: Option<u64>,
+) -> bool {
     // safe_traversal stat_at is not supported on Redox
     // This shouldn't be called on Redox, but provide a stub for compilation
     true // Return error
diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs
index 9f030ea1e..2bdd4805f 100644
--- a/src/uu/rm/src/rm.rs
+++ b/src/uu/rm/src/rm.rs
@@ -586,6 +586,17 @@ fn is_writable_metadata(_metadata: &Metadata) -> bool {
     true
 }
 
+pub(crate) fn show_one_fs_error(path: &Path, options: &Options) {
+    show_error!(
+        "skipping {}, since it's on a different device",
+        path.quote()
+    );
+
+    if options.preserve_root == PreserveRoot::YesAll {
+        show_error!("and --preserve-root=all is in effect");
+    }
+}
+
 /// Helper function to check fs and report errors if necessary.
 /// Returns true if the operation should be skipped/returned (i.e., on error).
 fn check_and_report_one_fs(path: &Path, options: &Options) -> bool {
@@ -593,14 +604,7 @@ fn check_and_report_one_fs(path: &Path, options: &Options) -> bool {
         if !additional_reason.is_empty() {
             show_error!("{}", additional_reason);
         }
-        show_error!(
-            "skipping {}, since it's on a different device",
-            path.quote()
-        );
-
-        if options.preserve_root == PreserveRoot::YesAll {
-            show_error!("and --preserve-root=all is in effect");
-        }
+        show_one_fs_error(path, options);
 
         return true;
     }

- Track device IDs (st_dev) during safe directory traversal to respect
  `--one-file-system` and `--preserve-root=all` options.
- Pass the parent device ID through recursive calls in `safe_remove_dir_recursive_impl`.
- Refactor `show_one_fs_error` into a reusable function to unify error reporting.
- Ensure the parent directory is not removed if any child removal fails or is skipped.
@github-actions
Copy link

GNU testsuite comparison:

GNU test failed: tests/rm/ir-1. tests/rm/ir-1 is passing on 'main'. Maybe you have to rebase?
Congrats! The gnu test tests/rm/one-file-system is no longer failing!

Previously, `safe_remove_dir_recursive` used a single boolean to track
both system errors and skipped files. This meant that if a user
declined to remove a file interactively, it was treated internally as
an error.

This change splits the state into `cmd_error` and `child_remains`.
Now, declining a file prevents the parent directory from being removed
(because it is not empty) but does not propagate a failure exit code.
@github-actions
Copy link

GNU testsuite comparison:

GNU test failed: tests/rm/empty-inacc. tests/rm/empty-inacc is passing on 'main'. Maybe you have to rebase?
Congrats! The gnu test tests/rm/one-file-system is no longer failing!

This refines the recursive removal logic on Unix to avoid incorrectly
preventing parent directory removal.

Previously, any failure to open a subdirectory (e.g., due to EACCES)
would unconditionally set `child_remains = true`. However, if the
directory is unreadable but empty, `handle_permission_denied` may
successfully unlink it.

Now, we only set `child_remains` and `cmd_error` if the direct removal
attempt (as a fallback for the open failure) actually fails. This fixes
the `test_inaccessible_dir_recursive` test case where a parent
directory remained even though its unreadable child was successfully
removed.
@github-actions
Copy link

GNU testsuite comparison:

Congrats! The gnu test tests/rm/one-file-system is no longer failing!

path.quote()
);

if !options.one_fs && options.preserve_root == PreserveRoot::YesAll {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

preserve-root message should show when preserve_root == PreserveRoot::YesAll, not when both conditions are false

no ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, removing !options.one_fs causes the one-file-system.sh test to fail.

The test expects ONLY the first line when --one-file-system is present.
Since preserve-root defaults to all in our implementation, the second line would be printed even for a standard --one-file-system call if we only check for YesAll.

if canonical.starts_with(&mount_dir) {
let len = mount_dir.as_os_str().len();
// Pick the mount with the longest matching prefix.
if best.is_none() || len > best.as_ref().unwrap().1 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you please replace with safer pattern matching to avoid the unwrap?

}

// Read mount information
let fs_list = read_fs_list().map_err(|err| format!("cannot read mount info: {err}"))?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading mount table for every file check introduces race conditions and poor performance

let fs_list = read_fs_list().map_err(|err| format!("cannot read mount info: {err}"))?;

// Canonicalize the path
let child_canon = path
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary canonicalization - using stat() device IDs would be more reliable and faster
no?

};

if failed {
cmd_error = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe rename it to had_command_error


if failed {
cmd_error = true;
child_remains = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe rename it to has_remaining_children


// Check if child and parent are on the same device
if child_mount.dev_id != parent_mount.dev_id {
return Err(String::new());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning empty string as error is not clear - please consider using a specific error type or enum variant

Rename `cmd_error` to `had_command_error` and `child_remains` to
`has_remaining_children` in `platform/unix.rs`.

These new names better reflect the boolean nature of the variables and
improve the readability of the control flow.
- Replace path canonicalization and mount point scanning with a more
  efficient stat-based (device ID) check.
- Add Windows support using volume_serial_number.
- Introduce OneFsError enum for clearer error reporting.
- Remove unused mount_for_path and read_fs_list.
- Replace unstable volume_serial_number() with std::path::Component::Prefix.
  This allows the filesystem boundary check to work on stable Rust while
  correctly identifying drive crossings on Windows.
- Remove unused uucore::fsext::MountInfo import.
This patch fixes several warnings on Windows by:
- Moving Unix-specific metadata retrieval into the `#[cfg(unix)]` block.
- Guarding the `StatFailed` error variant and its handling with `#[cfg(unix)]`.
- Removing the unused `MetadataExt` import for Windows.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rm --one-file-system should fail wih rm: skipping 'a/b', since it's on a different device

3 participants