Low severity3.6NVD Advisory· Published Apr 22, 2026· Updated Apr 27, 2026
CVE-2026-35362
CVE-2026-35362
Description
The safe_traversal module in uutils coreutils, which provides protection against Time-of-Check to Time-of-Use (TOCTOU) symlink races using file-descriptor-relative syscalls, is incorrectly limited to Linux targets. On other Unix-like systems such as macOS and FreeBSD, the utility fails to utilize these protections, leaving directory traversal operations vulnerable to symlink race conditions.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
coreutilscrates.io | < 0.6.0 | 0.6.0 |
Affected products
1Patches
130239e69a328feat: Expand safe directory traversal to all Unix platforms and fix related type conversions. (#9792)
17 files changed · +145 −130
src/uu/chmod/Cargo.toml+4 −7 modified@@ -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"
src/uu/chmod/src/chmod.rs+20 −13 modified@@ -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,17 +450,24 @@ 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); } } } 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)
src/uucore/src/lib/features.rs+1 −1 modified@@ -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;
src/uucore/src/lib/features/safe_traversal.rs+33 −36 modified@@ -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,23 +113,20 @@ impl DirFd { source: io::Error::from_raw_os_error(e as i32), } })?; - Ok(Self { fd }) } /// Open a subdirectory relative to this directory 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 { path: name.into(), 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")] - { - self.stat.st_nlink - } - #[cfg(not(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.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")] - { - self.stat.st_ino.into() - } - #[cfg(not(target_pointer_width = "32"))] + // st_ino type varies by platform (u32 on FreeBSD, u64 on Linux) + #[allow(clippy::unnecessary_cast)] { - 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")] - { - self.stat.st_nlink - } - #[cfg(not(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.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 }
src/uucore/src/lib/lib.rs+1 −1 modified@@ -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;
src/uu/cp/src/cp.rs+5 −2 modified@@ -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
src/uu/du/Cargo.toml+3 −1 modified@@ -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",
src/uu/du/src/du.rs+20 −14 modified@@ -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());
src/uu/install/src/install.rs+12 −12 modified@@ -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)
src/uu/mkfifo/src/mkfifo.rs+1 −1 modified@@ -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);
src/uu/rm/Cargo.toml+4 −1 modified@@ -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 }
src/uu/rm/src/platform/mod.rs+4 −4 modified@@ -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::*;
src/uu/rm/src/platform/unix.rs+16 −8 renamed@@ -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 +}
src/uu/rm/src/rm.rs+11 −21 modified@@ -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);
src/uu/stat/src/stat.rs+2 −2 modified@@ -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")) }
tests/by-util/test_chmod.rs+7 −6 modified@@ -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
.vscode/cspell.dictionaries/acronyms+names.wordlist.txt+1 −0 modified@@ -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
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/uutils/coreutils/pull/9792nvdIssue TrackingPatchWEB
- github.com/advisories/GHSA-ggc5-46rg-mr4vghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-35362ghsaADVISORY
- github.com/uutils/coreutils/commit/30239e69a328e76d2377f2a0bc02fbde61c34280ghsaWEB
- github.com/uutils/coreutils/releases/tag/0.6.0nvdRelease NotesWEB
News mentions
0No linked articles in our index yet.