Skip to content

Commit 1183529

Browse files
committed
rm: port to use the safe io functions
1 parent c890a34 commit 1183529

File tree

4 files changed

+215
-23
lines changed

4 files changed

+215
-23
lines changed

src/uu/rm/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ path = "src/rm.rs"
2020
[dependencies]
2121
thiserror = { workspace = true }
2222
clap = { workspace = true }
23-
uucore = { workspace = true, features = ["fs", "parser"] }
23+
uucore = { workspace = true, features = ["fs", "parser", "safe-traversal"] }
2424
fluent = { workspace = true }
2525

2626
[target.'cfg(unix)'.dependencies]

src/uu/rm/src/rm.rs

Lines changed: 161 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
55

6-
// spell-checker:ignore (path) eacces inacc rm-r4
6+
// spell-checker:ignore (path) eacces inacc rm-r4 unlinkat fstatat
77

88
use clap::builder::{PossibleValue, ValueParser};
99
use clap::{Arg, ArgAction, Command, parser::ValueSource};
@@ -21,6 +21,8 @@ use thiserror::Error;
2121
use uucore::display::Quotable;
2222
use uucore::error::{FromIo, UError, UResult};
2323
use uucore::parser::shortcut_value_parser::ShortcutValueParser;
24+
#[cfg(target_os = "linux")]
25+
use uucore::safe_traversal::DirFd;
2426
use uucore::translate;
2527

2628
use uucore::{format_usage, os_str_as_bytes, prompt_yes, show_error};
@@ -428,6 +430,140 @@ fn is_writable(_path: &Path) -> bool {
428430
true
429431
}
430432

433+
#[cfg(target_os = "linux")]
434+
fn safe_remove_dir_recursive(path: &Path, options: &Options) -> bool {
435+
// Try to open the directory using DirFd for secure traversal
436+
let dir_fd = match DirFd::open(path) {
437+
Ok(fd) => fd,
438+
Err(e) => {
439+
show_error!(
440+
"{}",
441+
e.map_err_context(|| translate!("rm-error-cannot-remove", "file" => path.quote()))
442+
);
443+
return true;
444+
}
445+
};
446+
447+
let error = safe_remove_dir_recursive_impl(path, &dir_fd, options);
448+
449+
// After processing all children, remove the directory itself
450+
if error {
451+
error
452+
} else {
453+
// Ask user permission if needed
454+
if options.interactive == InteractiveMode::Always && !prompt_dir(path, options) {
455+
return false;
456+
}
457+
458+
// Use regular fs::remove_dir for the root since we can't unlinkat ourselves
459+
match fs::remove_dir(path) {
460+
Ok(_) => false,
461+
Err(e) => {
462+
let e = e.map_err_context(
463+
|| translate!("rm-error-cannot-remove", "file" => path.quote()),
464+
);
465+
show_error!("{e}");
466+
true
467+
}
468+
}
469+
}
470+
}
471+
472+
#[cfg(target_os = "linux")]
473+
fn safe_remove_dir_recursive_impl(path: &Path, dir_fd: &DirFd, options: &Options) -> bool {
474+
// Check if we should descend into this directory
475+
if options.interactive == InteractiveMode::Always
476+
&& !is_dir_empty(path)
477+
&& !prompt_descend(path)
478+
{
479+
return false;
480+
}
481+
482+
// Read directory entries using safe traversal
483+
let entries = match dir_fd.read_dir() {
484+
Ok(entries) => entries,
485+
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
486+
// This is not considered an error - just like the original
487+
return false;
488+
}
489+
Err(e) => {
490+
show_error!(
491+
"{}",
492+
e.map_err_context(|| translate!("rm-error-cannot-remove", "file" => path.quote()))
493+
);
494+
return true;
495+
}
496+
};
497+
498+
let mut error = false;
499+
500+
// Process each entry
501+
for entry_name in entries {
502+
let entry_path = path.join(&entry_name);
503+
504+
// Get metadata for the entry using fstatat
505+
let entry_stat = match dir_fd.stat_at(&entry_name, false) {
506+
Ok(stat) => stat,
507+
Err(e) => {
508+
let e = e.map_err_context(
509+
|| translate!("rm-error-cannot-remove", "file" => entry_path.quote()),
510+
);
511+
show_error!("{e}");
512+
error = true;
513+
continue;
514+
}
515+
};
516+
517+
// Check if it's a directory
518+
let is_dir = (entry_stat.st_mode & libc::S_IFMT) == libc::S_IFDIR;
519+
520+
if is_dir {
521+
// Recursively remove directory
522+
let subdir_fd = match dir_fd.open_subdir(&entry_name) {
523+
Ok(fd) => fd,
524+
Err(e) => {
525+
let e = e.map_err_context(
526+
|| translate!("rm-error-cannot-remove", "file" => entry_path.quote()),
527+
);
528+
show_error!("{e}");
529+
error = true;
530+
continue;
531+
}
532+
};
533+
534+
let child_error = safe_remove_dir_recursive_impl(&entry_path, &subdir_fd, options);
535+
error = error || child_error;
536+
537+
// Try to remove the directory (even if there were some child errors)
538+
// Ask user permission if needed
539+
if options.interactive == InteractiveMode::Always && !prompt_dir(&entry_path, options) {
540+
continue;
541+
}
542+
543+
if let Err(e) = dir_fd.unlink_at(&entry_name, true) {
544+
let e = e.map_err_context(
545+
|| translate!("rm-error-cannot-remove", "file" => entry_path.quote()),
546+
);
547+
show_error!("{e}");
548+
error = true;
549+
}
550+
} else {
551+
// Remove file - check if user wants to remove it first
552+
if prompt_file(&entry_path, options) {
553+
if let Err(e) = dir_fd.unlink_at(&entry_name, false) {
554+
let e = e.map_err_context(
555+
|| translate!("rm-error-cannot-remove", "file" => entry_path.quote()),
556+
);
557+
show_error!("{e}");
558+
error = true;
559+
}
560+
}
561+
}
562+
}
563+
564+
error
565+
}
566+
431567
/// Recursively remove the directory tree rooted at the given path.
432568
///
433569
/// If `path` is a file or a symbolic link, just remove it. If it is a
@@ -454,25 +590,30 @@ fn remove_dir_recursive(path: &Path, options: &Options) -> bool {
454590
return false;
455591
}
456592

457-
// Special case: if we cannot access the metadata because the
458-
// filename is too long, fall back to try
459-
// `fs::remove_dir_all()`.
460-
//
461-
// TODO This is a temporary bandage; we shouldn't need to do this
462-
// at all. Instead of using the full path like "x/y/z", which
463-
// causes a `InvalidFilename` error when trying to access the file
464-
// metadata, we should be able to use just the last part of the
465-
// path, "z", and know that it is relative to the parent, "x/y".
466-
if let Some(s) = path.to_str() {
467-
if s.len() > 1000 {
468-
match fs::remove_dir_all(path) {
469-
Ok(_) => return false,
470-
Err(e) => {
471-
let e = e.map_err_context(
472-
|| translate!("rm-error-cannot-remove", "file" => path.quote()),
473-
);
474-
show_error!("{e}");
475-
return true;
593+
// Use secure traversal on Linux for long paths
594+
#[cfg(target_os = "linux")]
595+
{
596+
if let Some(s) = path.to_str() {
597+
if s.len() > 1000 {
598+
return safe_remove_dir_recursive(path, options);
599+
}
600+
}
601+
}
602+
603+
// Fallback for non-Linux or shorter paths
604+
#[cfg(not(target_os = "linux"))]
605+
{
606+
if let Some(s) = path.to_str() {
607+
if s.len() > 1000 {
608+
match fs::remove_dir_all(path) {
609+
Ok(_) => return false,
610+
Err(e) => {
611+
let e = e.map_err_context(
612+
|| translate!("rm-error-cannot-remove", "file" => path.quote()),
613+
);
614+
show_error!("{e}");
615+
return true;
616+
}
476617
}
477618
}
478619
}

src/uucore/src/lib/features/safe_traversal.rs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,35 @@ impl DirFd {
255255
})
256256
}
257257

258-
pub fn as_raw_fd(&self) -> RawFd {
259-
self.fd
258+
/// Remove a file or empty directory relative to this directory
259+
pub fn unlink_at(&self, name: &OsStr, is_dir: bool) -> io::Result<()> {
260+
let name_str = name.to_string_lossy();
261+
let name_cstr =
262+
CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?;
263+
let flags = if is_dir { libc::AT_REMOVEDIR } else { 0 };
264+
265+
let ret = unsafe { libc::unlinkat(self.fd, name_cstr.as_ptr(), flags) };
266+
267+
if ret < 0 {
268+
Err(SafeTraversalError::UnlinkFailed {
269+
path: name_str.to_string(),
270+
source: io::Error::last_os_error(),
271+
}
272+
.into())
273+
} else {
274+
Ok(())
275+
}
276+
}
277+
278+
/// Create a DirFd from an existing file descriptor (does not take ownership)
279+
pub fn from_raw_fd(fd: RawFd) -> io::Result<Self> {
280+
if fd < 0 {
281+
return Err(io::Error::new(
282+
io::ErrorKind::InvalidInput,
283+
"invalid file descriptor",
284+
));
285+
}
286+
Ok(DirFd { fd, owned: false })
260287
}
261288
}
262289

tests/by-util/test_rm.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,3 +1054,27 @@ fn test_non_utf8_paths() {
10541054

10551055
assert!(!at.dir_exists(non_utf8_dir_name));
10561056
}
1057+
1058+
#[test]
1059+
#[cfg(target_os = "linux")]
1060+
fn test_rm_recursive_long_path_safe_traversal() {
1061+
let ts = TestScenario::new(util_name!());
1062+
let at = &ts.fixtures;
1063+
1064+
let mut deep_path = String::from("rm_deep");
1065+
at.mkdir(&deep_path);
1066+
1067+
for i in 0..12 {
1068+
let long_dir_name = format!("{}{}", "z".repeat(80), i);
1069+
deep_path = format!("{deep_path}/{long_dir_name}");
1070+
at.mkdir_all(&deep_path);
1071+
}
1072+
1073+
at.write("rm_deep/test1.txt", "content1");
1074+
at.write(&format!("{deep_path}/test2.txt"), "content2");
1075+
1076+
ts.ucmd().arg("-rf").arg("rm_deep").succeeds();
1077+
1078+
// Verify the directory is completely removed
1079+
assert!(!at.dir_exists("rm_deep"));
1080+
}

0 commit comments

Comments
 (0)