Skip to content

Commit 43946dd

Browse files
committed
rm: port to use the safe io functions
1 parent dc2d00c commit 43946dd

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::LocalizedCommand;
@@ -429,6 +431,140 @@ fn is_writable(_path: &Path) -> bool {
429431
true
430432
}
431433

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

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

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)