mirror of
https://github.com/uutils/coreutils.git
synced 2026-06-10 15:48:22 -07:00
feat: Expand safe directory traversal to all Unix platforms and fix related type conversions. (#9792)
--------- Co-authored-by: Sylvestre Ledru <sylvestre@debian.org>
This commit is contained in:
co-authored by
Sylvestre Ledru
parent
2b1d40af7c
commit
30239e69a3
@@ -34,6 +34,7 @@ RISCV
|
||||
RNG # random number generator
|
||||
RNGs
|
||||
Solaris
|
||||
TOCTOU # time-of-check time-of-use
|
||||
UID # user ID
|
||||
UIDs
|
||||
UUID # universally unique identifier
|
||||
|
||||
@@ -20,15 +20,12 @@ path = "src/chmod.rs"
|
||||
[dependencies]
|
||||
clap = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
uucore = { workspace = true, features = [
|
||||
"entries",
|
||||
"fs",
|
||||
"mode",
|
||||
"perms",
|
||||
"safe-traversal",
|
||||
] }
|
||||
uucore = { workspace = true, features = ["entries", "fs", "mode", "perms"] }
|
||||
fluent = { workspace = true }
|
||||
|
||||
[target.'cfg(all(unix, not(target_os = "redox")))'.dependencies]
|
||||
uucore = { workspace = true, features = ["safe-traversal"] }
|
||||
|
||||
[[bin]]
|
||||
name = "chmod"
|
||||
path = "src/main.rs"
|
||||
|
||||
+20
-13
@@ -18,7 +18,7 @@ use uucore::libc::mode_t;
|
||||
use uucore::mode;
|
||||
use uucore::perms::{TraverseSymlinks, configure_symlink_and_recursion};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(unix, not(target_os = "redox")))]
|
||||
use uucore::safe_traversal::DirFd;
|
||||
use uucore::{format_usage, show, show_error};
|
||||
|
||||
@@ -338,7 +338,7 @@ impl Chmoder {
|
||||
}
|
||||
|
||||
/// Handle symlinks during directory traversal based on traversal mode
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
#[cfg(not(unix))]
|
||||
fn handle_symlink_during_traversal(
|
||||
&self,
|
||||
path: &Path,
|
||||
@@ -423,7 +423,8 @@ impl Chmoder {
|
||||
matches!(fs::canonicalize(&file), Ok(p) if p == Path::new("/"))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
// Non-safe traversal implementation for platforms without safe_traversal support
|
||||
#[cfg(any(not(unix), target_os = "redox"))]
|
||||
fn walk_dir_with_context(&self, file_path: &Path, is_command_line_arg: bool) -> UResult<()> {
|
||||
let mut r = self.chmod_file(file_path);
|
||||
|
||||
@@ -436,8 +437,7 @@ impl Chmoder {
|
||||
|
||||
// If the path is a directory (or we should follow symlinks), recurse into it
|
||||
if (!file_path.is_symlink() || should_follow_symlink) && file_path.is_dir() {
|
||||
// We buffer all paths in this dir to not keep to be able to close the fd so not
|
||||
// too many fd's are open during the recursion
|
||||
// We buffer all paths in this dir to not keep too many fd's open during recursion
|
||||
let mut paths_in_this_dir = Vec::new();
|
||||
|
||||
for dir_entry in file_path.read_dir()? {
|
||||
@@ -450,9 +450,16 @@ impl Chmoder {
|
||||
}
|
||||
}
|
||||
for path in paths_in_this_dir {
|
||||
if path.is_symlink() {
|
||||
r = self.handle_symlink_during_recursion(&path).and(r);
|
||||
} else {
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
if path.is_symlink() {
|
||||
r = self.handle_symlink_during_recursion(&path).and(r);
|
||||
} else {
|
||||
r = self.walk_dir_with_context(path.as_path(), false).and(r);
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "redox")]
|
||||
{
|
||||
r = self.walk_dir_with_context(path.as_path(), false).and(r);
|
||||
}
|
||||
}
|
||||
@@ -460,7 +467,7 @@ impl Chmoder {
|
||||
r
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(unix, not(target_os = "redox")))]
|
||||
fn walk_dir_with_context(&self, file_path: &Path, is_command_line_arg: bool) -> UResult<()> {
|
||||
let mut r = self.chmod_file(file_path);
|
||||
|
||||
@@ -490,7 +497,7 @@ impl Chmoder {
|
||||
r
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(unix, not(target_os = "redox")))]
|
||||
fn safe_traverse_dir(&self, dir_fd: &DirFd, dir_path: &Path) -> UResult<()> {
|
||||
let mut r = Ok(());
|
||||
|
||||
@@ -546,7 +553,7 @@ impl Chmoder {
|
||||
r
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(unix, not(target_os = "redox")))]
|
||||
fn handle_symlink_during_safe_recursion(
|
||||
&self,
|
||||
path: &Path,
|
||||
@@ -578,7 +585,7 @@ impl Chmoder {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(unix, not(target_os = "redox")))]
|
||||
fn safe_chmod_file(
|
||||
&self,
|
||||
file_path: &Path,
|
||||
@@ -608,7 +615,7 @@ impl Chmoder {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
#[cfg(not(unix))]
|
||||
fn handle_symlink_during_recursion(&self, path: &Path) -> UResult<()> {
|
||||
// Use the common symlink handling logic
|
||||
self.handle_symlink_during_traversal(path, false)
|
||||
|
||||
+5
-2
@@ -1566,6 +1566,7 @@ fn file_mode_for_interactive_overwrite(
|
||||
match path.metadata() {
|
||||
Ok(me) => {
|
||||
// Cast is necessary on some platforms
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
let mode: mode_t = me.mode() as mode_t;
|
||||
|
||||
// It looks like this extra information is added to the prompt iff the file's user write bit is 0
|
||||
@@ -1758,7 +1759,7 @@ pub(crate) fn copy_attributes(
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
#[cfg(feature = "selinux")]
|
||||
#[cfg(all(feature = "selinux", target_os = "linux"))]
|
||||
handle_preserve(&attributes.context, || -> CopyResult<()> {
|
||||
// Get the source context and apply it to the destination
|
||||
if let Ok(context) = selinux::SecurityContext::of_path(source, false, false) {
|
||||
@@ -2552,7 +2553,7 @@ fn copy_file(
|
||||
copy_attributes(source, dest, &options.attributes)?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "selinux")]
|
||||
#[cfg(all(feature = "selinux", target_os = "linux"))]
|
||||
if options.set_selinux_context && uucore::selinux::is_selinux_enabled() {
|
||||
// Set the given selinux permissions on the copied file.
|
||||
if let Err(e) =
|
||||
@@ -2620,8 +2621,10 @@ fn handle_no_preserve_mode(options: &Options, org_mode: u32) -> u32 {
|
||||
target_os = "redox",
|
||||
))]
|
||||
{
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
const MODE_RW_UGO: u32 =
|
||||
(S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH) as u32;
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
const S_IRWXUGO: u32 = (S_IRWXU | S_IRWXG | S_IRWXO) as u32;
|
||||
return if is_explicit_no_preserve_mode {
|
||||
MODE_RW_UGO
|
||||
|
||||
@@ -27,11 +27,13 @@ uucore = { workspace = true, features = [
|
||||
"parser-size",
|
||||
"parser-glob",
|
||||
"time",
|
||||
"safe-traversal",
|
||||
] }
|
||||
thiserror = { workspace = true }
|
||||
fluent = { workspace = true }
|
||||
|
||||
[target.'cfg(all(unix, not(target_os = "redox")))'.dependencies]
|
||||
uucore = { workspace = true, features = ["safe-traversal"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows-sys = { workspace = true, features = [
|
||||
"Win32_Storage_FileSystem",
|
||||
|
||||
+20
-14
@@ -25,7 +25,7 @@ use uucore::display::{Quotable, print_verbatim};
|
||||
use uucore::error::{FromIo, UError, UResult, USimpleError, set_exit_code};
|
||||
use uucore::fsext::{MetadataTimeField, metadata_get_time};
|
||||
use uucore::line_ending::LineEnding;
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(unix, not(target_os = "redox")))]
|
||||
use uucore::safe_traversal::DirFd;
|
||||
use uucore::translate;
|
||||
|
||||
@@ -164,7 +164,7 @@ impl Stat {
|
||||
}
|
||||
|
||||
/// Create a Stat using safe traversal methods with `DirFd` for the root directory
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(unix, not(target_os = "redox")))]
|
||||
fn new_from_dirfd(dir_fd: &DirFd, full_path: &Path) -> std::io::Result<Self> {
|
||||
// Get metadata for the directory itself using fstat
|
||||
let safe_metadata = dir_fd.metadata()?;
|
||||
@@ -293,9 +293,9 @@ fn read_block_size(s: Option<&str>) -> UResult<u64> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
// For now, implement safe_du only on Linux
|
||||
// This is done for Ubuntu but should be extended to other platforms that support openat
|
||||
#[cfg(all(unix, not(target_os = "redox")))]
|
||||
// Implement safe_du on Unix (except Redox which lacks full stat support)
|
||||
// This is done for TOCTOU safety
|
||||
fn safe_du(
|
||||
path: &Path,
|
||||
options: &TraversalOptions,
|
||||
@@ -439,7 +439,8 @@ fn safe_du(
|
||||
const S_IFMT: u32 = 0o170_000;
|
||||
const S_IFDIR: u32 = 0o040_000;
|
||||
const S_IFLNK: u32 = 0o120_000;
|
||||
let is_symlink = (lstat.st_mode & S_IFMT) == S_IFLNK;
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
let is_symlink = (lstat.st_mode as u32 & S_IFMT) == S_IFLNK;
|
||||
|
||||
// Handle symlinks with -L option
|
||||
// For safe traversal with -L, we skip symlinks to directories entirely
|
||||
@@ -450,12 +451,14 @@ fn safe_du(
|
||||
continue;
|
||||
}
|
||||
|
||||
let is_dir = (lstat.st_mode & S_IFMT) == S_IFDIR;
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
let is_dir = (lstat.st_mode as u32 & S_IFMT) == S_IFDIR;
|
||||
let entry_stat = lstat;
|
||||
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
let file_info = (entry_stat.st_ino != 0).then_some(FileInfo {
|
||||
file_id: entry_stat.st_ino as u128,
|
||||
dev_id: entry_stat.st_dev,
|
||||
dev_id: entry_stat.st_dev as u64,
|
||||
});
|
||||
|
||||
// For safe traversal, we need to handle stats differently
|
||||
@@ -465,6 +468,7 @@ fn safe_du(
|
||||
Stat {
|
||||
path: entry_path.clone(),
|
||||
size: 0,
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
blocks: entry_stat.st_blocks as u64,
|
||||
inodes: 1,
|
||||
inode: file_info,
|
||||
@@ -476,7 +480,9 @@ fn safe_du(
|
||||
// For files
|
||||
Stat {
|
||||
path: entry_path.clone(),
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
size: entry_stat.st_size as u64,
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
blocks: entry_stat.st_blocks as u64,
|
||||
inodes: 1,
|
||||
inode: file_info,
|
||||
@@ -1096,14 +1102,14 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
let mut seen_inodes: HashSet<FileInfo> = HashSet::new();
|
||||
|
||||
// Determine which traversal method to use
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(unix, not(target_os = "redox")))]
|
||||
let use_safe_traversal = traversal_options.dereference != Deref::All;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
#[cfg(not(all(unix, not(target_os = "redox"))))]
|
||||
let use_safe_traversal = false;
|
||||
|
||||
if use_safe_traversal {
|
||||
// Use safe traversal (Linux only, when not using -L)
|
||||
#[cfg(target_os = "linux")]
|
||||
// Use safe traversal (Unix except Redox, when not using -L)
|
||||
#[cfg(all(unix, not(target_os = "redox")))]
|
||||
{
|
||||
// Pre-populate seen_inodes with the starting directory to detect cycles
|
||||
if let Ok(stat) = Stat::new(&path, None, &traversal_options) {
|
||||
@@ -1158,9 +1164,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
.send(Ok(StatPrintInfo { stat, depth: 0 }))
|
||||
.map_err(|e| USimpleError::new(1, e.to_string()))?;
|
||||
} else {
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(unix)]
|
||||
let error_msg = translate!("du-error-cannot-access", "path" => path.quote());
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
#[cfg(not(unix))]
|
||||
let error_msg =
|
||||
translate!("du-error-cannot-access-no-such-file", "path" => path.quote());
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ mod mode;
|
||||
use clap::{Arg, ArgAction, ArgMatches, Command};
|
||||
use file_diff::diff;
|
||||
use filetime::{FileTime, set_file_times};
|
||||
#[cfg(feature = "selinux")]
|
||||
#[cfg(all(feature = "selinux", target_os = "linux"))]
|
||||
use selinux::SecurityContext;
|
||||
use std::ffi::OsString;
|
||||
use std::fmt::Debug;
|
||||
@@ -27,7 +27,7 @@ use uucore::error::{FromIo, UError, UResult, UUsageError};
|
||||
use uucore::fs::dir_strip_dot_for_creation;
|
||||
use uucore::perms::{Verbosity, VerbosityLevel, wrap_chown};
|
||||
use uucore::process::{getegid, geteuid};
|
||||
#[cfg(feature = "selinux")]
|
||||
#[cfg(all(feature = "selinux", target_os = "linux"))]
|
||||
use uucore::selinux::{
|
||||
SeLinuxError, contexts_differ, get_selinux_security_context, is_selinux_enabled,
|
||||
selinux_error_description, set_selinux_security_context,
|
||||
@@ -118,7 +118,7 @@ enum InstallError {
|
||||
#[error("{}", translate!("install-error-extra-operand", "operand" => .0.quote(), "usage" => .1.clone()))]
|
||||
ExtraOperand(OsString, String),
|
||||
|
||||
#[cfg(feature = "selinux")]
|
||||
#[cfg(all(feature = "selinux", target_os = "linux"))]
|
||||
#[error("{}", .0)]
|
||||
SelinuxContextFailed(String),
|
||||
}
|
||||
@@ -1030,7 +1030,7 @@ fn copy(from: &Path, to: &Path, b: &Behavior) -> UResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "selinux")]
|
||||
#[cfg(all(feature = "selinux", target_os = "linux"))]
|
||||
fn get_context_for_selinux(b: &Behavior) -> Option<&String> {
|
||||
if b.default_context {
|
||||
None
|
||||
@@ -1165,7 +1165,7 @@ fn need_copy(from: &Path, to: &Path, b: &Behavior) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(feature = "selinux")]
|
||||
#[cfg(all(feature = "selinux", target_os = "linux"))]
|
||||
/// Sets the `SELinux` security context for install's -Z flag behavior.
|
||||
///
|
||||
/// This function implements the specific behavior needed for install's -Z flag,
|
||||
@@ -1199,7 +1199,7 @@ pub fn set_selinux_default_context(path: &Path) -> Result<(), SeLinuxError> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "selinux")]
|
||||
#[cfg(all(feature = "selinux", target_os = "linux"))]
|
||||
/// Gets the default `SELinux` context for a path based on the system's security policy.
|
||||
///
|
||||
/// This function attempts to determine what the "correct" `SELinux` context should be
|
||||
@@ -1255,7 +1255,7 @@ fn get_default_context_for_path(path: &Path) -> Result<Option<String>, SeLinuxEr
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[cfg(feature = "selinux")]
|
||||
#[cfg(all(feature = "selinux", target_os = "linux"))]
|
||||
/// Derives an appropriate `SELinux` context based on a parent directory context.
|
||||
///
|
||||
/// This is a heuristic function that attempts to generate an appropriate
|
||||
@@ -1293,7 +1293,7 @@ fn derive_context_from_parent(parent_context: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "selinux")]
|
||||
#[cfg(all(feature = "selinux", target_os = "linux"))]
|
||||
/// Helper function to collect paths that need `SELinux` context setting.
|
||||
///
|
||||
/// Traverses from the given starting path up to existing parent directories.
|
||||
@@ -1307,7 +1307,7 @@ fn collect_paths_for_context_setting(starting_path: &Path) -> Vec<&Path> {
|
||||
paths
|
||||
}
|
||||
|
||||
#[cfg(feature = "selinux")]
|
||||
#[cfg(all(feature = "selinux", target_os = "linux"))]
|
||||
/// Sets the `SELinux` security context for a directory hierarchy.
|
||||
///
|
||||
/// This function traverses from the given starting path up to existing parent directories
|
||||
@@ -1347,7 +1347,7 @@ fn set_selinux_context_for_directories(target_path: &Path, context: Option<&Stri
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "selinux")]
|
||||
#[cfg(all(feature = "selinux", target_os = "linux"))]
|
||||
/// Sets `SELinux` context for created directories using install's -Z default behavior.
|
||||
///
|
||||
/// Similar to `set_selinux_context_for_directories` but uses install's
|
||||
@@ -1371,10 +1371,10 @@ pub fn set_selinux_context_for_directories_install(target_path: &Path, context:
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(feature = "selinux")]
|
||||
#[cfg(all(feature = "selinux", target_os = "linux"))]
|
||||
use super::derive_context_from_parent;
|
||||
|
||||
#[cfg(feature = "selinux")]
|
||||
#[cfg(all(feature = "selinux", target_os = "linux"))]
|
||||
#[test]
|
||||
fn test_derive_context_from_parent() {
|
||||
// Test cases: (input_context, file_type, expected_output, description)
|
||||
|
||||
@@ -59,7 +59,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
}
|
||||
|
||||
// Apply SELinux context if requested
|
||||
#[cfg(feature = "selinux")]
|
||||
#[cfg(all(feature = "selinux", target_os = "linux"))]
|
||||
{
|
||||
// Extract the SELinux related flags and options
|
||||
let set_security_context = matches.get_flag(options::SECURITY_CONTEXT);
|
||||
|
||||
@@ -20,10 +20,13 @@ path = "src/rm.rs"
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
uucore = { workspace = true, features = ["fs", "parser", "safe-traversal"] }
|
||||
uucore = { workspace = true, features = ["fs", "parser"] }
|
||||
fluent = { workspace = true }
|
||||
indicatif = { workspace = true }
|
||||
|
||||
[target.'cfg(all(unix, not(target_os = "redox")))'.dependencies]
|
||||
uucore = { workspace = true, features = ["safe-traversal"] }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = { workspace = true }
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
// Platform-specific implementations for the rm utility
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod linux;
|
||||
#[cfg(all(unix, not(target_os = "redox")))]
|
||||
pub mod unix;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use linux::*;
|
||||
#[cfg(all(unix, not(target_os = "redox")))]
|
||||
pub use unix::*;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
// Linux-specific implementations for the rm utility
|
||||
// Unix-specific implementations for the rm utility
|
||||
|
||||
// spell-checker:ignore fstatat unlinkat statx behaviour
|
||||
|
||||
@@ -42,8 +42,8 @@ fn prompt_file_with_stat(path: &Path, stat: &libc::stat, options: &Options) -> b
|
||||
return true;
|
||||
}
|
||||
|
||||
let is_symlink = (stat.st_mode & libc::S_IFMT) == libc::S_IFLNK;
|
||||
let writable = mode_writable(stat.st_mode);
|
||||
let is_symlink = ((stat.st_mode as libc::mode_t) & libc::S_IFMT) == libc::S_IFLNK;
|
||||
let writable = mode_writable(stat.st_mode as libc::mode_t);
|
||||
let len = stat.st_size as u64;
|
||||
let stdin_ok = options.__presume_input_tty.unwrap_or(false) || stdin().is_terminal();
|
||||
|
||||
@@ -82,8 +82,8 @@ fn prompt_dir_with_mode(path: &Path, mode: libc::mode_t, options: &Options) -> b
|
||||
return true;
|
||||
}
|
||||
|
||||
let readable = mode_readable(mode);
|
||||
let writable = mode_writable(mode);
|
||||
let readable = mode_readable(mode as libc::mode_t);
|
||||
let writable = mode_writable(mode as libc::mode_t);
|
||||
let stdin_ok = options.__presume_input_tty.unwrap_or(false) || stdin().is_terminal();
|
||||
|
||||
match (stdin_ok, readable, writable, options.interactive) {
|
||||
@@ -317,7 +317,7 @@ pub fn safe_remove_dir_recursive(
|
||||
} else {
|
||||
// Ask user permission if needed
|
||||
if options.interactive == InteractiveMode::Always
|
||||
&& !prompt_dir_with_mode(path, initial_mode, options)
|
||||
&& !prompt_dir_with_mode(path, initial_mode as libc::mode_t, options)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -345,6 +345,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
|
||||
let entries = match dir_fd.read_dir() {
|
||||
@@ -376,7 +377,7 @@ pub fn safe_remove_dir_recursive_impl(path: &Path, dir_fd: &DirFd, options: &Opt
|
||||
};
|
||||
|
||||
// Check if it's a directory
|
||||
let is_dir = (entry_stat.st_mode & libc::S_IFMT) == libc::S_IFDIR;
|
||||
let is_dir = ((entry_stat.st_mode as libc::mode_t) & libc::S_IFMT) == libc::S_IFDIR;
|
||||
|
||||
if is_dir {
|
||||
// Ask user if they want to descend into this directory
|
||||
@@ -413,7 +414,7 @@ pub fn safe_remove_dir_recursive_impl(path: &Path, dir_fd: &DirFd, options: &Opt
|
||||
// Ask user permission if needed for this subdirectory
|
||||
if !child_error
|
||||
&& options.interactive == InteractiveMode::Always
|
||||
&& !prompt_dir_with_mode(&entry_path, entry_stat.st_mode, options)
|
||||
&& !prompt_dir_with_mode(&entry_path, entry_stat.st_mode as libc::mode_t, options)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -432,3 +433,10 @@ pub fn safe_remove_dir_recursive_impl(path: &Path, dir_fd: &DirFd, options: &Opt
|
||||
|
||||
error
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
pub fn safe_remove_dir_recursive_impl(_path: &Path, _dir_fd: &DirFd, _options: &Options) -> 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
|
||||
}
|
||||
+11
-21
@@ -26,7 +26,7 @@ use uucore::translate;
|
||||
use uucore::{format_usage, os_str_as_bytes, prompt_yes, show_error};
|
||||
|
||||
mod platform;
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(unix, not(target_os = "redox")))]
|
||||
use platform::{safe_remove_dir_recursive, safe_remove_empty_dir, safe_remove_file};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -538,17 +538,7 @@ fn is_readable_metadata(metadata: &Metadata) -> bool {
|
||||
}
|
||||
|
||||
/// Whether the given file or directory is readable.
|
||||
#[cfg(unix)]
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn is_readable(path: &Path) -> bool {
|
||||
match fs::metadata(path) {
|
||||
Err(_) => false,
|
||||
Ok(metadata) => is_readable_metadata(&metadata),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the given file or directory is readable.
|
||||
#[cfg(not(unix))]
|
||||
#[cfg(any(not(unix), target_os = "redox"))]
|
||||
fn is_readable(_path: &Path) -> bool {
|
||||
true
|
||||
}
|
||||
@@ -605,14 +595,14 @@ fn remove_dir_recursive(
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use secure traversal on Linux for all recursive directory removals
|
||||
#[cfg(target_os = "linux")]
|
||||
// Use secure traversal on Unix (except Redox) for all recursive directory removals
|
||||
#[cfg(all(unix, not(target_os = "redox")))]
|
||||
{
|
||||
safe_remove_dir_recursive(path, options, progress_bar)
|
||||
}
|
||||
|
||||
// Fallback for non-Linux or use fs::remove_dir_all for very long paths
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
// Fallback for non-Unix, Redox, or use fs::remove_dir_all for very long paths
|
||||
#[cfg(any(not(unix), target_os = "redox"))]
|
||||
{
|
||||
if let Some(s) = path.to_str() {
|
||||
if s.len() > 1000 {
|
||||
@@ -734,8 +724,8 @@ fn remove_dir(path: &Path, options: &Options, progress_bar: Option<&ProgressBar>
|
||||
return true;
|
||||
}
|
||||
|
||||
// Use safe traversal on Linux for empty directory removal
|
||||
#[cfg(target_os = "linux")]
|
||||
// Use safe traversal on Unix (except Redox) for empty directory removal
|
||||
#[cfg(all(unix, not(target_os = "redox")))]
|
||||
{
|
||||
if let Some(result) = safe_remove_empty_dir(path, options, progress_bar) {
|
||||
return result;
|
||||
@@ -758,15 +748,15 @@ fn remove_file(path: &Path, options: &Options, progress_bar: Option<&ProgressBar
|
||||
pb.inc(1);
|
||||
}
|
||||
|
||||
// Use safe traversal on Linux for individual file removal
|
||||
#[cfg(target_os = "linux")]
|
||||
// Use safe traversal on Unix (except Redox) for individual file removal
|
||||
#[cfg(all(unix, not(target_os = "redox")))]
|
||||
{
|
||||
if let Some(result) = safe_remove_file(path, options, progress_bar) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback method for non-Linux or when safe traversal is unavailable
|
||||
// Fallback method for non-Unix, Redox, or when safe traversal is unavailable
|
||||
match fs::remove_file(path) {
|
||||
Ok(_) => {
|
||||
verbose_removed_file(path, options);
|
||||
|
||||
@@ -1044,7 +1044,7 @@ impl Stater {
|
||||
'B' => OutputType::Unsigned(512),
|
||||
// SELinux security context string
|
||||
'C' => {
|
||||
#[cfg(feature = "selinux")]
|
||||
#[cfg(all(feature = "selinux", target_os = "linux"))]
|
||||
{
|
||||
if uucore::selinux::is_selinux_enabled() {
|
||||
match uucore::selinux::get_selinux_security_context(
|
||||
@@ -1060,7 +1060,7 @@ impl Stater {
|
||||
OutputType::Str(translate!("stat-selinux-unsupported-system"))
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "selinux"))]
|
||||
#[cfg(not(all(feature = "selinux", target_os = "linux")))]
|
||||
{
|
||||
OutputType::Str(translate!("stat-selinux-unsupported-os"))
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ pub mod pipes;
|
||||
pub mod proc_info;
|
||||
#[cfg(all(unix, feature = "process"))]
|
||||
pub mod process;
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(unix, not(target_os = "redox")))]
|
||||
pub mod safe_traversal;
|
||||
#[cfg(all(target_os = "linux", feature = "tty"))]
|
||||
pub mod tty;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// Safe directory traversal using openat() and related syscalls
|
||||
// This module provides TOCTOU-safe filesystem operations for recursive traversal
|
||||
//
|
||||
// Only available on Linux
|
||||
// Available on Unix
|
||||
//
|
||||
// spell-checker:ignore CLOEXEC RDONLY TOCTOU closedir dirp fdopendir fstatat openat REMOVEDIR unlinkat smallfile
|
||||
// spell-checker:ignore RAII dirfd fchownat fchown FchmodatFlags fchmodat fchmod
|
||||
@@ -85,15 +85,11 @@ fn read_dir_entries(fd: &OwnedFd) -> io::Result<Vec<OsString>> {
|
||||
|
||||
// Duplicate the fd for Dir (it takes ownership)
|
||||
let dup_fd = nix::unistd::dup(fd).map_err(|e| io::Error::from_raw_os_error(e as i32))?;
|
||||
|
||||
let mut dir = Dir::from_fd(dup_fd).map_err(|e| io::Error::from_raw_os_error(e as i32))?;
|
||||
|
||||
for entry_result in dir.iter() {
|
||||
let entry = entry_result.map_err(|e| io::Error::from_raw_os_error(e as i32))?;
|
||||
|
||||
let name = entry.file_name();
|
||||
let name_os = OsStr::from_bytes(name.to_bytes());
|
||||
|
||||
if name_os != "." && name_os != ".." {
|
||||
entries.push(name_os.to_os_string());
|
||||
}
|
||||
@@ -117,7 +113,6 @@ impl DirFd {
|
||||
source: io::Error::from_raw_os_error(e as i32),
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(Self { fd })
|
||||
}
|
||||
|
||||
@@ -125,7 +120,6 @@ impl DirFd {
|
||||
pub fn open_subdir(&self, name: &OsStr) -> io::Result<Self> {
|
||||
let name_cstr =
|
||||
CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?;
|
||||
|
||||
let flags = OFlag::O_RDONLY | OFlag::O_DIRECTORY | OFlag::O_CLOEXEC;
|
||||
let fd = openat(&self.fd, name_cstr.as_c_str(), flags, Mode::empty()).map_err(|e| {
|
||||
SafeTraversalError::OpenFailed {
|
||||
@@ -133,7 +127,6 @@ impl DirFd {
|
||||
source: io::Error::from_raw_os_error(e as i32),
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(Self { fd })
|
||||
}
|
||||
|
||||
@@ -174,7 +167,6 @@ impl DirFd {
|
||||
path: translate!("safe-traversal-current-directory").into(),
|
||||
source: io::Error::from_raw_os_error(e as i32),
|
||||
})?;
|
||||
|
||||
Ok(stat)
|
||||
}
|
||||
|
||||
@@ -254,7 +246,7 @@ impl DirFd {
|
||||
FchmodatFlags::NoFollowSymlink
|
||||
};
|
||||
|
||||
let mode = Mode::from_bits_truncate(mode);
|
||||
let mode = Mode::from_bits_truncate(mode as libc::mode_t);
|
||||
|
||||
let name_cstr =
|
||||
CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?;
|
||||
@@ -267,7 +259,7 @@ impl DirFd {
|
||||
|
||||
/// Change mode of this directory
|
||||
pub fn fchmod(&self, mode: u32) -> io::Result<()> {
|
||||
let mode = Mode::from_bits_truncate(mode);
|
||||
let mode = Mode::from_bits_truncate(mode as libc::mode_t);
|
||||
|
||||
nix::sys::stat::fchmod(&self.fd, mode)
|
||||
.map_err(|e| io::Error::from_raw_os_error(e as i32))?;
|
||||
@@ -378,30 +370,30 @@ impl Metadata {
|
||||
}
|
||||
|
||||
pub fn file_type(&self) -> FileType {
|
||||
FileType::from_mode(self.stat.st_mode)
|
||||
FileType::from_mode(self.stat.st_mode as libc::mode_t)
|
||||
}
|
||||
|
||||
pub fn file_info(&self) -> FileInfo {
|
||||
FileInfo::from_stat(&self.stat)
|
||||
}
|
||||
|
||||
// st_size type varies by platform (i64 vs u64)
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
pub fn size(&self) -> u64 {
|
||||
self.stat.st_size as u64
|
||||
}
|
||||
|
||||
// st_mode type varies by platform (u16 on macOS, u32 on Linux)
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
pub fn mode(&self) -> u32 {
|
||||
self.stat.st_mode
|
||||
self.stat.st_mode as u32
|
||||
}
|
||||
|
||||
pub fn nlink(&self) -> u64 {
|
||||
// st_nlink is u32 on most platforms except x86_64
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
// st_nlink type varies by platform (u16 on FreeBSD, u32/u64 on others)
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
{
|
||||
self.stat.st_nlink
|
||||
}
|
||||
#[cfg(not(target_arch = "x86_64"))]
|
||||
{
|
||||
self.stat.st_nlink.into()
|
||||
self.stat.st_nlink as u64
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,34 +413,31 @@ impl Metadata {
|
||||
|
||||
// Add MetadataExt trait implementation for compatibility
|
||||
impl std::os::unix::fs::MetadataExt for Metadata {
|
||||
// st_dev type varies by platform (i32 on macOS, u64 on Linux)
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
fn dev(&self) -> u64 {
|
||||
self.stat.st_dev
|
||||
self.stat.st_dev as u64
|
||||
}
|
||||
|
||||
fn ino(&self) -> u64 {
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
// st_ino type varies by platform (u32 on FreeBSD, u64 on Linux)
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
{
|
||||
self.stat.st_ino.into()
|
||||
}
|
||||
#[cfg(not(target_pointer_width = "32"))]
|
||||
{
|
||||
self.stat.st_ino
|
||||
self.stat.st_ino as u64
|
||||
}
|
||||
}
|
||||
|
||||
// st_mode type varies by platform (u16 on macOS, u32 on Linux)
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
fn mode(&self) -> u32 {
|
||||
self.stat.st_mode
|
||||
self.stat.st_mode as u32
|
||||
}
|
||||
|
||||
fn nlink(&self) -> u64 {
|
||||
// st_nlink is u32 on most platforms except x86_64
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
// st_nlink type varies by platform (u16 on FreeBSD, u32/u64 on others)
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
{
|
||||
self.stat.st_nlink
|
||||
}
|
||||
#[cfg(not(target_arch = "x86_64"))]
|
||||
{
|
||||
self.stat.st_nlink.into()
|
||||
self.stat.st_nlink as u64
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,10 +449,14 @@ impl std::os::unix::fs::MetadataExt for Metadata {
|
||||
self.stat.st_gid
|
||||
}
|
||||
|
||||
// st_rdev type varies by platform (i32 on macOS, u64 on Linux)
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
fn rdev(&self) -> u64 {
|
||||
self.stat.st_rdev
|
||||
self.stat.st_rdev as u64
|
||||
}
|
||||
|
||||
// st_size type varies by platform (i64 on some platforms, u64 on others)
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
fn size(&self) -> u64 {
|
||||
self.stat.st_size as u64
|
||||
}
|
||||
@@ -534,10 +527,14 @@ impl std::os::unix::fs::MetadataExt for Metadata {
|
||||
}
|
||||
}
|
||||
|
||||
// st_blksize type varies by platform (i32/i64/u32/u64 depending on platform)
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
fn blksize(&self) -> u64 {
|
||||
self.stat.st_blksize as u64
|
||||
}
|
||||
|
||||
// st_blocks type varies by platform (i64 on some platforms, u64 on others)
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
fn blocks(&self) -> u64 {
|
||||
self.stat.st_blocks as u64
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ pub use crate::features::perms;
|
||||
pub use crate::features::pipes;
|
||||
#[cfg(all(unix, feature = "process"))]
|
||||
pub use crate::features::process;
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(unix, not(target_os = "redox")))]
|
||||
pub use crate::features::safe_traversal;
|
||||
#[cfg(all(unix, not(target_os = "fuchsia"), feature = "signals"))]
|
||||
pub use crate::features::signals;
|
||||
|
||||
@@ -390,10 +390,12 @@ fn test_chmod_recursive_correct_exit_code() {
|
||||
perms.set_mode(0o000);
|
||||
set_permissions(at.plus_as_string("a"), perms).unwrap();
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let err_msg = "chmod: Permission denied\n";
|
||||
#[cfg(target_os = "linux")]
|
||||
// With safe_traversal enabled on all Unix platforms (except Redox),
|
||||
// we get detailed error messages that include the file path
|
||||
#[cfg(all(unix, not(target_os = "redox")))]
|
||||
let err_msg = "chmod: cannot access 'a': Permission denied\n";
|
||||
#[cfg(not(all(unix, not(target_os = "redox"))))]
|
||||
let err_msg = "chmod: Permission denied\n";
|
||||
|
||||
// order of command is a, a/b then c
|
||||
// command is expected to fail and not just take the last exit code
|
||||
@@ -434,9 +436,8 @@ fn test_chmod_recursive() {
|
||||
make_file(&at.plus_as_string("a/b/b"), 0o100444);
|
||||
make_file(&at.plus_as_string("a/b/c/c"), 0o100444);
|
||||
make_file(&at.plus_as_string("z/y"), 0o100444);
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let err_msg = "chmod: Permission denied\n";
|
||||
#[cfg(target_os = "linux")]
|
||||
// With safe_traversal enabled on all Unix platforms, the error message
|
||||
// now includes the file path consistently across platforms
|
||||
let err_msg = "chmod: cannot access 'z': Permission denied\n";
|
||||
|
||||
// only the permissions of folder `a` and `z` are changed
|
||||
|
||||
Reference in New Issue
Block a user