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:
Jake Abendroth
2026-01-09 08:22:56 +01:00
committed by GitHub
co-authored by Sylvestre Ledru
parent 2b1d40af7c
commit 30239e69a3
17 changed files with 145 additions and 130 deletions
@@ -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
+4 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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
+3 -1
View File
@@ -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
View File
@@ -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());
+12 -12
View File
@@ -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)
+1 -1
View File
@@ -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);
+4 -1
View File
@@ -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 }
+4 -4
View File
@@ -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
View File
@@ -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);
+2 -2
View File
@@ -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"))
}
+1 -1
View File
@@ -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;
+33 -36
View File
@@ -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
}
+1 -1
View File
@@ -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;
+7 -6
View File
@@ -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