Skip to content

Commit 3edf14e

Browse files
mattsu2020ChrisDrydenEcordonniercakebakersylvestre
authored
rm:fix safe traversal/access (#9577)
* chmod:fix safe traversal/access (#9554) * feat(chmod): use dirfd for recursive subdirectory traversal - Update chmod recursive logic to use directory file descriptors instead of full paths for subdirectories - Improves performance, avoids path length issues, and ensures dirfd-relative openat calls - Add test to verify strace output shows no AT_FDCWD with multi-component paths * test(chmod): add spell-check ignore for dirfd, subdirs, openat, FDCWD Added a spell-checker ignore directive in the chmod test file to suppress false positives for legitimate technical terms used in Unix API calls. * test(chmod): enforce strace requirement in recursive test, fail fast instead of skip Previously, the test_chmod_recursive_uses_dirfd_for_subdirs test skipped gracefully if strace was unavailable, without failing. This change enforces the strace dependency by failing the test immediately if strace is not installed or runnable, ensuring the test runs reliably in environments where it is expected to pass, and preventing silent skips. * ci: install strace in Ubuntu CI jobs for debugging system calls Add installation of strace tool on Ubuntu runners in both individual build/test and feature build/test jobs. This enables tracing system calls during execution, aiding in debugging and performance analysis within the CI/CD pipeline. Updated existing apt-get commands and added conditional steps for Linux-only installations. * ci: Add strace installation to Ubuntu-based CI workflows Install strace on ubuntu-latest runners across multiple jobs to enable system call tracing for testing purposes, ensuring compatibility with tests that require this debugging tool. This includes updating package lists in existing installation steps. * chore(build): install strace and prevent apt prompts in Cross.toml pre-build Modified the pre-build command to install strace utility for debugging and added -y flag to apt-get install to skip prompts, ensuring non-interactive builds. * feat(build): support Alpine-based cross images in pre-build Detect package manager (apt vs apk) to install tzdata and strace in both Debian/Ubuntu and Alpine *-musl targets. Added fallback warning for unsupported managers. This ensures strace is available for targets using Alpine, which doesn't have apt-get. * refactor(build): improve pre-build script readability by using multi-line strings Replace escaped multi-line string with triple-quoted string for better readability in Cross.toml. * feat(ci): install strace in WSL2 GitHub Actions workflow Install strace utility in the WSL2 environment to support tracing system calls during testing. Minor update to Cross.toml spell-checker ignore list for consistency with change. * ci(wsl2): install strace as root with non-interactive apt-get Updated the WSL2 workflow step to use root shell (wsl-bash-root) for installing strace, removing sudo calls and adding DEBIAN_FRONTEND=noninteractive to prevent prompts. This improves CI reliability by ensuring direct root access and automated, interrupt-free package installation. * ci: Move strace installation to user shell and update spell ignore Fix WSL2 GitHub Actions workflow by installing strace as the user instead of root for better permission handling, and add "noninteractive" to the spell-checker ignore comment for consistency with the new apt-get command. This ensures the tool is available in the testing environment without unnecessary privilege escalation. * chore: ci: remove unused strace installation from CI workflows Remove strace package installation from multiple GitHub Actions workflow files (CICD.yml, l10n.yml, wsl2.yml). Strace was historically installed in Ubuntu jobs for debugging system calls, but it's no longer required for the tests and builds, reducing CI setup time and dependencies. * ci: add strace installation and fix spell-checker comments in CI files - Install strace package in CICD workflow to support safe traversal verification for utilities like rm, chmod, chown, chgrp, mv, and du, enabling syscall tracing for testing. - Clean up spell-checker ignore comments in wsl2.yml and Cross.toml by removing misplaced flags.第二个测试产品**ci: add strace installation and fix spell-checker comments in CI files** - Install strace package in CICD workflow to support safe traversal verification for utilities like rm, chmod, chown, chgrp, mv, and du, enabling syscall tracing for testing. - Clean up spell-checker ignore comments in wsl2.yml and Cross.toml by removing misplaced flags. * test: add regression guard for recursive chmod dirfd-relative traversal Add a check in check-safe-traversal.sh to ensure recursive chmod operations use dirfd-relative openat calls instead of AT_FDCWD with multi-component paths, preventing potential race conditions. Ignore the corresponding Rust test as it is now covered by this shell script guard. * Merge pull request #9561 from ChrisDryden/seq_benches seq: adding large integers benchmarks * install: do not call chown when called as root - `pseudo` is a tool which simulates being root by intercepting calls to e.g. `geteuid` and `chown` (by using the `LD_PRELOAD` mechanism). This is used e.g. to build filesystems for embedded devices without running as root on the build machine. - the `chown` call getting removed in this commit does not work when running with `pseudo` and using `PSEUDO_IGNORE_PATHS`: in this case, the call to `geteuid()` gets intercepted by `libpseudo.so` and returns 0, however the call to `chown()` isn't intercepted by `libpseudo.so` in case it is in a path from `PSEUDO_IGNORE_PATHS`, and will thus fail since the process is not really root - the call to `chown()` was added in #5735 with the intent of making the test `install-C-root.sh` pass, however it isn't required (GNU coreutils also does not call `chown` just because `install` was called as root) Fixes #9116 Signed-off-by: Etienne Cordonnier <ecordonnier@snap.com> * du: handle `--files0-from=-` with piped in `-` (#8985) * du: handle --files0-from=- with piped in '-' * build-gnu.sh: remove incorrect string replacement in tests/du/files0-from.pl --------- Co-authored-by: Sylvestre Ledru <sylvestre@debian.org> * perf: optimize rm prompts by reusing stat data to avoid extra syscalls This change adds inline functions for checking file modes and refactors prompt functions to accept pre-fetched stat data. It modifies safe_remove_* functions to handle paths without parents and updates safe_remove_dir_recursive to fetch and reuse initial mode. This reduces redundant statx system calls, improving performance during recursive removals. * feat(rm/linux): Refine interactive file removal prompts - Add specific prompts for symlinks and empty files in 'Always' mode - Refactor matching logic for better clarity and to match GNU rm behavior - Improve handling of write-protected and non-terminal stdin scenarios This enhances the user experience by providing more accurate and targeted confirmations during file removal on Linux. * refactor(rm/linux): reformat prompt_yes! macros for improved readability Refactored multiple call sites of the prompt_yes! macro in linux.rs to use consistent multi-line formatting, enhancing code readability and adhering to style guidelines without altering functionality. Adjusted import ordering slightly for better organization. * refactor(src/uu/rm/src/platform/linux.rs): remove unused 'self' import from std::io Removed the unused 'self' import from the std::io module to clean up the code and avoid potential confusion, as it was not referenced anywhere in the file. This is a minor refactoring for better maintainability. * chore(spell-checker): update ignore list to include statx and behaviour Add "statx" (a Linux system call name) and "behaviour" (potential spelling variant) to the spell-checker ignore comment in the rm utility's Linux platform code, preventing false positives in linting. * fix(linux/rm): correct prompting logic for write-protected files in Interactive::Always mode Refactor the prompt_file_with_stat function in src/uu/rm/src/platform/linux.rs to fix inconsistent prompting for Interactive::Always. Previously, it always used non-protected wording regardless of file writability. Now, it checks if the file is writable and uses appropriate messaging (simple for writable, protected for non-writable). The match logic for Interactive::Once and PromptProtected is also simplified using a triple condition for better readability and to ensure empty vs non-empty protected files are distinguished correctly, matching expected rm behavior. * style(rm): wrap long line in prompt_file_with_stat macro for readability Reformatted the prompt_yes! macro call across multiple lines to improve code readability and adhere to line length conventions. No functional changes. --------- Signed-off-by: Etienne Cordonnier <ecordonnier@snap.com> Co-authored-by: Chris Dryden <christopher.paul.dryden@gmail.com> Co-authored-by: Etienne Cordonnier <ecordonnier@snap.com> Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com> Co-authored-by: Sylvestre Ledru <sylvestre@debian.org>
1 parent 8689fb1 commit 3edf14e

File tree

2 files changed

+110
-13
lines changed

2 files changed

+110
-13
lines changed

src/uu/rm/src/platform/linux.rs

Lines changed: 102 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,106 @@
55

66
// Linux-specific implementations for the rm utility
77

8-
// spell-checker:ignore fstatat unlinkat
8+
// spell-checker:ignore fstatat unlinkat statx behaviour
99

1010
use indicatif::ProgressBar;
1111
use std::ffi::OsStr;
1212
use std::fs;
13+
use std::io::{IsTerminal, stdin};
14+
use std::os::unix::fs::PermissionsExt;
1315
use std::path::Path;
1416
use uucore::display::Quotable;
1517
use uucore::error::FromIo;
18+
use uucore::prompt_yes;
1619
use uucore::safe_traversal::DirFd;
1720
use uucore::show_error;
1821
use uucore::translate;
1922

2023
use super::super::{
21-
InteractiveMode, Options, is_dir_empty, is_readable_metadata, prompt_descend, prompt_dir,
22-
prompt_file, remove_file, show_permission_denied_error, show_removal_error,
23-
verbose_removed_directory, verbose_removed_file,
24+
InteractiveMode, Options, is_dir_empty, is_readable_metadata, prompt_descend, remove_file,
25+
show_permission_denied_error, show_removal_error, verbose_removed_directory,
26+
verbose_removed_file,
2427
};
2528

29+
#[inline]
30+
fn mode_readable(mode: libc::mode_t) -> bool {
31+
(mode & libc::S_IRUSR) != 0
32+
}
33+
34+
#[inline]
35+
fn mode_writable(mode: libc::mode_t) -> bool {
36+
(mode & libc::S_IWUSR) != 0
37+
}
38+
39+
/// File prompt that reuses existing stat data to avoid extra statx calls
40+
fn prompt_file_with_stat(path: &Path, stat: &libc::stat, options: &Options) -> bool {
41+
if options.interactive == InteractiveMode::Never {
42+
return true;
43+
}
44+
45+
let is_symlink = (stat.st_mode & libc::S_IFMT) == libc::S_IFLNK;
46+
let writable = mode_writable(stat.st_mode);
47+
let len = stat.st_size as u64;
48+
let stdin_ok = options.__presume_input_tty.unwrap_or(false) || stdin().is_terminal();
49+
50+
// Match original behaviour:
51+
// - Interactive::Always: always prompt; use non-protected wording when writable,
52+
// otherwise fall through to protected wording.
53+
if options.interactive == InteractiveMode::Always {
54+
if is_symlink {
55+
return prompt_yes!("remove symbolic link {}?", path.quote());
56+
}
57+
if writable {
58+
return if len == 0 {
59+
prompt_yes!("remove regular empty file {}?", path.quote())
60+
} else {
61+
prompt_yes!("remove file {}?", path.quote())
62+
};
63+
}
64+
// Not writable: use protected wording below
65+
}
66+
67+
// Interactive::Once or ::PromptProtected (and non-writable Always) paths
68+
match (stdin_ok, writable, len == 0) {
69+
(false, _, _) if options.interactive == InteractiveMode::PromptProtected => true,
70+
(_, true, _) => true,
71+
(_, false, true) => prompt_yes!(
72+
"remove write-protected regular empty file {}?",
73+
path.quote()
74+
),
75+
_ => prompt_yes!("remove write-protected regular file {}?", path.quote()),
76+
}
77+
}
78+
79+
/// Directory prompt that reuses existing stat data to avoid extra statx calls
80+
fn prompt_dir_with_mode(path: &Path, mode: libc::mode_t, options: &Options) -> bool {
81+
if options.interactive == InteractiveMode::Never {
82+
return true;
83+
}
84+
85+
let readable = mode_readable(mode);
86+
let writable = mode_writable(mode);
87+
let stdin_ok = options.__presume_input_tty.unwrap_or(false) || stdin().is_terminal();
88+
89+
match (stdin_ok, readable, writable, options.interactive) {
90+
(false, _, _, InteractiveMode::PromptProtected) => true,
91+
(false, false, false, InteractiveMode::Never) => true,
92+
(_, false, false, _) => prompt_yes!(
93+
"attempt removal of inaccessible directory {}?",
94+
path.quote()
95+
),
96+
(_, false, true, InteractiveMode::Always) => {
97+
prompt_yes!(
98+
"attempt removal of inaccessible directory {}?",
99+
path.quote()
100+
)
101+
}
102+
(_, true, false, _) => prompt_yes!("remove write-protected directory {}?", path.quote()),
103+
(_, _, _, InteractiveMode::Always) => prompt_yes!("remove directory {}?", path.quote()),
104+
(_, _, _, _) => true,
105+
}
106+
}
107+
26108
/// Whether the given file or directory is readable.
27109
pub fn is_readable(path: &Path) -> bool {
28110
fs::metadata(path).is_ok_and(|metadata| is_readable_metadata(&metadata))
@@ -34,7 +116,8 @@ pub fn safe_remove_file(
34116
options: &Options,
35117
progress_bar: Option<&ProgressBar>,
36118
) -> Option<bool> {
37-
let parent = path.parent()?;
119+
// If there is no parent (path is directly under cwd), unlinkat relative to "."
120+
let parent = path.parent().unwrap_or(Path::new("."));
38121
let file_name = path.file_name()?;
39122

40123
let dir_fd = DirFd::open(parent).ok()?;
@@ -65,7 +148,7 @@ pub fn safe_remove_empty_dir(
65148
options: &Options,
66149
progress_bar: Option<&ProgressBar>,
67150
) -> Option<bool> {
68-
let parent = path.parent()?;
151+
let parent = path.parent().unwrap_or(Path::new("."));
69152
let dir_name = path.file_name()?;
70153

71154
let dir_fd = DirFd::open(parent).ok()?;
@@ -196,15 +279,15 @@ pub fn safe_remove_dir_recursive(
196279
) -> bool {
197280
// Base case 1: this is a file or a symbolic link.
198281
// Use lstat to avoid race condition between check and use
199-
match fs::symlink_metadata(path) {
282+
let initial_mode = match fs::symlink_metadata(path) {
200283
Ok(metadata) if !metadata.is_dir() => {
201284
return remove_file(path, options, progress_bar);
202285
}
203-
Ok(_) => {}
286+
Ok(metadata) => metadata.permissions().mode(),
204287
Err(e) => {
205288
return show_removal_error(e, path);
206289
}
207-
}
290+
};
208291

209292
// Try to open the directory using DirFd for secure traversal
210293
let dir_fd = match DirFd::open(path) {
@@ -233,7 +316,9 @@ pub fn safe_remove_dir_recursive(
233316
error
234317
} else {
235318
// Ask user permission if needed
236-
if options.interactive == InteractiveMode::Always && !prompt_dir(path, options) {
319+
if options.interactive == InteractiveMode::Always
320+
&& !prompt_dir_with_mode(path, initial_mode, options)
321+
{
237322
return false;
238323
}
239324

@@ -252,7 +337,11 @@ pub fn safe_remove_dir_recursive(
252337
}
253338

254339
// Directory is empty and user approved removal
255-
remove_dir_with_special_cases(path, options, error)
340+
if let Some(result) = safe_remove_empty_dir(path, options, progress_bar) {
341+
result
342+
} else {
343+
remove_dir_with_special_cases(path, options, error)
344+
}
256345
}
257346
}
258347

@@ -324,7 +413,7 @@ pub fn safe_remove_dir_recursive_impl(path: &Path, dir_fd: &DirFd, options: &Opt
324413
// Ask user permission if needed for this subdirectory
325414
if !child_error
326415
&& options.interactive == InteractiveMode::Always
327-
&& !prompt_dir(&entry_path, options)
416+
&& !prompt_dir_with_mode(&entry_path, entry_stat.st_mode, options)
328417
{
329418
continue;
330419
}
@@ -335,7 +424,7 @@ pub fn safe_remove_dir_recursive_impl(path: &Path, dir_fd: &DirFd, options: &Opt
335424
}
336425
} else {
337426
// Remove file - check if user wants to remove it first
338-
if prompt_file(&entry_path, options) {
427+
if prompt_file_with_stat(&entry_path, &entry_stat, options) {
339428
error = handle_unlink(dir_fd, entry_name.as_ref(), &entry_path, false, options);
340429
}
341430
}

util/check-safe-traversal.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,14 @@ fi
167167
if echo "$AVAILABLE_UTILS" | grep -q "rm"; then
168168
cp -r test_dir test_rm
169169
check_utility "rm" "openat,unlinkat,newfstatat,unlink,rmdir" "openat" "-rf test_rm" "recursive_remove"
170+
171+
# Regression guard: rm must not issue path-based statx calls (should rely on dirfd-relative newfstatat)
172+
if grep -qE 'statx\(AT_FDCWD, "/' strace_rm_recursive_remove.log; then
173+
fail_immediately "rm is using path-based statx (absolute path); expected dirfd-relative newfstatat"
174+
fi
175+
if grep -qE 'statx\(AT_FDCWD, "[^"]*/' strace_rm_recursive_remove.log; then
176+
fail_immediately "rm is using path-based statx (multi-component relative path); expected dirfd-relative newfstatat"
177+
fi
170178
fi
171179

172180
# Test chmod - should use openat, fchmodat, newfstatat

0 commit comments

Comments
 (0)