From 533318019ef4557656e3ff0582634217588a84d3 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Wed, 18 Mar 2026 23:40:54 +0100 Subject: [PATCH 01/21] sort: add collation key arena storage to LineData Add collation_key_buffer (arena) and collation_key_ends (offsets) to LineData and RecycledChunk, with a collation_key() accessor. All sort keys for a chunk are stored in a single Vec to avoid millions of small heap allocations. --- src/uu/sort/src/chunks.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/uu/sort/src/chunks.rs b/src/uu/sort/src/chunks.rs index c808c622b81..ec80dfdf4e2 100644 --- a/src/uu/sort/src/chunks.rs +++ b/src/uu/sort/src/chunks.rs @@ -62,6 +62,11 @@ pub struct LineData<'a> { impl LineData<'_> { /// Get the collation sort key for a line at the given index. pub fn collation_key(&self, index: usize) -> &[u8] { + debug_assert!( + index < self.collation_key_ends.len(), + "collation_key index {index} out of bounds (len {})", + self.collation_key_ends.len() + ); let start = if index == 0 { 0 } else { From 6ca91463300a61ea5c020607f773eacea2525b78 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 27 Mar 2026 12:16:11 +0100 Subject: [PATCH 02/21] deps: add rustix as workspace dependency Add rustix 1.1.4 with default-features = false to prepare for migrating from nix to rustix for safer syscall bindings. --- Cargo.toml | 2 +- fuzz/Cargo.lock | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4f98541268e..cb072dbff82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -429,7 +429,7 @@ rstest = "0.26.0" rstest_reuse = "0.7.0" rustc-hash = "2.1.1" rust-ini = "0.21.0" -rustix = "1.1.4" +rustix = { version = "1.1.4", default-features = false } same-file = "1.0.6" self_cell = "1.0.4" selinux = "=0.6.0" diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index ac43508bf3a..98097dbd458 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -1967,9 +1967,10 @@ dependencies = [ "icu_locale", "jiff", "jiff-icu", - "nix", + "libc", "parse_datetime", "regex", + "rustix", "uucore", "windows-sys 0.61.2", ] @@ -1998,7 +1999,7 @@ version = "0.8.0" dependencies = [ "clap", "fluent", - "nix", + "libc", "rust-ini", "thiserror", "uucore", @@ -2051,10 +2052,11 @@ dependencies = [ "fluent", "foldhash 0.2.0", "itertools", + "libc", "memchr", - "nix", "rand 0.10.0", "rayon", + "rustix", "self_cell", "tempfile", "thiserror", @@ -2102,7 +2104,7 @@ dependencies = [ "clap", "fluent", "libc", - "nix", + "rustix", "thiserror", "unicode-width", "uucore", @@ -2139,11 +2141,11 @@ dependencies = [ "libc", "md-5", "memchr", - "nix", "num-traits", "os_display", "procfs", "rustc-hash", + "rustix", "sha1", "sha2", "sha3", From bf79152e5ed037b32ebac54651112a6ce0c2e26e Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 27 Mar 2026 12:25:52 +0100 Subject: [PATCH 03/21] refactor: replace nix::libc re-exports with direct libc imports Replace all `nix::libc::*` usages with direct `libc::*` imports to decouple libc access from the nix crate, as preparation for migrating from nix to rustix. Add direct libc dependency to crates that previously relied on nix's libc re-export: stty, env, sort, date, mknod, touch. --- Cargo.lock | 6 ++++++ src/uu/date/Cargo.toml | 1 + src/uu/date/src/locale.rs | 1 - src/uu/env/Cargo.toml | 1 + src/uu/env/src/env.rs | 2 -- src/uu/mknod/Cargo.toml | 1 + src/uu/mknod/src/mknod.rs | 2 +- src/uu/sort/Cargo.toml | 1 + src/uu/sort/src/sort.rs | 3 --- src/uu/stty/Cargo.toml | 1 + src/uu/stty/src/stty.rs | 10 +++++----- src/uu/touch/Cargo.toml | 1 + src/uu/touch/src/touch.rs | 2 +- src/uucore/src/lib/features/safe_traversal.rs | 1 - src/uucore/src/lib/features/signals.rs | 3 --- src/uucore/src/lib/mods/panic.rs | 2 -- tests/by-util/test_env.rs | 2 -- 17 files changed, 19 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb64f01c704..96212ca832e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3431,6 +3431,7 @@ dependencies = [ "icu_locale", "jiff", "jiff-icu", + "libc", "nix", "parse_datetime", "regex", @@ -3525,6 +3526,7 @@ version = "0.8.0" dependencies = [ "clap", "fluent", + "libc", "nix", "rust-ini", "thiserror 2.0.18", @@ -3778,6 +3780,7 @@ version = "0.8.0" dependencies = [ "clap", "fluent", + "libc", "nix", "uucore", ] @@ -4136,6 +4139,7 @@ dependencies = [ "fluent", "foldhash 0.2.0", "itertools 0.14.0", + "libc", "memchr", "nix", "rand 0.10.0", @@ -4196,6 +4200,7 @@ dependencies = [ "cfg_aliases", "clap", "fluent", + "libc", "nix", "uucore", ] @@ -4291,6 +4296,7 @@ dependencies = [ "filetime", "fluent", "jiff", + "libc", "nix", "parse_datetime", "tempfile", diff --git a/src/uu/date/Cargo.toml b/src/uu/date/Cargo.toml index 36604d0c337..7f0b18905c5 100644 --- a/src/uu/date/Cargo.toml +++ b/src/uu/date/Cargo.toml @@ -44,6 +44,7 @@ regex = { workspace = true } uucore = { workspace = true, features = ["parser", "i18n-datetime"] } [target.'cfg(unix)'.dependencies] +libc = { workspace = true } nix = { workspace = true, features = ["time"] } [target.'cfg(windows)'.dependencies] diff --git a/src/uu/date/src/locale.rs b/src/uu/date/src/locale.rs index 8d1c0b0133e..d5a044c734b 100644 --- a/src/uu/date/src/locale.rs +++ b/src/uu/date/src/locale.rs @@ -28,7 +28,6 @@ macro_rules! cfg_langinfo { cfg_langinfo! { use std::ffi::CStr; use std::sync::OnceLock; - use nix::libc; #[cfg(test)] use std::sync::Mutex; diff --git a/src/uu/env/Cargo.toml b/src/uu/env/Cargo.toml index b6b5a8cce8a..fda68b1ae50 100644 --- a/src/uu/env/Cargo.toml +++ b/src/uu/env/Cargo.toml @@ -26,6 +26,7 @@ uucore = { workspace = true, features = ["signals"] } fluent = { workspace = true } [target.'cfg(unix)'.dependencies] +libc = { workspace = true } nix = { workspace = true, features = ["signal"] } [[bin]] diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index 7b36c7c6355..2f116b82932 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -18,8 +18,6 @@ use native_int_str::{ Convert, NCvt, NativeIntStr, NativeIntString, NativeStr, from_native_int_representation_owned, }; #[cfg(unix)] -use nix::libc; -#[cfg(unix)] use nix::sys::signal::{ SigHandler::{SigDfl, SigIgn}, SigSet, SigmaskHow, Signal, signal, sigprocmask, diff --git a/src/uu/mknod/Cargo.toml b/src/uu/mknod/Cargo.toml index 2bdcfe5ad91..d50a5ff7322 100644 --- a/src/uu/mknod/Cargo.toml +++ b/src/uu/mknod/Cargo.toml @@ -23,6 +23,7 @@ path = "src/mknod.rs" clap = { workspace = true } uucore = { workspace = true, features = ["mode", "fs"] } fluent = { workspace = true } +libc = { workspace = true } nix = { workspace = true } [features] diff --git a/src/uu/mknod/src/mknod.rs b/src/uu/mknod/src/mknod.rs index 9b6ab45f648..f636fb6ca76 100644 --- a/src/uu/mknod/src/mknod.rs +++ b/src/uu/mknod/src/mknod.rs @@ -6,7 +6,7 @@ // spell-checker:ignore (ToDO) parsemode makedev sysmacros perror IFBLK IFCHR IFIFO sflag use clap::{Arg, ArgAction, Command, value_parser}; -use nix::libc::{S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR, mode_t}; +use libc::{S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR, mode_t}; use nix::sys::stat::{Mode, SFlag, mknod as nix_mknod, umask as nix_umask}; use uucore::display::Quotable; diff --git a/src/uu/sort/Cargo.toml b/src/uu/sort/Cargo.toml index c648763764f..c01f790c514 100644 --- a/src/uu/sort/Cargo.toml +++ b/src/uu/sort/Cargo.toml @@ -50,6 +50,7 @@ foldhash = { workspace = true } ctrlc = { workspace = true } [target.'cfg(unix)'.dependencies] +libc = { workspace = true } nix = { workspace = true, features = ["resource"] } [dev-dependencies] diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index 201267906bb..a7a315cd932 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -1412,8 +1412,6 @@ pub(crate) fn fd_soft_limit() -> Option { #[cfg(unix)] pub(crate) fn current_open_fd_count() -> Option { - use nix::libc; - fn count_dir(path: &str) -> Option { let entries = std::fs::read_dir(path).ok()?; let mut count = 0usize; @@ -1772,7 +1770,6 @@ fn locale_failed_to_set() -> bool { #[cfg(unix)] fn locale_failed_to_set() -> bool { - use nix::libc; unsafe { libc::setlocale(libc::LC_COLLATE, c"".as_ptr()).is_null() } } diff --git a/src/uu/stty/Cargo.toml b/src/uu/stty/Cargo.toml index f6c472a891d..73d26d34465 100644 --- a/src/uu/stty/Cargo.toml +++ b/src/uu/stty/Cargo.toml @@ -24,6 +24,7 @@ uucore = { workspace = true, features = ["parser"] } fluent = { workspace = true } [target.'cfg(unix)'.dependencies] +libc = { workspace = true } nix = { workspace = true, features = ["ioctl", "term"] } [[bin]] diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index 6909d2c4f5f..a6cbeb97b2b 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -19,14 +19,14 @@ mod flags; use crate::flags::AllFlags; use crate::flags::COMBINATION_SETTINGS; use clap::{Arg, ArgAction, ArgMatches, Command}; -use nix::libc::{O_NONBLOCK, TIOCGWINSZ, TIOCSWINSZ, c_ushort}; +use libc::{O_NONBLOCK, TIOCGWINSZ, TIOCSWINSZ, c_ushort}; #[cfg(all( target_os = "linux", not(target_arch = "powerpc"), not(target_arch = "powerpc64") ))] -use nix::libc::{TCGETS2, termios2}; +use libc::{TCGETS2, termios2}; use nix::sys::termios::{ ControlFlags, InputFlags, LocalFlags, OutputFlags, SetArg, SpecialCharacterIndices as S, @@ -520,7 +520,7 @@ fn parse_rows_cols(arg: &str) -> Option { /// - Returns `None` if format is invalid fn parse_saved_state(arg: &str) -> Option> { let parts: Vec<&str> = arg.split(':').collect(); - let expected_parts = 4 + nix::libc::NCCS; + let expected_parts = 4 + libc::NCCS; // GNU requires exactly the right number of parts for this platform if parts.len() != expected_parts { @@ -692,7 +692,7 @@ fn print_terminal_size( { // For some reason the normal nix Termios struct does not expose the line, // so we get the underlying libc::termios struct to get that information. - let libc_termios: nix::libc::termios = termios.clone().into(); + let libc_termios: libc::termios = termios.clone().into(); let line = libc_termios.c_line; printer.print(&translate!("stty-output-line", "line" => line)); } @@ -824,7 +824,7 @@ fn string_to_flag(option: &str) -> Option> { None } -fn control_char_to_string(cc: nix::libc::cc_t) -> nix::Result { +fn control_char_to_string(cc: libc::cc_t) -> nix::Result { if cc == 0 { return Ok(translate!("stty-output-undef")); } diff --git a/src/uu/touch/Cargo.toml b/src/uu/touch/Cargo.toml index cbcda5ae4a2..4a93a3112ae 100644 --- a/src/uu/touch/Cargo.toml +++ b/src/uu/touch/Cargo.toml @@ -29,6 +29,7 @@ uucore = { workspace = true, features = ["libc", "parser"] } fluent = { workspace = true } [target.'cfg(unix)'.dependencies] +libc = { workspace = true } nix = { workspace = true, features = ["fs"] } [dev-dependencies] diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index d1080c1dddf..45c6c1cd1ea 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -16,7 +16,7 @@ use jiff::fmt::strtime; use jiff::tz::TimeZone; use jiff::{Timestamp, ToSpan, Zoned}; #[cfg(unix)] -use nix::libc::O_NONBLOCK; +use libc::O_NONBLOCK; #[cfg(unix)] use nix::sys::stat::futimens; #[cfg(unix)] diff --git a/src/uucore/src/lib/features/safe_traversal.rs b/src/uucore/src/lib/features/safe_traversal.rs index 98fc81dd928..f8aaa160968 100644 --- a/src/uucore/src/lib/features/safe_traversal.rs +++ b/src/uucore/src/lib/features/safe_traversal.rs @@ -24,7 +24,6 @@ use std::path::{Path, PathBuf}; use nix::dir::Dir; use nix::fcntl::{OFlag, openat}; -use nix::libc; use nix::sys::stat::{FchmodatFlags, FileStat, Mode, fchmodat, fstatat, mkdirat}; use nix::unistd::{Gid, Uid, UnlinkatFlags, fchown, fchownat, unlinkat}; use os_display::Quotable; diff --git a/src/uucore/src/lib/features/signals.rs b/src/uucore/src/lib/features/signals.rs index a5141f299e3..99305e990d9 100644 --- a/src/uucore/src/lib/features/signals.rs +++ b/src/uucore/src/lib/features/signals.rs @@ -12,8 +12,6 @@ #[cfg(unix)] use nix::errno::Errno; -#[cfg(any(target_os = "linux", target_os = "android"))] -use nix::libc; #[cfg(unix)] use nix::sys::signal::{ SaFlags, SigAction, SigHandler, SigHandler::SigDfl, SigHandler::SigIgn, SigSet, Signal, @@ -545,7 +543,6 @@ static SIGPIPE_WAS_IGNORED: AtomicBool = AtomicBool::new(false); #[cfg(unix)] #[allow(clippy::missing_safety_doc)] pub unsafe extern "C" fn capture_startup_state() { - use nix::libc; use std::mem::MaybeUninit; use std::ptr; diff --git a/src/uucore/src/lib/mods/panic.rs b/src/uucore/src/lib/mods/panic.rs index 2a67a10ba8d..c5917f173a9 100644 --- a/src/uucore/src/lib/mods/panic.rs +++ b/src/uucore/src/lib/mods/panic.rs @@ -52,8 +52,6 @@ pub fn mute_sigpipe_panic() { /// variable. If set to "default", it restores SIGPIPE to SIG_DFL. #[cfg(unix)] pub fn preserve_inherited_sigpipe() { - use nix::libc; - // Check if parent specified that SIGPIPE should be default if let Ok(val) = std::env::var("RUST_SIGPIPE") { if val == "default" { diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index 4a989bd053b..dbb0f9a515a 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.rs @@ -5,8 +5,6 @@ // spell-checker:ignore (words) bamf chdir rlimit prlimit COMSPEC cout cerr FFFD winsize xpixel ypixel Secho sighandler #![allow(clippy::missing_errors_doc)] -#[cfg(unix)] -use nix::libc; #[cfg(unix)] use nix::sys::signal::Signal; #[cfg(feature = "echo")] From 090171628109533321667cff0d3465e934964b9c Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 27 Mar 2026 14:20:10 +0100 Subject: [PATCH 04/21] refactor: add libc-based signal wrappers in uucore::signals::csignal Add a `csignal` module with thin libc-based wrappers for signal operations that rustix does not cover: signal(), sigaction(), sigprocmask(), SigSet, SigHandler, and SigmaskHow. These wrappers will replace nix's signal module as part of the nix-to-rustix migration. Also add libc as a non-optional unix dependency for uucore. --- src/uucore/Cargo.toml | 1 + src/uucore/src/lib/features/signals.rs | 167 +++++++++++++++++++++++++ 2 files changed, 168 insertions(+) diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 2f11ecfd30a..5b194de3b78 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -94,6 +94,7 @@ fluent-bundle = { workspace = true } thiserror = { workspace = true } [target.'cfg(unix)'.dependencies] +libc = { workspace = true } nix = { workspace = true, features = [ "dir", "fs", diff --git a/src/uucore/src/lib/features/signals.rs b/src/uucore/src/lib/features/signals.rs index 99305e990d9..52aa04a809a 100644 --- a/src/uucore/src/lib/features/signals.rs +++ b/src/uucore/src/lib/features/signals.rs @@ -18,6 +18,173 @@ use nix::sys::signal::{ Signal::SIGINT, Signal::SIGPIPE, sigaction, signal, }; +// --------------------------------------------------------------------------- +// Thin libc-based signal wrappers +// +// These replace nix's signal module for operations that rustix does not cover +// (sigaction, signal(), SigSet, SigHandler, sigprocmask). +// --------------------------------------------------------------------------- +#[cfg(unix)] +pub mod csignal { + use std::io; + use std::mem::MaybeUninit; + + /// Signal handler disposition. + #[derive(Debug, Clone, Copy)] + pub enum SigHandler { + /// Default signal action. + SigDfl, + /// Ignore the signal. + SigIgn, + /// Call the given handler function. + Handler(extern "C" fn(libc::c_int)), + } + + impl SigHandler { + fn to_sigaction_handler(self) -> libc::sighandler_t { + match self { + Self::SigDfl => libc::SIG_DFL, + Self::SigIgn => libc::SIG_IGN, + Self::Handler(f) => f as libc::sighandler_t, + } + } + + fn from_sigaction_handler(handler: libc::sighandler_t) -> Self { + if handler == libc::SIG_DFL { + Self::SigDfl + } else if handler == libc::SIG_IGN { + Self::SigIgn + } else { + // SAFETY: the handler is a valid function pointer set by a previous signal() call + Self::Handler(unsafe { + std::mem::transmute::(handler) + }) + } + } + } + + /// Wrapper around `libc::sigset_t`. + #[derive(Clone)] + pub struct SigSet(libc::sigset_t); + + impl SigSet { + /// Creates an empty signal set. + pub fn empty() -> Self { + let mut set = MaybeUninit::::uninit(); + // SAFETY: sigemptyset initializes the sigset_t + unsafe { libc::sigemptyset(set.as_mut_ptr()) }; + Self(unsafe { set.assume_init() }) + } + + /// Creates a signal set with all signals. + pub fn all() -> Self { + let mut set = MaybeUninit::::uninit(); + // SAFETY: sigfillset initializes the sigset_t + unsafe { libc::sigfillset(set.as_mut_ptr()) }; + Self(unsafe { set.assume_init() }) + } + + /// Adds a signal to the set. + pub fn add(&mut self, signum: libc::c_int) { + // SAFETY: sigaddset operates on a valid sigset_t + unsafe { libc::sigaddset(&raw mut self.0, signum) }; + } + + /// Returns a pointer to the inner sigset_t. + pub fn as_ptr(&self) -> *const libc::sigset_t { + &raw const self.0 + } + } + + /// Flags for `sigaction`. + pub mod sa_flags { + pub const SA_RESTART: libc::c_int = libc::SA_RESTART as libc::c_int; + } + + /// How to modify the signal mask in `sigprocmask`. + #[derive(Debug, Clone, Copy)] + pub enum SigmaskHow { + /// Block the signals in the set. + Block, + /// Unblock the signals in the set. + Unblock, + /// Set the signal mask to the given set. + SetMask, + } + + impl SigmaskHow { + fn as_libc(self) -> libc::c_int { + match self { + Self::Block => libc::SIG_BLOCK, + Self::Unblock => libc::SIG_UNBLOCK, + Self::SetMask => libc::SIG_SETMASK, + } + } + } + + fn last_os_error() -> io::Error { + io::Error::last_os_error() + } + + /// Set the disposition of a signal using `libc::signal()`. + /// + /// Returns the previous signal handler. + /// + /// # Safety + /// If `handler` is `SigHandler::Handler(f)`, the function `f` must be + /// async-signal-safe. + pub unsafe fn set_signal_handler( + signum: libc::c_int, + handler: SigHandler, + ) -> io::Result { + let prev = unsafe { libc::signal(signum, handler.to_sigaction_handler()) }; + if prev == libc::SIG_ERR { + Err(last_os_error()) + } else { + Ok(SigHandler::from_sigaction_handler(prev)) + } + } + + /// Install a signal action using `libc::sigaction()`. + /// + /// # Safety + /// If the handler is a function pointer, it must be async-signal-safe. + pub unsafe fn set_signal_action( + signum: libc::c_int, + handler: SigHandler, + flags: libc::c_int, + mask: &SigSet, + ) -> io::Result<()> { + let mut sa: libc::sigaction = unsafe { std::mem::zeroed() }; + sa.sa_sigaction = handler.to_sigaction_handler(); + sa.sa_flags = flags as _; + sa.sa_mask = mask.0; + + let ret = unsafe { libc::sigaction(signum, &raw const sa, std::ptr::null_mut()) }; + if ret == -1 { + Err(last_os_error()) + } else { + Ok(()) + } + } + + /// Block/unblock/set signal mask using `libc::sigprocmask()`. + pub fn sigprocmask( + how: SigmaskHow, + set: Option<&SigSet>, + oldset: Option<&mut SigSet>, + ) -> io::Result<()> { + let set_ptr = set.map_or(std::ptr::null(), SigSet::as_ptr); + let oldset_ptr = oldset.map_or(std::ptr::null_mut(), |s| &raw mut s.0); + let ret = unsafe { libc::sigprocmask(how.as_libc(), set_ptr, oldset_ptr) }; + if ret == -1 { + Err(last_os_error()) + } else { + Ok(()) + } + } +} + /// The default signal value. pub static DEFAULT_SIGNAL: usize = 15; From 1efa0ac0f285847e6f3fd72cfb61e00c1cded9f4 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 28 Mar 2026 11:05:44 +0100 Subject: [PATCH 05/21] refactor: migrate pipes.rs and cat splice.rs from nix to rustix Replace nix pipe/splice/vmsplice wrappers with rustix equivalents in uucore's pipes module. Also migrate cat's splice.rs to use rustix::io::{read, write} instead of nix::unistd. The return types change from nix::Result to std::io::Result, which is more idiomatic and compatible with the broader ecosystem. --- Cargo.lock | 2 ++ src/uu/cat/Cargo.toml | 1 + src/uu/cat/src/splice.rs | 16 ++++++++-------- src/uucore/Cargo.toml | 1 + src/uucore/src/lib/features/pipes.rs | 27 ++++++++++++++++----------- 5 files changed, 28 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 96212ca832e..6b053d99e63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3289,6 +3289,7 @@ dependencies = [ "fluent", "memchr", "nix", + "rustix", "tempfile", "thiserror 2.0.18", "uucore", @@ -4514,6 +4515,7 @@ dependencies = [ "os_display", "procfs", "rustc-hash", + "rustix", "selinux", "sha1", "sha2", diff --git a/src/uu/cat/Cargo.toml b/src/uu/cat/Cargo.toml index 753efdbde52..9f4626f75cb 100644 --- a/src/uu/cat/Cargo.toml +++ b/src/uu/cat/Cargo.toml @@ -27,6 +27,7 @@ fluent = { workspace = true } [target.'cfg(unix)'.dependencies] nix = { workspace = true } +rustix = { workspace = true } [target.'cfg(windows)'.dependencies] winapi-util = { workspace = true } diff --git a/src/uu/cat/src/splice.rs b/src/uu/cat/src/splice.rs index ca5265d2bf8..7acfb613ebd 100644 --- a/src/uu/cat/src/splice.rs +++ b/src/uu/cat/src/splice.rs @@ -4,7 +4,7 @@ // file that was distributed with this source code. use super::{CatResult, FdReadable, InputHandle}; -use nix::unistd; +use rustix::io::{read, write}; use std::os::{fd::AsFd, unix::io::AsRawFd}; use uucore::pipes::{pipe, splice, splice_exact}; @@ -52,20 +52,20 @@ pub(super) fn write_fast_using_splice( /// Move exactly `num_bytes` bytes from `read_fd` to `write_fd`. /// /// Panics if not enough bytes can be read. -fn copy_exact(read_fd: &impl AsFd, write_fd: &impl AsFd, num_bytes: usize) -> nix::Result<()> { +fn copy_exact(read_fd: &impl AsFd, write_fd: &impl AsFd, num_bytes: usize) -> std::io::Result<()> { let mut left = num_bytes; let mut buf = [0; BUF_SIZE]; while left > 0 { - let read = unistd::read(read_fd, &mut buf)?; - assert_ne!(read, 0, "unexpected end of pipe"); + let n = read(read_fd, &mut buf)?; + assert_ne!(n, 0, "unexpected end of pipe"); let mut written = 0; - while written < read { - match unistd::write(write_fd, &buf[written..read])? { + while written < n { + match write(write_fd, &buf[written..n])? { 0 => panic!(), - n => written += n, + w => written += w, } } - left -= read; + left -= n; } Ok(()) } diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 5b194de3b78..caed3aab90c 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -104,6 +104,7 @@ nix = { workspace = true, features = [ "user", "zerocopy", ] } +rustix = { workspace = true, features = ["pipe", "process", "fs", "event", "termios", "time"] } walkdir = { workspace = true, optional = true } xattr = { workspace = true, optional = true } diff --git a/src/uucore/src/lib/features/pipes.rs b/src/uucore/src/lib/features/pipes.rs index 069736c5215..09ef34e298d 100644 --- a/src/uucore/src/lib/features/pipes.rs +++ b/src/uucore/src/lib/features/pipes.rs @@ -3,27 +3,25 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -//! Thin pipe-related wrappers around functions from the `nix` crate. +//! Thin pipe-related wrappers around functions from the `rustix` crate. use std::fs::File; #[cfg(any(target_os = "linux", target_os = "android"))] use std::os::fd::AsFd; #[cfg(any(target_os = "linux", target_os = "android"))] -use nix::fcntl::SpliceFFlags; +use rustix::pipe::SpliceFlags; -pub use nix::{Error, Result}; - -/// A wrapper around [`nix::unistd::pipe`] that ensures the pipe is cleaned up. +/// A wrapper around [`rustix::pipe::pipe`] that ensures the pipe is cleaned up. /// /// Returns two `File` objects: everything written to the second can be read /// from the first. -pub fn pipe() -> Result<(File, File)> { - let (read, write) = nix::unistd::pipe()?; +pub fn pipe() -> std::io::Result<(File, File)> { + let (read, write) = rustix::pipe::pipe()?; Ok((File::from(read), File::from(write))) } -/// Less noisy wrapper around [`nix::fcntl::splice`]. +/// Less noisy wrapper around [`rustix::pipe::splice`]. /// /// Up to `len` bytes are moved from `source` to `target`. Returns the number /// of successfully moved bytes. @@ -33,8 +31,15 @@ pub fn pipe() -> Result<(File, File)> { /// a [`pipe`] and then from the pipe into your target (with `splice_exact`): /// this is still very efficient. #[cfg(any(target_os = "linux", target_os = "android"))] -pub fn splice(source: &impl AsFd, target: &impl AsFd, len: usize) -> Result { - nix::fcntl::splice(source, None, target, None, len, SpliceFFlags::empty()) +pub fn splice(source: &impl AsFd, target: &impl AsFd, len: usize) -> std::io::Result { + Ok(rustix::pipe::splice( + source, + None, + target, + None, + len, + SpliceFlags::empty(), + )?) } /// Splice wrapper which fully finishes the write. @@ -43,7 +48,7 @@ pub fn splice(source: &impl AsFd, target: &impl AsFd, len: usize) -> Result Result<()> { +pub fn splice_exact(source: &impl AsFd, target: &impl AsFd, len: usize) -> std::io::Result<()> { let mut left = len; while left != 0 { let written = splice(source, target, left)?; From ae2ecc5d6e3e9a539b7d958eb206dab71462fd47 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 28 Mar 2026 11:13:24 +0100 Subject: [PATCH 06/21] refactor: migrate uucore process.rs from nix to rustix Replace nix process functions with rustix equivalents: - geteuid/getegid/getuid/getgid/getpid/getpgrp/getsid -> rustix::process - kill/test_kill -> rustix::process::kill_process/test_kill_process - Signal::try_from -> signal_from_raw helper using from_raw_unchecked - SigHandler for send_signal_group -> csignal wrapper The getsid return type changes from Result to io::Result, which is more idiomatic. --- src/uucore/src/lib/features/process.rs | 77 ++++++++++++++------------ 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/src/uucore/src/lib/features/process.rs b/src/uucore/src/lib/features/process.rs index 57a2e846554..d6bd35043a1 100644 --- a/src/uucore/src/lib/features/process.rs +++ b/src/uucore/src/lib/features/process.rs @@ -8,10 +8,7 @@ // spell-checker:ignore pgrep pwait snice getpgrp use libc::{gid_t, pid_t, uid_t}; -#[cfg(not(target_os = "redox"))] -use nix::errno::Errno; -use nix::sys::signal::{self as nix_signal, SigHandler, Signal}; -use nix::unistd::Pid; +use rustix::process::{self as rprocess, Pid, Signal}; use std::io; use std::process::Child; use std::process::ExitStatus; @@ -20,35 +17,47 @@ use std::sync::atomic::AtomicBool; use std::thread; use std::time::{Duration, Instant}; +use crate::signals::csignal; + +/// Convert a raw signal number to a `Signal`, returning an error for invalid values. +fn signal_from_raw(sig: i32) -> io::Result { + if sig > 0 { + // SAFETY: we've checked the value is positive; the kernel will reject + // truly invalid signal numbers. + Ok(unsafe { Signal::from_raw_unchecked(sig) }) + } else { + Err(io::Error::from_raw_os_error(libc::EINVAL)) + } +} + /// `geteuid()` returns the effective user ID of the calling process. pub fn geteuid() -> uid_t { - nix::unistd::geteuid().as_raw() + rprocess::geteuid().as_raw() } /// `getpgrp()` returns the process group ID of the calling process. -/// It is a trivial wrapper over nix::unistd::getpgrp. pub fn getpgrp() -> pid_t { - nix::unistd::getpgrp().as_raw() + rprocess::getpgrp().as_raw_nonzero().get() } /// `getegid()` returns the effective group ID of the calling process. pub fn getegid() -> gid_t { - nix::unistd::getegid().as_raw() + rprocess::getegid().as_raw() } /// `getgid()` returns the real group ID of the calling process. pub fn getgid() -> gid_t { - nix::unistd::getgid().as_raw() + rprocess::getgid().as_raw() } /// `getuid()` returns the real user ID of the calling process. pub fn getuid() -> uid_t { - nix::unistd::getuid().as_raw() + rprocess::getuid().as_raw() } /// `getpid()` returns the pid of the calling process. pub fn getpid() -> pid_t { - nix::unistd::getpid().as_raw() + rprocess::getpid().as_raw_nonzero().get() } /// `getsid()` returns the session ID of the process with process ID pid. @@ -57,22 +66,23 @@ pub fn getpid() -> pid_t { /// /// # Error /// -/// - [Errno::EPERM] A process with process ID pid exists, but it is not in the same session as the calling process, and the implementation considers this an error. -/// - [Errno::ESRCH] No process with process ID pid was found. -/// +/// - EPERM: A process with process ID pid exists, but it is not in the same session as the calling process, and the implementation considers this an error. +/// - ESRCH: No process with process ID pid was found. /// /// # Platform /// /// This function only support standard POSIX implementation platform, /// so some system such as redox doesn't supported. #[cfg(not(target_os = "redox"))] -pub fn getsid(pid: i32) -> Result { +pub fn getsid(pid: i32) -> io::Result { let pid = if pid == 0 { None } else { - Some(Pid::from_raw(pid)) + Some(Pid::from_raw(pid).ok_or_else(|| io::Error::from_raw_os_error(libc::EINVAL))?) }; - nix::unistd::getsid(pid).map(Pid::as_raw) + rprocess::getsid(pid) + .map(|p| p.as_raw_nonzero().get()) + .map_err(Into::into) } /// Missing methods for Child objects @@ -97,15 +107,15 @@ pub trait ChildExt { impl ChildExt for Child { fn send_signal(&mut self, signal: usize) -> io::Result<()> { - let pid = Pid::from_raw(self.id() as pid_t); - let result = if signal == 0 { - nix_signal::kill(pid, None) + let pid = Pid::from_raw(self.id() as pid_t) + .ok_or_else(|| io::Error::from_raw_os_error(libc::EINVAL))?; + if signal == 0 { + rprocess::test_kill_process(pid)?; } else { - let signal = Signal::try_from(signal as i32) - .map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))?; - nix_signal::kill(pid, Some(signal)) - }; - result.map_err(|e| io::Error::from_raw_os_error(e as i32)) + let sig = signal_from_raw(signal as i32)?; + rprocess::kill_process(pid, sig)?; + } + Ok(()) } fn send_signal_group(&mut self, signal: usize) -> io::Result<()> { @@ -118,20 +128,19 @@ impl ChildExt for Child { // Signal 0 is special - it just checks if process exists, doesn't send anything. // No need to manipulate signal handlers for it. if signal == 0 { - return nix_signal::kill(Pid::from_raw(0), None) - .map_err(|e| io::Error::from_raw_os_error(e as i32)); + return rprocess::test_kill_current_process_group().map_err(Into::into); } - let signal = Signal::try_from(signal as i32) - .map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))?; + let sig = signal_from_raw(signal as i32)?; // Ignore the signal temporarily so we don't receive it ourselves. - let old_handler = unsafe { nix_signal::signal(signal, SigHandler::SigIgn) } - .map_err(|e| io::Error::from_raw_os_error(e as i32))?; - let result = nix_signal::kill(Pid::from_raw(0), Some(signal)); + let old_handler = unsafe { + csignal::set_signal_handler(signal as libc::c_int, csignal::SigHandler::SigIgn) + }?; + let result = rprocess::kill_current_process_group(sig); // Restore the old handler - let _ = unsafe { nix_signal::signal(signal, old_handler) }; - result.map_err(|e| io::Error::from_raw_os_error(e as i32)) + let _ = unsafe { csignal::set_signal_handler(signal as libc::c_int, old_handler) }; + result.map_err(Into::into) } fn wait_or_timeout( From ef679217054fc6bf503ccbeedb6b5022a6a3e428 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 28 Mar 2026 11:27:42 +0100 Subject: [PATCH 07/21] refactor: migrate safe_traversal.rs from nix to rustix Replace all nix APIs with rustix equivalents in the security-critical TOCTOU-safe filesystem traversal module: - nix::dir::Dir -> rustix::fs::Dir (uses Dir::read_from which borrows fd) - nix::fcntl::{OFlag, openat} -> rustix::fs::{OFlags, openat} - nix::sys::stat::{fstat, fstatat, fchmod, fchmodat, mkdirat} -> rustix::fs - nix::unistd::{fchown, fchownat, unlinkat} -> rustix::fs - FchmodatFlags/UnlinkatFlags -> rustix::fs::AtFlags - Error handling simplified: rustix Errno implements Into Also update rm's unix platform code to use the new Stat type. All 31 safe_traversal unit tests pass. --- src/uu/rm/src/platform/unix.rs | 4 +- src/uucore/src/lib/features/safe_traversal.rs | 236 +++++++----------- 2 files changed, 88 insertions(+), 152 deletions(-) diff --git a/src/uu/rm/src/platform/unix.rs b/src/uu/rm/src/platform/unix.rs index d2bcf6e2f46..59e090b6361 100644 --- a/src/uu/rm/src/platform/unix.rs +++ b/src/uu/rm/src/platform/unix.rs @@ -16,7 +16,7 @@ use std::path::Path; use uucore::display::Quotable; use uucore::error::FromIo; use uucore::prompt_yes; -use uucore::safe_traversal::{DirFd, SymlinkBehavior}; +use uucore::safe_traversal::{DirFd, Stat, SymlinkBehavior}; use uucore::show_error; use uucore::translate; @@ -37,7 +37,7 @@ fn mode_writable(mode: libc::mode_t) -> bool { } /// File prompt that reuses existing stat data to avoid extra statx calls -fn prompt_file_with_stat(path: &Path, stat: &libc::stat, options: &Options) -> bool { +fn prompt_file_with_stat(path: &Path, stat: &Stat, options: &Options) -> bool { if options.interactive == InteractiveMode::Never { return true; } diff --git a/src/uucore/src/lib/features/safe_traversal.rs b/src/uucore/src/lib/features/safe_traversal.rs index f8aaa160968..b2ecce4a594 100644 --- a/src/uucore/src/lib/features/safe_traversal.rs +++ b/src/uucore/src/lib/features/safe_traversal.rs @@ -10,7 +10,7 @@ // // spell-checker:ignore CLOEXEC RDONLY TOCTOU closedir dirp fdopendir fstatat openat REMOVEDIR unlinkat smallfile // spell-checker:ignore RAII dirfd fchownat fchown FchmodatFlags fchmodat fchmod mkdirat CREAT WRONLY ELOOP ENOTDIR -// spell-checker:ignore atimensec mtimensec ctimensec +// spell-checker:ignore atimensec mtimensec ctimensec chmodat chownat #[cfg(test)] use std::os::unix::ffi::OsStringExt; @@ -22,11 +22,15 @@ use std::os::unix::ffi::OsStrExt; use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd, RawFd}; use std::path::{Path, PathBuf}; -use nix::dir::Dir; -use nix::fcntl::{OFlag, openat}; -use nix::sys::stat::{FchmodatFlags, FileStat, Mode, fchmodat, fstatat, mkdirat}; -use nix::unistd::{Gid, Uid, UnlinkatFlags, fchown, fchownat, unlinkat}; use os_display::Quotable; +// Re-export Stat so downstream crates can use it without depending on rustix directly +pub use rustix::fs::Stat; + +use rustix::fs::{ + AtFlags, Dir, Mode, OFlags, chmodat, fchmod, fchown, fstat, mkdirat, open, openat, statat, + unlinkat, +}; +use rustix::fs::{Gid, Uid, chownat}; use crate::translate; @@ -107,15 +111,17 @@ impl From for io::Error { } } -// Helper function to read directory entries using nix +// Helper function to read directory entries using rustix fn read_dir_entries(fd: &OwnedFd) -> io::Result> { let mut entries = Vec::new(); - // 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))?; + // Use dup + Dir::new instead of Dir::read_from, because read_from does + // openat(fd, ".") which requires execute permission on the directory. + // Dir::new wraps the fd directly (using getdents64 or fdopendir). + let dup_fd = rustix::io::dup(fd).map_err(io::Error::from)?; + let dir = Dir::new(dup_fd).map_err(io::Error::from)?; + for entry_result in dir { + let entry = entry_result.map_err(io::Error::from)?; let name = entry.file_name(); let name_os = OsStr::from_bytes(name.to_bytes()); if name_os != "." && name_os != ".." { @@ -138,15 +144,13 @@ impl DirFd { /// * `path` - The path to the directory to open /// * `symlink_behavior` - Whether to follow symlinks when opening pub fn open(path: &Path, symlink_behavior: SymlinkBehavior) -> io::Result { - let mut flags = OFlag::O_RDONLY | OFlag::O_DIRECTORY | OFlag::O_CLOEXEC; + let mut flags = OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC; if !symlink_behavior.should_follow() { - flags |= OFlag::O_NOFOLLOW; + flags |= OFlags::NOFOLLOW; } - let fd = nix::fcntl::open(path, flags, Mode::empty()).map_err(|e| { - SafeTraversalError::OpenFailed { - path: path.into(), - source: io::Error::from_raw_os_error(e as i32), - } + let fd = open(path, flags, Mode::empty()).map_err(|e| SafeTraversalError::OpenFailed { + path: path.into(), + source: io::Error::from(e), })?; Ok(Self { fd }) } @@ -157,37 +161,35 @@ impl DirFd { /// * `name` - The name of the subdirectory to open /// * `symlink_behavior` - Whether to follow symlinks when opening pub fn open_subdir(&self, name: &OsStr, symlink_behavior: SymlinkBehavior) -> io::Result { - let name_cstr = - CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?; - let mut flags = OFlag::O_RDONLY | OFlag::O_DIRECTORY | OFlag::O_CLOEXEC; + // Validate no null bytes (preserve PathContainsNull error) + CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?; + let mut flags = OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC; if !symlink_behavior.should_follow() { - flags |= OFlag::O_NOFOLLOW; + flags |= OFlags::NOFOLLOW; } - let fd = openat(&self.fd, name_cstr.as_c_str(), flags, Mode::empty()).map_err(|e| { + let fd = openat(&self.fd, name, flags, Mode::empty()).map_err(|e| { SafeTraversalError::OpenFailed { path: name.into(), - source: io::Error::from_raw_os_error(e as i32), + source: io::Error::from(e), } })?; Ok(Self { fd }) } /// Get raw stat data for a file relative to this directory - pub fn stat_at(&self, name: &OsStr, symlink_behavior: SymlinkBehavior) -> io::Result { - let name_cstr = - CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?; + pub fn stat_at(&self, name: &OsStr, symlink_behavior: SymlinkBehavior) -> io::Result { + // Validate no null bytes (preserve PathContainsNull error) + CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?; let flags = if symlink_behavior.should_follow() { - nix::fcntl::AtFlags::empty() + AtFlags::empty() } else { - nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW + AtFlags::SYMLINK_NOFOLLOW }; - let stat = fstatat(&self.fd, name_cstr.as_c_str(), flags).map_err(|e| { - SafeTraversalError::StatFailed { - path: name.into(), - source: io::Error::from_raw_os_error(e as i32), - } + let stat = statat(&self.fd, name, flags).map_err(|e| SafeTraversalError::StatFailed { + path: name.into(), + source: io::Error::from(e), })?; Ok(stat) @@ -209,10 +211,10 @@ impl DirFd { } /// Get raw stat data for this directory - pub fn fstat(&self) -> io::Result { - let stat = nix::sys::stat::fstat(&self.fd).map_err(|e| SafeTraversalError::StatFailed { + pub fn fstat(&self) -> io::Result { + let stat = fstat(&self.fd).map_err(|e| SafeTraversalError::StatFailed { path: translate!("safe-traversal-current-directory").into(), - source: io::Error::from_raw_os_error(e as i32), + source: io::Error::from(e), })?; Ok(stat) } @@ -230,19 +232,17 @@ impl DirFd { /// Remove a file or empty directory relative to this directory pub fn unlink_at(&self, name: &OsStr, is_dir: bool) -> io::Result<()> { - let name_cstr = - CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?; + // Validate no null bytes (preserve PathContainsNull error) + CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?; let flags = if is_dir { - UnlinkatFlags::RemoveDir + AtFlags::REMOVEDIR } else { - UnlinkatFlags::NoRemoveDir + AtFlags::empty() }; - unlinkat(&self.fd, name_cstr.as_c_str(), flags).map_err(|e| { - SafeTraversalError::UnlinkFailed { - path: name.into(), - source: io::Error::from_raw_os_error(e as i32), - } + unlinkat(&self.fd, name, flags).map_err(|e| SafeTraversalError::UnlinkFailed { + path: name.into(), + source: io::Error::from(e), })?; Ok(()) @@ -257,20 +257,19 @@ impl DirFd { gid: Option, symlink_behavior: SymlinkBehavior, ) -> io::Result<()> { - let name_cstr = - CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?; + // Validate no null bytes (preserve PathContainsNull error) + CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?; let flags = if symlink_behavior.should_follow() { - nix::fcntl::AtFlags::empty() + AtFlags::empty() } else { - nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW + AtFlags::SYMLINK_NOFOLLOW }; let uid = uid.map(Uid::from_raw); let gid = gid.map(Gid::from_raw); - fchownat(&self.fd, name_cstr.as_c_str(), uid, gid, flags) - .map_err(|e| io::Error::from_raw_os_error(e as i32))?; + chownat(&self.fd, name, uid, gid, flags).map_err(io::Error::from)?; Ok(()) } @@ -280,7 +279,7 @@ impl DirFd { let uid = uid.map(Uid::from_raw); let gid = gid.map(Gid::from_raw); - fchown(&self.fd, uid, gid).map_err(|e| io::Error::from_raw_os_error(e as i32))?; + fchown(&self.fd, uid, gid).map_err(io::Error::from)?; Ok(()) } @@ -293,40 +292,38 @@ impl DirFd { symlink_behavior: SymlinkBehavior, ) -> io::Result<()> { let flags = if symlink_behavior.should_follow() { - FchmodatFlags::FollowSymlink + AtFlags::empty() } else { - FchmodatFlags::NoFollowSymlink + AtFlags::SYMLINK_NOFOLLOW }; - let mode = Mode::from_bits_truncate(mode as libc::mode_t); + let mode = Mode::from_raw_mode(mode as libc::mode_t); - let name_cstr = - CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?; + // Validate no null bytes (preserve PathContainsNull error) + CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?; - fchmodat(&self.fd, name_cstr.as_c_str(), mode, flags) - .map_err(|e| io::Error::from_raw_os_error(e as i32))?; + chmodat(&self.fd, name, mode, flags).map_err(io::Error::from)?; Ok(()) } /// Change mode of this directory pub fn fchmod(&self, mode: u32) -> io::Result<()> { - let mode = Mode::from_bits_truncate(mode as libc::mode_t); + let mode = Mode::from_raw_mode(mode as libc::mode_t); - nix::sys::stat::fchmod(&self.fd, mode) - .map_err(|e| io::Error::from_raw_os_error(e as i32))?; + fchmod(&self.fd, mode).map_err(io::Error::from)?; Ok(()) } /// Create a directory relative to this directory pub fn mkdir_at(&self, name: &OsStr, mode: u32) -> io::Result<()> { - let name_cstr = - CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?; - let mode = Mode::from_bits_truncate(mode as libc::mode_t); + // Validate no null bytes (preserve PathContainsNull error) + CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?; + let mode = Mode::from_raw_mode(mode as libc::mode_t); - if let Err(e) = mkdirat(self.fd.as_fd(), name_cstr.as_c_str(), mode) { - let err = io::Error::from_raw_os_error(e as i32); + if let Err(e) = mkdirat(self.fd.as_fd(), name, mode) { + let err = io::Error::from(e); return Err(SafeTraversalError::OpenFailed { path: name.into(), source: err, @@ -339,13 +336,12 @@ impl DirFd { /// Open a file for writing relative to this directory /// Creates the file if it doesn't exist, truncates if it does pub fn open_file_at(&self, name: &OsStr) -> io::Result { - let name_cstr = - CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?; - let flags = OFlag::O_CREAT | OFlag::O_WRONLY | OFlag::O_TRUNC | OFlag::O_CLOEXEC; - let mode = Mode::from_bits_truncate(0o666); // Default file permissions + // Validate no null bytes (preserve PathContainsNull error) + CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?; + let flags = OFlags::CREATE | OFlags::WRONLY | OFlags::TRUNC | OFlags::CLOEXEC; + let mode = Mode::from_raw_mode(0o666); // Default file permissions - let fd: OwnedFd = openat(self.fd.as_fd(), name_cstr.as_c_str(), flags, mode) - .map_err(|e| io::Error::from_raw_os_error(e as i32))?; + let fd: OwnedFd = openat(self.fd.as_fd(), name, flags, mode).map_err(io::Error::from)?; // Convert OwnedFd to raw fd and create File let raw_fd = fd.into_raw_fd(); @@ -532,7 +528,7 @@ pub struct FileInfo { } impl FileInfo { - pub fn from_stat(stat: &libc::stat) -> Self { + pub fn from_stat(stat: &Stat) -> Self { // Allow unnecessary cast because st_dev and st_ino have different types on different platforms #[allow(clippy::unnecessary_cast)] Self { @@ -592,11 +588,11 @@ impl FileType { /// Metadata wrapper for safer access to file information #[derive(Debug, Clone)] pub struct Metadata { - stat: FileStat, + stat: Stat, } impl Metadata { - pub fn from_stat(stat: FileStat) -> Self { + pub fn from_stat(stat: Stat) -> Self { Self { stat } } @@ -692,94 +688,34 @@ impl std::os::unix::fs::MetadataExt for Metadata { self.stat.st_size as u64 } + #[allow(clippy::unnecessary_cast)] fn atime(&self) -> i64 { - #[cfg(target_pointer_width = "32")] - { - self.stat.st_atime.into() - } - #[cfg(not(target_pointer_width = "32"))] - { - self.stat.st_atime - } + self.stat.st_atime as i64 } + #[allow(clippy::unnecessary_cast)] fn atime_nsec(&self) -> i64 { - #[cfg(target_os = "netbsd")] - { - self.stat.st_atimensec - } - - #[cfg(not(target_os = "netbsd"))] - { - #[cfg(target_pointer_width = "32")] - { - self.stat.st_atime_nsec.into() - } - #[cfg(not(target_pointer_width = "32"))] - { - self.stat.st_atime_nsec - } - } + self.stat.st_atime_nsec as i64 } + #[allow(clippy::unnecessary_cast)] fn mtime(&self) -> i64 { - #[cfg(target_pointer_width = "32")] - { - self.stat.st_mtime.into() - } - #[cfg(not(target_pointer_width = "32"))] - { - self.stat.st_mtime - } + self.stat.st_mtime as i64 } + #[allow(clippy::unnecessary_cast)] fn mtime_nsec(&self) -> i64 { - #[cfg(target_os = "netbsd")] - { - self.stat.st_mtimensec - } - - #[cfg(not(target_os = "netbsd"))] - { - #[cfg(target_pointer_width = "32")] - { - self.stat.st_mtime_nsec.into() - } - #[cfg(not(target_pointer_width = "32"))] - { - self.stat.st_mtime_nsec - } - } + self.stat.st_mtime_nsec as i64 } + #[allow(clippy::unnecessary_cast)] fn ctime(&self) -> i64 { - #[cfg(target_pointer_width = "32")] - { - self.stat.st_ctime.into() - } - #[cfg(not(target_pointer_width = "32"))] - { - self.stat.st_ctime - } + self.stat.st_ctime as i64 } + #[allow(clippy::unnecessary_cast)] fn ctime_nsec(&self) -> i64 { - #[cfg(target_os = "netbsd")] - { - self.stat.st_ctimensec - } - - #[cfg(not(target_os = "netbsd"))] - { - #[cfg(target_pointer_width = "32")] - { - self.stat.st_ctime_nsec.into() - } - #[cfg(not(target_pointer_width = "32"))] - { - self.stat.st_ctime_nsec - } - } + self.stat.st_ctime_nsec as i64 } // st_blksize type varies by platform (i32/i64/u32/u64 depending on platform) @@ -949,7 +885,7 @@ mod tests { let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap(); // Duplicate the fd first so we don't have ownership conflicts - let dup_fd = nix::unistd::dup(&dir_fd).unwrap(); + let dup_fd = rustix::io::dup(&dir_fd).unwrap(); let from_raw_fd = DirFd::from_raw_fd(dup_fd.into_raw_fd()).unwrap(); // Both should refer to the same directory From f9dc90d433004b498c759b5968c57f2d8c6af723 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 28 Mar 2026 11:31:36 +0100 Subject: [PATCH 08/21] refactor: migrate mode.rs, fs.rs, and buf_copy/linux.rs from nix to rustix - mode.rs: nix::sys::stat::{Mode, umask} -> rustix::process::umask, rustix::fs::Mode - fs.rs: nix::sys::stat::{fstat, stat, lstat, FileStat} -> rustix::fs equivalents - buf_copy/linux.rs: nix::unistd::{read, write} -> rustix::io::{read, write}, nix::Error -> rustix::io::Errno --- src/uucore/src/lib/features/buf_copy/linux.rs | 22 +++++++++---------- src/uucore/src/lib/features/fs.rs | 11 +++++----- src/uucore/src/lib/features/mode.rs | 3 ++- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/uucore/src/lib/features/buf_copy/linux.rs b/src/uucore/src/lib/features/buf_copy/linux.rs index 7760d668025..3eefde6cd3b 100644 --- a/src/uucore/src/lib/features/buf_copy/linux.rs +++ b/src/uucore/src/lib/features/buf_copy/linux.rs @@ -31,10 +31,10 @@ impl FdWritable for T where T: Write + AsFd + AsRawFd {} const SPLICE_SIZE: usize = 1024 * 128; const BUF_SIZE: usize = 1024 * 16; -/// Conversion from a `nix::Error` into our `Error` which implements `UError`. -impl From for Error { - fn from(error: nix::Error) -> Self { - Self::Io(std::io::Error::from_raw_os_error(error as i32)) +/// Conversion from a `rustix::io::Errno` into our `Error` which implements `UError`. +impl From for Error { + fn from(error: rustix::io::Errno) -> Self { + Self::Io(std::io::Error::from(error)) } } @@ -126,19 +126,19 @@ pub(crate) fn copy_exact( write_fd: &impl AsFd, num_bytes: usize, ) -> std::io::Result { - use nix::unistd; - let mut left = num_bytes; let mut buf = [0; BUF_SIZE]; let mut written = 0; while left > 0 { - let read = unistd::read(read_fd, &mut buf)?; - assert_ne!(read, 0, "unexpected end of pipe"); - while written < read { - let n = unistd::write(write_fd, &buf[written..read])?; + let n_read = rustix::io::read(read_fd, &mut buf)?; + assert_ne!(n_read, 0, "unexpected end of pipe"); + let mut written = 0; + while written < n_read { + let n = rustix::io::write(write_fd, &buf[written..n_read])?; written += n; } - left -= read; + total_written += written; + left -= n_read; } Ok(written) } diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 6ef229d175c..bed8967df20 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -43,7 +43,7 @@ macro_rules! has { /// Information to uniquely identify a file pub struct FileInformation( - #[cfg(unix)] nix::sys::stat::FileStat, + #[cfg(unix)] rustix::fs::Stat, #[cfg(windows)] winapi_util::file::Information, ); @@ -51,7 +51,7 @@ impl FileInformation { /// Get information from a currently open file #[cfg(unix)] pub fn from_file(file: &impl AsFd) -> IOResult { - let stat = nix::sys::stat::fstat(file)?; + let stat = rustix::fs::fstat(file)?; Ok(Self(stat)) } @@ -70,9 +70,9 @@ impl FileInformation { #[cfg(unix)] { let stat = if dereference { - nix::sys::stat::stat(path.as_ref()) + rustix::fs::stat(path.as_ref()) } else { - nix::sys::stat::lstat(path.as_ref()) + rustix::fs::lstat(path.as_ref()) }; Ok(Self(stat?)) } @@ -762,8 +762,7 @@ pub fn is_stdin_directory(stdin: &Stdin) -> bool { #[cfg(unix)] { use mode::{S_IFDIR, S_IFMT}; - use nix::sys::stat::fstat; - let mode = fstat(stdin.as_fd()).unwrap().st_mode as u32; + let mode = rustix::fs::fstat(stdin.as_fd()).unwrap().st_mode as u32; // We use the S_IFMT mask ala S_ISDIR() to avoid mistaking // sockets for directories. mode & S_IFMT == S_IFDIR diff --git a/src/uucore/src/lib/features/mode.rs b/src/uucore/src/lib/features/mode.rs index 4e737595ee2..b259fc6c276 100644 --- a/src/uucore/src/lib/features/mode.rs +++ b/src/uucore/src/lib/features/mode.rs @@ -178,7 +178,8 @@ pub fn get_umask() -> u32 { // from /proc/self/status. But that's a lot of work. #[cfg(unix)] { - use nix::sys::stat::{Mode, umask}; + use rustix::fs::Mode; + use rustix::process::umask; let mask = umask(Mode::empty()); let _ = umask(mask); From db9e2a0ec781e32eb24384b70fab9dc151d45edf Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 28 Mar 2026 11:39:28 +0100 Subject: [PATCH 09/21] refactor: migrate signals.rs and lib.rs from nix to csignal/rustix Replace all nix signal and poll APIs in uucore: - signals.rs: nix::sys::signal -> csignal wrappers - signals.rs: nix::poll -> rustix::event::poll - signals.rs: nix::sys::stat -> rustix::fs::fstat + libc constants - lib.rs: nix sigaction -> csignal::set_signal_action - dd/progress.rs: update install_sigusr1_handler to use libc::SIGUSR1 Return types change from nix::Result/Errno to std::io::Result. --- src/uu/dd/src/progress.rs | 4 +- src/uucore/src/lib/features/signals.rs | 81 +++++++++++--------------- src/uucore/src/lib/lib.rs | 24 ++++---- 3 files changed, 49 insertions(+), 60 deletions(-) diff --git a/src/uu/dd/src/progress.rs b/src/uu/dd/src/progress.rs index cc5527f5573..079540b585e 100644 --- a/src/uu/dd/src/progress.rs +++ b/src/uu/dd/src/progress.rs @@ -459,8 +459,8 @@ extern "C" fn sigusr1_handler(_: std::os::raw::c_int) { } #[cfg(target_os = "linux")] -pub(crate) fn install_sigusr1_handler() -> Result<(), nix::errno::Errno> { - uucore::signals::install_signal_handler(nix::sys::signal::Signal::SIGUSR1, sigusr1_handler) +pub(crate) fn install_sigusr1_handler() -> std::io::Result<()> { + uucore::signals::install_signal_handler(libc::SIGUSR1, sigusr1_handler) } /// Return a closure that can be used in its own thread to print progress info. diff --git a/src/uucore/src/lib/features/signals.rs b/src/uucore/src/lib/features/signals.rs index 52aa04a809a..7f6ae188af4 100644 --- a/src/uucore/src/lib/features/signals.rs +++ b/src/uucore/src/lib/features/signals.rs @@ -10,14 +10,6 @@ //! It provides a way to convert signal names to their corresponding values and vice versa. //! It also provides a way to ignore the SIGINT signal and enable pipe errors. -#[cfg(unix)] -use nix::errno::Errno; -#[cfg(unix)] -use nix::sys::signal::{ - SaFlags, SigAction, SigHandler, SigHandler::SigDfl, SigHandler::SigIgn, SigSet, Signal, - Signal::SIGINT, Signal::SIGPIPE, sigaction, signal, -}; - // --------------------------------------------------------------------------- // Thin libc-based signal wrappers // @@ -650,42 +642,41 @@ pub fn signal_list_value_by_name_or_number(spec: &str) -> Option { /// Restores SIGPIPE to default behavior (process terminates on broken pipe). #[cfg(unix)] -pub fn enable_pipe_errors() -> Result<(), Errno> { - // We pass the error as is, the return value would just be Ok(SigDfl), so we can safely ignore it. - // SAFETY: this function is safe as long as we do not use a custom SigHandler -- we use the default one. - unsafe { signal(SIGPIPE, SigDfl) }.map(|_| ()) +pub fn enable_pipe_errors() -> std::io::Result<()> { + // SAFETY: SigDfl is always safe. + unsafe { csignal::set_signal_handler(libc::SIGPIPE, csignal::SigHandler::SigDfl) }.map(|_| ()) } /// Ignores SIGPIPE signal (broken pipe errors are returned instead of terminating). /// Use this to override the default SIGPIPE handling when you need to handle /// broken pipe errors gracefully (e.g., tee with --output-error). #[cfg(unix)] -pub fn disable_pipe_errors() -> Result<(), Errno> { - // SAFETY: this function is safe as long as we do not use a custom SigHandler -- we use the default one. - unsafe { signal(SIGPIPE, SigIgn) }.map(|_| ()) +pub fn disable_pipe_errors() -> std::io::Result<()> { + // SAFETY: SigIgn is always safe. + unsafe { csignal::set_signal_handler(libc::SIGPIPE, csignal::SigHandler::SigIgn) }.map(|_| ()) } /// Ignores the SIGINT signal. #[cfg(unix)] -pub fn ignore_interrupts() -> Result<(), Errno> { - // We pass the error as is, the return value would just be Ok(SigIgn), so we can safely ignore it. - // SAFETY: this function is safe as long as we do not use a custom SigHandler -- we use the default one. - unsafe { signal(SIGINT, SigIgn) }.map(|_| ()) +pub fn ignore_interrupts() -> std::io::Result<()> { + // SAFETY: SigIgn is always safe. + unsafe { csignal::set_signal_handler(libc::SIGINT, csignal::SigHandler::SigIgn) }.map(|_| ()) } /// Installs a signal handler. The handler must be async-signal-safe. #[cfg(unix)] pub fn install_signal_handler( - sig: Signal, - handler: extern "C" fn(std::os::raw::c_int), -) -> Result<(), Errno> { - let action = SigAction::new( - SigHandler::Handler(handler), - SaFlags::SA_RESTART, - SigSet::empty(), - ); - unsafe { sigaction(sig, &action) }?; - Ok(()) + sig: libc::c_int, + handler: extern "C" fn(libc::c_int), +) -> std::io::Result<()> { + unsafe { + csignal::set_signal_action( + sig, + csignal::SigHandler::Handler(handler), + csignal::sa_flags::SA_RESTART, + &csignal::SigSet::empty(), + ) + } } // Detect closed stdin/stdout before Rust reopens them as /dev/null (see issue #2873) @@ -807,10 +798,9 @@ pub const fn sigpipe_was_ignored() -> bool { #[cfg(target_os = "linux")] pub fn ensure_stdout_not_broken() -> std::io::Result { - use nix::{ - poll::{PollFd, PollFlags, PollTimeout, poll}, - sys::stat::{SFlag, fstat}, - }; + use rustix::event::{PollFd, PollFlags, poll}; + use rustix::fs::fstat; + use rustix::time::Timespec; use std::io::stdout; use std::os::fd::AsFd; @@ -818,32 +808,31 @@ pub fn ensure_stdout_not_broken() -> std::io::Result { // First, check that stdout is a fifo and return true if it's not the case let stat = fstat(out.as_fd())?; - if !SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFIFO) { + if (stat.st_mode & libc::S_IFMT) != libc::S_IFIFO { return Ok(true); } // POLLRDBAND is the flag used by GNU tee. - let mut pfds = [PollFd::new(out.as_fd(), PollFlags::POLLRDBAND)]; + let mut pfds = [PollFd::new(&out, PollFlags::RDBAND)]; // Then, ensure that the pipe is not broken. - // Use ZERO timeout to return immediately - we just want to check the current state. - let res = poll(&mut pfds, PollTimeout::ZERO)?; + // Use 0 timeout to return immediately - we just want to check the current state. + let zero_timeout = Timespec { + tv_sec: 0, + tv_nsec: 0, + }; + let res = poll(&mut pfds, Some(&zero_timeout))?; if res > 0 { // poll returned with events ready - check if POLLERR is set (pipe broken) - let error = pfds.iter().any(|pfd| { - if let Some(revents) = pfd.revents() { - revents.contains(PollFlags::POLLERR) - } else { - true - } - }); + let error = pfds + .iter() + .any(|pfd| pfd.revents().contains(PollFlags::ERR)); return Ok(!error); } - // res == 0 means no events ready (timeout reached immediately with ZERO timeout). + // res == 0 means no events ready (timeout reached immediately with 0 timeout). // This means the pipe is healthy (not broken). - // res < 0 would be an error, but nix returns Err in that case. Ok(true) } diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 4b2cb9b502b..267026024dc 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -131,11 +131,7 @@ pub use crate::features::smack; //## core functions #[cfg(unix)] -use nix::errno::Errno; -#[cfg(unix)] -use nix::sys::signal::{ - SaFlags, SigAction, SigHandler::SigDfl, SigSet, Signal::SIGBUS, Signal::SIGSEGV, sigaction, -}; +use crate::signals::csignal; use std::borrow::Cow; use std::ffi::{OsStr, OsString}; use std::io::{BufRead, BufReader}; @@ -149,17 +145,21 @@ use std::sync::{LazyLock, atomic::Ordering}; /// Disables the custom signal handlers installed by Rust for stack-overflow handling. With those custom signal handlers processes ignore the first SIGBUS and SIGSEGV signal they receive. /// See for details. #[cfg(unix)] -pub fn disable_rust_signal_handlers() -> Result<(), Errno> { +pub fn disable_rust_signal_handlers() -> std::io::Result<()> { unsafe { - sigaction( - SIGSEGV, - &SigAction::new(SigDfl, SaFlags::empty(), SigSet::all()), + csignal::set_signal_action( + libc::SIGSEGV, + csignal::SigHandler::SigDfl, + 0, + &csignal::SigSet::all(), ) }?; unsafe { - sigaction( - SIGBUS, - &SigAction::new(SigDfl, SaFlags::empty(), SigSet::all()), + csignal::set_signal_action( + libc::SIGBUS, + csignal::SigHandler::SigDfl, + 0, + &csignal::SigSet::all(), ) }?; Ok(()) From a588733fa5cd5b4e7653a85ea8841744419d95cd Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 28 Mar 2026 13:03:13 +0100 Subject: [PATCH 10/21] refactor: migrate all utilities from nix to rustix/libc Migrate 19 utility crates from nix to rustix and direct libc calls: - kill, timeout, env: signal handling via csignal wrappers + libc - sync, cat, tail, dd, tsort: file operations via rustix::fs - touch: futimens via rustix::fs::futimens - stty, tty: terminal ops via rustix::termios + libc ioctl - sort: getrlimit via rustix::process, sysconf via libc - wc, cp, df: stat/fstat via rustix::fs - nice: priority via rustix::process - mkfifo, mknod: via direct libc calls Also add rustix::io::Errno conversions to uucore error.rs. --- Cargo.lock | 30 ++-- src/uu/cat/Cargo.toml | 3 +- src/uu/cat/src/cat.rs | 4 +- src/uu/cat/src/platform/unix.rs | 14 +- src/uu/cp/Cargo.toml | 1 - src/uu/cp/src/cp.rs | 31 ++++- src/uu/dd/Cargo.toml | 2 +- src/uu/dd/src/dd.rs | 61 +++++---- src/uu/df/Cargo.toml | 2 +- src/uu/df/src/df.rs | 2 +- src/uu/env/Cargo.toml | 1 - src/uu/env/src/env.rs | 80 ++++++----- src/uu/kill/Cargo.toml | 2 +- src/uu/kill/src/kill.rs | 24 ++-- src/uu/mkfifo/Cargo.toml | 2 +- src/uu/mkfifo/src/mkfifo.rs | 7 +- src/uu/mknod/Cargo.toml | 1 - src/uu/mknod/src/mknod.rs | 37 +++-- src/uu/sort/Cargo.toml | 2 +- src/uu/sort/src/buffer_hint.rs | 17 +-- src/uu/sort/src/sort.rs | 11 +- src/uu/stty/Cargo.toml | 2 +- src/uu/stty/src/flags.rs | 227 ++++++++++++------------------- src/uu/stty/src/stty.rs | 166 +++++++++++----------- src/uu/sync/Cargo.toml | 3 +- src/uu/sync/src/sync.rs | 53 ++++---- src/uu/tail/Cargo.toml | 2 +- src/uu/tail/src/tail.rs | 8 +- src/uu/timeout/Cargo.toml | 2 - src/uu/timeout/src/timeout.rs | 55 ++++---- src/uu/touch/Cargo.toml | 2 +- src/uu/touch/src/touch.rs | 38 ++---- src/uu/tsort/Cargo.toml | 2 +- src/uu/tsort/src/tsort.rs | 10 +- src/uu/tty/Cargo.toml | 2 +- src/uu/tty/src/tty.rs | 6 +- src/uu/wc/Cargo.toml | 2 +- src/uu/wc/src/count_fast.rs | 8 +- src/uu/wc/src/wc.rs | 4 +- src/uucore/src/lib/mods/error.rs | 41 ++++++ 40 files changed, 485 insertions(+), 482 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6b053d99e63..71a30502bb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3288,7 +3288,6 @@ dependencies = [ "codspeed-divan-compat", "fluent", "memchr", - "nix", "rustix", "tempfile", "thiserror 2.0.18", @@ -3388,7 +3387,6 @@ dependencies = [ "fluent", "indicatif", "libc", - "nix", "selinux", "tempfile", "thiserror 2.0.18", @@ -3450,7 +3448,7 @@ dependencies = [ "fluent", "gcd", "libc", - "nix", + "rustix", "tempfile", "thiserror 2.0.18", "uucore", @@ -3463,7 +3461,7 @@ dependencies = [ "clap", "codspeed-divan-compat", "fluent", - "nix", + "rustix", "tempfile", "thiserror 2.0.18", "unicode-width 0.2.2", @@ -3528,7 +3526,6 @@ dependencies = [ "clap", "fluent", "libc", - "nix", "rust-ini", "thiserror 2.0.18", "uucore", @@ -3693,7 +3690,7 @@ version = "0.8.0" dependencies = [ "clap", "fluent", - "nix", + "libc", "uucore", ] @@ -3771,7 +3768,7 @@ version = "0.8.0" dependencies = [ "clap", "fluent", - "nix", + "libc", "uucore", ] @@ -3782,7 +3779,6 @@ dependencies = [ "clap", "fluent", "libc", - "nix", "uucore", ] @@ -4142,9 +4138,9 @@ dependencies = [ "itertools 0.14.0", "libc", "memchr", - "nix", "rand 0.10.0", "rayon", + "rustix", "self_cell", "tempfile", "thiserror 2.0.18", @@ -4202,7 +4198,7 @@ dependencies = [ "clap", "fluent", "libc", - "nix", + "rustix", "uucore", ] @@ -4221,7 +4217,8 @@ version = "0.8.0" dependencies = [ "clap", "fluent", - "nix", + "libc", + "rustix", "uucore", "windows-sys 0.61.2", ] @@ -4249,9 +4246,9 @@ dependencies = [ "fluent", "libc", "memchr", - "nix", "notify", "rstest", + "rustix", "same-file", "uucore", "windows-sys 0.61.2", @@ -4285,7 +4282,6 @@ dependencies = [ "clap", "fluent", "libc", - "nix", "uucore", ] @@ -4298,8 +4294,8 @@ dependencies = [ "fluent", "jiff", "libc", - "nix", "parse_datetime", + "rustix", "tempfile", "thiserror 2.0.18", "uucore", @@ -4343,8 +4339,8 @@ dependencies = [ "clap", "codspeed-divan-compat", "fluent", - "nix", "rustc-hash", + "rustix", "string-interner", "thiserror 2.0.18", "uucore", @@ -4356,7 +4352,7 @@ version = "0.8.0" dependencies = [ "clap", "fluent", - "nix", + "rustix", "uucore", ] @@ -4440,7 +4436,7 @@ dependencies = [ "codspeed-divan-compat", "fluent", "libc", - "nix", + "rustix", "tempfile", "thiserror 2.0.18", "unicode-width 0.2.2", diff --git a/src/uu/cat/Cargo.toml b/src/uu/cat/Cargo.toml index 9f4626f75cb..e5c8ca35b86 100644 --- a/src/uu/cat/Cargo.toml +++ b/src/uu/cat/Cargo.toml @@ -26,8 +26,7 @@ uucore = { workspace = true, features = ["fast-inc", "fs", "pipes", "signals"] } fluent = { workspace = true } [target.'cfg(unix)'.dependencies] -nix = { workspace = true } -rustix = { workspace = true } +rustix = { workspace = true, features = ["fs"] } [target.'cfg(windows)'.dependencies] winapi-util = { workspace = true } diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index aef7d5eab50..46e4e1be911 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -84,10 +84,10 @@ enum CatError { /// Wrapper around `io::Error` #[error("{}", strip_errno(.0))] Io(#[from] io::Error), - /// Wrapper around `nix::Error` + /// Wrapper around `rustix::io::Errno` #[cfg(any(target_os = "linux", target_os = "android"))] #[error("{0}")] - Nix(#[from] nix::Error), + Rustix(#[from] rustix::io::Errno), /// Unknown file type; it's not a regular file, socket, etc. #[error("{}", translate!("cat-error-unknown-filetype", "ft_debug" => .ft_debug))] UnknownFiletype { diff --git a/src/uu/cat/src/platform/unix.rs b/src/uu/cat/src/platform/unix.rs index b2cdda2aa67..6cc55fc6967 100644 --- a/src/uu/cat/src/platform/unix.rs +++ b/src/uu/cat/src/platform/unix.rs @@ -5,8 +5,7 @@ // spell-checker:ignore lseek seekable -use nix::fcntl::{FcntlArg, OFlag, fcntl}; -use nix::unistd::{Whence, lseek}; +use rustix::fs::{OFlags, SeekFrom, fcntl_getfl}; use std::os::fd::AsFd; use uucore::fs::FileInformation; @@ -31,10 +30,10 @@ pub fn is_unsafe_overwrite(input: &I, output: &O) -> bool { if file_size == 0 { return false; } - // `lseek` returns an error if the file descriptor is closed or it refers to + // `seek` returns an error if the file descriptor is closed or it refers to // a non-seekable resource (e.g., pipe, socket, or some devices). - let input_pos = lseek(input.as_fd(), 0, Whence::SeekCur); - let output_pos = lseek(output.as_fd(), 0, Whence::SeekCur); + let input_pos = rustix::fs::seek(input, SeekFrom::Current(0)).map(|v| v as i64); + let output_pos = rustix::fs::seek(output, SeekFrom::Current(0)).map(|v| v as i64); if is_appending(output) { if let Ok(pos) = input_pos { if pos >= 0 && (pos as u64) >= file_size { @@ -54,9 +53,8 @@ pub fn is_unsafe_overwrite(input: &I, output: &O) -> bool { /// Whether the file is opened with the `O_APPEND` flag fn is_appending(file: &F) -> bool { - let flags_raw = fcntl(file.as_fd(), FcntlArg::F_GETFL).unwrap_or_default(); - let flags = OFlag::from_bits_truncate(flags_raw); - flags.contains(OFlag::O_APPEND) + let flags = fcntl_getfl(file).unwrap_or(OFlags::empty()); + flags.contains(OFlags::APPEND) } #[cfg(test)] diff --git a/src/uu/cp/Cargo.toml b/src/uu/cp/Cargo.toml index bea4afc67c2..79e00d6bb0b 100644 --- a/src/uu/cp/Cargo.toml +++ b/src/uu/cp/Cargo.toml @@ -41,7 +41,6 @@ fluent = { workspace = true } [target.'cfg(unix)'.dependencies] exacl = { workspace = true, optional = true } -nix = { workspace = true, features = ["fs"] } [[bin]] name = "cp" diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index e781cc14ba4..f6e336539fb 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -23,7 +23,7 @@ use clap::{Arg, ArgAction, ArgMatches, Command, builder::ValueParser, value_pars use filetime::FileTime; use indicatif::{ProgressBar, ProgressStyle}; #[cfg(unix)] -use nix::sys::stat::{Mode, SFlag, dev_t, mknod as nix_mknod, mode_t}; +use libc::{dev_t, mode_t}; use thiserror::Error; use platform::copy_on_write; @@ -2799,14 +2799,31 @@ fn copy_node( overwrite.verify(dest, debug)?; fs::remove_file(dest)?; } - let sflag = if source_metadata.file_type().is_char_device() { - SFlag::S_IFCHR + let sflag: mode_t = if source_metadata.file_type().is_char_device() { + libc::S_IFCHR } else { - SFlag::S_IFBLK + libc::S_IFBLK }; - let mode = Mode::from_bits_truncate(source_metadata.mode() as mode_t); - nix_mknod(dest, sflag, mode, source_metadata.rdev() as dev_t) - .map_err(|e| translate!("cp-error-cannot-create-special-file", "path" => dest.quote(), "error" => e.desc()).into()) + let mode = (source_metadata.mode() as mode_t) & 0o7777; + use std::io; + let c_path = std::ffi::CString::new(dest.as_os_str().as_encoded_bytes()) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path contains null byte"))?; + // SAFETY: c_path is a valid null-terminated C string + let ret = unsafe { + libc::mknod( + c_path.as_ptr(), + sflag | mode, + source_metadata.rdev() as dev_t, + ) + }; + if ret != 0 { + let e = io::Error::last_os_error(); + return Err( + translate!("cp-error-cannot-create-special-file", "path" => dest.quote(), "error" => uucore::error::strip_errno(&e)) + .into(), + ); + } + Ok(()) } fn copy_link( diff --git a/src/uu/dd/Cargo.toml b/src/uu/dd/Cargo.toml index e92ac0f0f2c..76443f902a8 100644 --- a/src/uu/dd/Cargo.toml +++ b/src/uu/dd/Cargo.toml @@ -33,7 +33,7 @@ thiserror = { workspace = true } fluent = { workspace = true } [target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] -nix = { workspace = true, features = ["fs", "signal"] } +rustix = { workspace = true, features = ["fs"] } [[bin]] name = "dd" diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index e1d7b0ad6b2..07f7dcb10b2 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -16,15 +16,13 @@ mod progress; use crate::bufferedoutput::BufferedOutput; use blocks::conv_block_unblock_helper; use datastructures::{ConversionMode, IConvFlags, IFlags, OConvFlags, OFlags, options}; -#[cfg(any(target_os = "linux", target_os = "android"))] -use nix::fcntl::FcntlArg; -#[cfg(any(target_os = "linux", target_os = "android"))] -use nix::fcntl::OFlag; use parseargs::Parser; use progress::ProgUpdateType; use progress::{ProgUpdate, ReadStat, StatusLevel, WriteStat, gen_prog_updater}; #[cfg(target_os = "linux")] use progress::{check_and_reset_sigusr1, install_sigusr1_handler}; +#[cfg(any(target_os = "linux", target_os = "android"))] +use rustix::fs::OFlags as RustixOFlags; use uucore::io::OwnedFileDescriptorOrHandle; use uucore::translate; @@ -55,10 +53,7 @@ use std::time::{Duration, Instant}; use clap::{Arg, Command}; use gcd::Gcd; #[cfg(target_os = "linux")] -use nix::{ - errno::Errno, - fcntl::{PosixFadviseAdvice, posix_fadvise}, -}; +use rustix::fs::{Advice as PosixFadviseAdvice, fadvise}; use uucore::display::Quotable; use uucore::error::{FromIo, UResult}; #[cfg(unix)] @@ -315,14 +310,16 @@ impl Source { /// portion of the source is no longer needed. If not possible, /// then this function returns an error. #[cfg(target_os = "linux")] - fn discard_cache(&self, offset: libc::off_t, len: libc::off_t) -> nix::Result<()> { + fn discard_cache(&self, offset: libc::off_t, len: libc::off_t) -> io::Result<()> { #[allow(clippy::match_wildcard_for_single_variants)] match self { Self::File(f) => { - let advice = PosixFadviseAdvice::POSIX_FADV_DONTNEED; - posix_fadvise(f.as_fd(), offset, len, advice) + let advice = PosixFadviseAdvice::DontNeed; + let len = std::num::NonZeroU64::new(len as u64); + fadvise(f.as_fd(), offset as u64, len, advice)?; + Ok(()) } - _ => Err(Errno::ESPIPE), // "Illegal seek" + _ => Err(io::Error::from_raw_os_error(libc::ESPIPE)), // "Illegal seek" } } } @@ -697,13 +694,15 @@ impl Dest { /// specified portion of the destination is no longer needed. If /// not possible, then this function returns an error. #[cfg(target_os = "linux")] - fn discard_cache(&self, offset: libc::off_t, len: libc::off_t) -> nix::Result<()> { + fn discard_cache(&self, offset: libc::off_t, len: libc::off_t) -> io::Result<()> { match self { Self::File(f, _) => { - let advice = PosixFadviseAdvice::POSIX_FADV_DONTNEED; - posix_fadvise(f.as_fd(), offset, len, advice) + let advice = PosixFadviseAdvice::DontNeed; + let len = std::num::NonZeroU64::new(len as u64); + fadvise(f.as_fd(), offset as u64, len, advice)?; + Ok(()) } - _ => Err(Errno::ESPIPE), // "Illegal seek" + _ => Err(io::Error::from_raw_os_error(libc::ESPIPE)), // "Illegal seek" } } } @@ -717,31 +716,33 @@ fn is_sparse(buf: &[u8]) -> bool { /// This follows GNU dd behavior for partial block writes with O_DIRECT. #[cfg(any(target_os = "linux", target_os = "android"))] fn handle_o_direct_write(f: &mut File, buf: &[u8], original_error: io::Error) -> io::Result { - use nix::fcntl::{FcntlArg, OFlag, fcntl}; + use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl}; - // Get current flags using nix - let oflags = match fcntl(&mut *f, FcntlArg::F_GETFL) { - Ok(flags) => OFlag::from_bits_retain(flags), - Err(_) => return Err(original_error), + // Get current flags + let Ok(oflags) = fcntl_getfl(&*f) else { + return Err(original_error); }; // If O_DIRECT is set, try removing it temporarily - if oflags.contains(OFlag::O_DIRECT) { - let flags_without_direct = oflags - OFlag::O_DIRECT; + if oflags.contains(OFlags::DIRECT) { + let flags_without_direct = oflags & !OFlags::DIRECT; - // Remove O_DIRECT flag using nix - if fcntl(&mut *f, FcntlArg::F_SETFL(flags_without_direct)).is_err() { + // Remove O_DIRECT flag + if fcntl_setfl(&*f, flags_without_direct).is_err() { return Err(original_error); } // Retry the write without O_DIRECT let write_result = f.write(buf); - // Restore O_DIRECT flag using nix (GNU doesn't restore it, but we'll be safer) + // Restore O_DIRECT flag (GNU doesn't restore it, but we'll be safer) // Log any restoration errors without failing the operation - if let Err(os_err) = fcntl(&mut *f, FcntlArg::F_SETFL(oflags)) { + if let Err(os_err) = fcntl_setfl(&*f, oflags) { // Just log the error, don't fail the whole operation - show_error!("Failed to restore O_DIRECT flag: {os_err}"); + show_error!( + "Failed to restore O_DIRECT flag: {}", + io::Error::from(os_err) + ); } write_result @@ -888,9 +889,9 @@ impl<'a> Output<'a> { let fx = OwnedFileDescriptorOrHandle::from(io::stdout())?; #[cfg(any(target_os = "linux", target_os = "android"))] if let Some(libc_flags) = make_linux_oflags(&settings.oflags) { - nix::fcntl::fcntl( + rustix::fs::fcntl_setfl( fx.as_raw().as_fd(), - FcntlArg::F_SETFL(OFlag::from_bits_retain(libc_flags)), + RustixOFlags::from_bits_retain(libc_flags as _), )?; } diff --git a/src/uu/df/Cargo.toml b/src/uu/df/Cargo.toml index 9b99baa9287..dfb5aea8def 100644 --- a/src/uu/df/Cargo.toml +++ b/src/uu/df/Cargo.toml @@ -26,7 +26,7 @@ thiserror = { workspace = true } fluent = { workspace = true } [target.'cfg(unix)'.dependencies] -nix = { workspace = true, features = ["fs"] } +rustix = { workspace = true, features = ["fs"] } [dev-dependencies] divan = { workspace = true } diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index 51b950dd460..2f97814d02b 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -301,7 +301,7 @@ fn get_all_filesystems(opt: &Options) -> UResult> { // Run a sync call before any operation if so instructed. if opt.sync { #[cfg(not(any(windows, target_os = "redox")))] - nix::unistd::sync(); + rustix::fs::sync(); } let mut mounts = vec![]; diff --git a/src/uu/env/Cargo.toml b/src/uu/env/Cargo.toml index fda68b1ae50..62081d51a15 100644 --- a/src/uu/env/Cargo.toml +++ b/src/uu/env/Cargo.toml @@ -27,7 +27,6 @@ fluent = { workspace = true } [target.'cfg(unix)'.dependencies] libc = { workspace = true } -nix = { workspace = true, features = ["signal"] } [[bin]] name = "env" diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index 2f116b82932..d68c680cf43 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -17,13 +17,6 @@ use ini::Ini; use native_int_str::{ Convert, NCvt, NativeIntStr, NativeIntString, NativeStr, from_native_int_representation_owned, }; -#[cfg(unix)] -use nix::sys::signal::{ - SigHandler::{SigDfl, SigIgn}, - SigSet, SigmaskHow, Signal, signal, sigprocmask, -}; -#[cfg(unix)] -use nix::unistd::execvp; use std::borrow::Cow; #[cfg(unix)] use std::collections::{BTreeMap, BTreeSet}; @@ -36,6 +29,8 @@ use std::io::Write as _; use std::io::stderr; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; +#[cfg(unix)] +use uucore::signals::csignal::{self, SigHandler}; use uucore::display::{Quotable, print_all_env_vars}; use uucore::error::{ExitCode, UError, UResult, USimpleError, UUsageError}; @@ -293,16 +288,25 @@ fn build_signal_request( } #[cfg(unix)] -fn signal_from_value(sig_value: usize) -> UResult { - Signal::try_from(sig_value as i32).map_err(|_| { - USimpleError::new( +fn signal_from_value(sig_value: usize) -> UResult { + let sig = sig_value as libc::c_int; + // Validate signal number is in a reasonable range. + // On Linux, reject glibc-reserved signals between standard signals and SIGRTMIN + // (typically 32 and 33, used internally by NPTL). + #[cfg(any(target_os = "linux", target_os = "android"))] + let reserved = sig > libc::SIGSYS && sig < libc::SIGRTMIN(); + #[cfg(not(any(target_os = "linux", target_os = "android")))] + let reserved = false; + if sig <= 0 || sig >= 128 || reserved { + return Err(USimpleError::new( 125, translate!( "env-error-invalid-signal", "signal" => sig_value.to_string().quote() ), - ) - }) + )); + } + Ok(sig) } fn load_config_file(opts: &mut Options) -> UResult<()> { @@ -838,11 +842,21 @@ impl EnvAppData { } // Execute the program using execvp. this replaces the current - // process. The execvp function takes care of appending a NULL - // argument to the argument list so that we don't have to. - match execvp(&prog_cstring, &argv) { - Err(nix::errno::Errno::ENOENT) => Err(self.make_error_no_such_file_or_dir(&prog)), - Err(nix::errno::Errno::EACCES) => { + // process. + // + // Build a null-terminated array of pointers for execvp. + let mut argv_ptrs: Vec<*const libc::c_char> = argv.iter().map(|a| a.as_ptr()).collect(); + argv_ptrs.push(std::ptr::null()); + + // SAFETY: prog_cstring is a valid null-terminated C string, + // argv_ptrs is a null-terminated array of valid C string pointers. + // execvp only returns on error. + unsafe { libc::execvp(prog_cstring.as_ptr(), argv_ptrs.as_ptr()) }; + + let err = io::Error::last_os_error(); + match err.raw_os_error() { + Some(libc::ENOENT) => Err(self.make_error_no_such_file_or_dir(&prog)), + Some(libc::EACCES) => { uucore::show_error!( "{}", translate!( @@ -852,7 +866,7 @@ impl EnvAppData { ); Err(126.into()) } - Err(_) => { + _ => { uucore::show_error!( "{}", translate!( @@ -862,9 +876,6 @@ impl EnvAppData { ); Err(126.into()) } - Ok(_) => { - unreachable!("execvp should never return on success") - } } } @@ -1074,7 +1085,7 @@ fn apply_signal_action( signal_fn: F, ) -> UResult<()> where - F: Fn(Signal) -> UResult<()>, + F: Fn(libc::c_int) -> UResult<()>, { request.for_each_signal(|sig_value, explicit| { // On some platforms ALL_SIGNALS may contain values that are not valid in libc. @@ -1098,41 +1109,38 @@ where } #[cfg(unix)] -fn ignore_signal(sig: Signal) -> UResult<()> { - // SAFETY: This is safe because we write the handler for each signal only once, and therefore "the current handler is the default", as the documentation requires it. - let result = unsafe { signal(sig, SigIgn) }; - if let Err(err) = result { +fn ignore_signal(sig: libc::c_int) -> UResult<()> { + if let Err(err) = unsafe { csignal::set_signal_handler(sig, SigHandler::SigIgn) } { return Err(USimpleError::new( 125, - translate!("env-error-failed-set-signal-action", "signal" => (sig as i32), "error" => err.desc()), + translate!("env-error-failed-set-signal-action", "signal" => sig, "error" => err), )); } Ok(()) } #[cfg(unix)] -fn reset_signal(sig: Signal) -> UResult<()> { - let result = unsafe { signal(sig, SigDfl) }; - if let Err(err) = result { +fn reset_signal(sig: libc::c_int) -> UResult<()> { + if let Err(err) = unsafe { csignal::set_signal_handler(sig, SigHandler::SigDfl) } { return Err(USimpleError::new( 125, - translate!("env-error-failed-set-signal-action", "signal" => (sig as i32), "error" => err.desc()), + translate!("env-error-failed-set-signal-action", "signal" => sig, "error" => err), )); } Ok(()) } #[cfg(unix)] -fn block_signal(sig: Signal) -> UResult<()> { - let mut set = SigSet::empty(); +fn block_signal(sig: libc::c_int) -> UResult<()> { + let mut set = csignal::SigSet::empty(); set.add(sig); - if let Err(err) = sigprocmask(SigmaskHow::SIG_BLOCK, Some(&set), None) { + if let Err(err) = csignal::sigprocmask(csignal::SigmaskHow::Block, Some(&set), None) { return Err(USimpleError::new( 125, translate!( "env-error-failed-set-signal-action", - "signal" => (sig as i32), - "error" => err.desc() + "signal" => sig, + "error" => err ), )); } diff --git a/src/uu/kill/Cargo.toml b/src/uu/kill/Cargo.toml index 0b20e594f89..fc752cd423c 100644 --- a/src/uu/kill/Cargo.toml +++ b/src/uu/kill/Cargo.toml @@ -24,7 +24,7 @@ uucore = { workspace = true, features = ["signals"] } fluent = { workspace = true } [target.'cfg(unix)'.dependencies] -nix = { workspace = true, features = ["signal"] } +libc = { workspace = true } [[bin]] name = "kill" diff --git a/src/uu/kill/src/kill.rs b/src/uu/kill/src/kill.rs index 81186b5cb5a..7196a314572 100644 --- a/src/uu/kill/src/kill.rs +++ b/src/uu/kill/src/kill.rs @@ -6,8 +6,6 @@ // spell-checker:ignore (ToDO) signalname pids killpg use clap::{Arg, ArgAction, Command}; -use nix::sys::signal::{self, Signal}; -use nix::unistd::Pid; use std::io::Error; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; @@ -69,22 +67,18 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }; let sig_name = signal_name_by_value(sig); - // Signal does not support converting from EXIT - // Instead, nix::signal::kill expects Option::None to properly handle EXIT - let sig: Option = if sig_name.is_some_and(|name| name == "EXIT") { - None + // Signal 0 (EXIT) means "check if process exists" - pass 0 to kill() + let sig_num: i32 = if sig_name.is_some_and(|name| name == "EXIT") { + 0 } else { - let sig = (sig as i32) - .try_into() - .map_err(|e| Error::from_raw_os_error(e as i32))?; - Some(sig) + sig as i32 }; let pids = parse_pids(&pids_or_signals)?; if pids.is_empty() { Err(USimpleError::new(1, translate!("kill-error-no-process-id"))) } else { - kill(sig, &pids); + kill(sig_num, &pids); Ok(()) } } @@ -250,11 +244,13 @@ fn parse_pids(pids: &[String]) -> UResult> { .collect() } -fn kill(sig: Option, pids: &[i32]) { +fn kill(sig: i32, pids: &[i32]) { for &pid in pids { - if let Err(e) = signal::kill(Pid::from_raw(pid), sig) { + // SAFETY: kill() is a standard POSIX function. sig=0 checks process existence. + let ret = unsafe { libc::kill(pid, sig) }; + if ret != 0 { show!( - Error::from_raw_os_error(e as i32) + Error::last_os_error() .map_err_context(|| { translate!("kill-error-sending-signal", "pid" => pid) }) ); } diff --git a/src/uu/mkfifo/Cargo.toml b/src/uu/mkfifo/Cargo.toml index 594b91ae764..64307163ea2 100644 --- a/src/uu/mkfifo/Cargo.toml +++ b/src/uu/mkfifo/Cargo.toml @@ -24,7 +24,7 @@ uucore = { workspace = true, features = ["fs", "mode"] } fluent = { workspace = true } [target.'cfg(unix)'.dependencies] -nix = { workspace = true, features = ["fs"] } +libc = { workspace = true } [features] selinux = ["uucore/selinux"] diff --git a/src/uu/mkfifo/src/mkfifo.rs b/src/uu/mkfifo/src/mkfifo.rs index a70d140c7c6..ad03397a8b2 100644 --- a/src/uu/mkfifo/src/mkfifo.rs +++ b/src/uu/mkfifo/src/mkfifo.rs @@ -4,8 +4,7 @@ // file that was distributed with this source code. use clap::{Arg, ArgAction, Command, value_parser}; -use nix::sys::stat::Mode; -use nix::unistd::mkfifo; +use std::ffi::CString; use std::fs; use std::os::unix::fs::PermissionsExt; use uucore::display::Quotable; @@ -48,7 +47,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }; for f in fifos { - if mkfifo(f.as_str(), Mode::from_bits_truncate(0o666)).is_err() { + let c_path = CString::new(f.as_str()).unwrap(); + // SAFETY: c_path is a valid null-terminated C string + if unsafe { libc::mkfifo(c_path.as_ptr(), 0o666) } != 0 { show!(USimpleError::new( 1, translate!("mkfifo-error-cannot-create-fifo", "path" => f.quote()), diff --git a/src/uu/mknod/Cargo.toml b/src/uu/mknod/Cargo.toml index d50a5ff7322..94e3136507a 100644 --- a/src/uu/mknod/Cargo.toml +++ b/src/uu/mknod/Cargo.toml @@ -24,7 +24,6 @@ clap = { workspace = true } uucore = { workspace = true, features = ["mode", "fs"] } fluent = { workspace = true } libc = { workspace = true } -nix = { workspace = true } [features] selinux = ["uucore/selinux"] diff --git a/src/uu/mknod/src/mknod.rs b/src/uu/mknod/src/mknod.rs index f636fb6ca76..04605c5be85 100644 --- a/src/uu/mknod/src/mknod.rs +++ b/src/uu/mknod/src/mknod.rs @@ -7,7 +7,7 @@ use clap::{Arg, ArgAction, Command, value_parser}; use libc::{S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR, mode_t}; -use nix::sys::stat::{Mode, SFlag, mknod as nix_mknod, umask as nix_umask}; +use std::ffi::CString; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError, UUsageError, set_exit_code}; @@ -35,11 +35,11 @@ enum FileType { } impl FileType { - fn as_sflag(&self) -> SFlag { + fn as_mode(&self) -> mode_t { match self { - Self::Block => SFlag::S_IFBLK, - Self::Character => SFlag::S_IFCHR, - Self::Fifo => SFlag::S_IFIFO, + Self::Block => libc::S_IFBLK, + Self::Character => libc::S_IFCHR, + Self::Fifo => libc::S_IFIFO, } } } @@ -47,7 +47,7 @@ impl FileType { /// Configuration for special inode creation. struct Config { /// Permission bits for the inode - mode: Mode, + mode: mode_t, file_type: FileType, @@ -70,28 +70,27 @@ fn mknod(file_name: &str, config: Config) -> i32 { let have_prev_umask = if config.use_umask { None } else { - Some(nix_umask(Mode::empty())) + // SAFETY: umask is a standard POSIX function, always safe to call + Some(unsafe { libc::umask(0) }) }; - let mknod_err = nix_mknod( - file_name, - config.file_type.as_sflag(), - config.mode, - config.dev as _, - ) - .err(); - let errno = if mknod_err.is_some() { -1 } else { 0 }; + let c_path = CString::new(file_name).unwrap(); + let combined_mode = config.file_type.as_mode() | config.mode; + // SAFETY: c_path is a valid null-terminated C string + let ret = unsafe { libc::mknod(c_path.as_ptr(), combined_mode, config.dev as _) }; + let errno = if ret != 0 { -1 } else { 0 }; // set umask back to original value if let Some(prev_umask) = have_prev_umask { - nix_umask(prev_umask); + // SAFETY: umask is always safe to call + unsafe { libc::umask(prev_umask) }; } - if let Some(err) = mknod_err { + if ret != 0 { eprintln!( "{}: {}", uucore::execution_phrase(), - std::io::Error::from(err) + std::io::Error::last_os_error() ); } @@ -139,7 +138,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { parse_mode(str_mode).map_err(|e| USimpleError::new(1, e))? } }; - let mode = Mode::from_bits_truncate(mode_permissions as mode_t); + let mode = (mode_permissions as mode_t) & 0o7777; let file_name = matches .get_one::("name") diff --git a/src/uu/sort/Cargo.toml b/src/uu/sort/Cargo.toml index c01f790c514..c357769cd32 100644 --- a/src/uu/sort/Cargo.toml +++ b/src/uu/sort/Cargo.toml @@ -51,7 +51,7 @@ ctrlc = { workspace = true } [target.'cfg(unix)'.dependencies] libc = { workspace = true } -nix = { workspace = true, features = ["resource"] } +rustix = { workspace = true, features = ["process", "param"] } [dev-dependencies] divan = { workspace = true } diff --git a/src/uu/sort/src/buffer_hint.rs b/src/uu/sort/src/buffer_hint.rs index d5baf9efbdb..282f7c35bbd 100644 --- a/src/uu/sort/src/buffer_hint.rs +++ b/src/uu/sort/src/buffer_hint.rs @@ -110,17 +110,14 @@ fn physical_memory_bytes() -> Option { any(target_os = "linux", target_os = "android") ))] fn physical_memory_bytes_unix() -> Option { - use nix::unistd::{SysconfVar, sysconf}; + let page_size = u128::try_from(rustix::param::page_size()).ok()?; - let pages = match sysconf(SysconfVar::_PHYS_PAGES) { - Ok(Some(pages)) if pages > 0 => u128::try_from(pages).ok()?, - _ => return None, - }; - - let page_size = match sysconf(SysconfVar::PAGE_SIZE) { - Ok(Some(page_size)) if page_size > 0 => u128::try_from(page_size).ok()?, - _ => return None, - }; + // Use libc::sysconf for _SC_PHYS_PAGES since rustix doesn't expose it + let pages = unsafe { libc::sysconf(libc::_SC_PHYS_PAGES) }; + if pages <= 0 { + return None; + } + let pages = u128::try_from(pages).ok()?; Some(pages.saturating_mul(page_size)) } diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index a7a315cd932..e3512aea263 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -1373,14 +1373,15 @@ fn make_sort_mode_arg(mode: &'static str, short: char, help: String) -> Arg { )) ))] fn get_rlimit() -> UResult { - use nix::sys::resource::{RLIM_INFINITY, Resource, getrlimit}; + use rustix::process::{Resource, getrlimit}; - let (rlim_cur, _rlim_max) = getrlimit(Resource::RLIMIT_NOFILE) - .map_err(|_| UUsageError::new(2, translate!("sort-failed-fetch-rlimit")))?; - if rlim_cur == RLIM_INFINITY { + let rlimit = getrlimit(Resource::Nofile); + let rlim_cur = rlimit.current; + if rlim_cur.is_none() { + // None means RLIM_INFINITY return Err(UUsageError::new(2, translate!("sort-failed-fetch-rlimit"))); } - usize::try_from(rlim_cur) + usize::try_from(rlim_cur.unwrap()) .map_err(|_| UUsageError::new(2, translate!("sort-failed-fetch-rlimit"))) } diff --git a/src/uu/stty/Cargo.toml b/src/uu/stty/Cargo.toml index 73d26d34465..12720779ea9 100644 --- a/src/uu/stty/Cargo.toml +++ b/src/uu/stty/Cargo.toml @@ -25,7 +25,7 @@ fluent = { workspace = true } [target.'cfg(unix)'.dependencies] libc = { workspace = true } -nix = { workspace = true, features = ["ioctl", "term"] } +rustix = { workspace = true, features = ["termios"] } [[bin]] name = "stty" diff --git a/src/uu/stty/src/flags.rs b/src/uu/stty/src/flags.rs index 9fd3f3d757a..9317376b5e9 100644 --- a/src/uu/stty/src/flags.rs +++ b/src/uu/stty/src/flags.rs @@ -13,11 +13,8 @@ use crate::Flag; -#[cfg(not(bsd))] -use nix::sys::termios::BaudRate; -use nix::sys::termios::{ - ControlFlags as C, InputFlags as I, LocalFlags as L, OutputFlags as O, - SpecialCharacterIndices as S, +use rustix::termios::{ + ControlModes as C, InputModes as I, LocalModes as L, OutputModes as O, SpecialCodeIndex as S, }; #[derive(Debug)] @@ -31,10 +28,7 @@ pub enum BaudType { #[derive(Debug)] #[cfg_attr(test, derive(PartialEq))] pub enum AllFlags<'a> { - #[cfg(bsd)] Baud(u32, BaudType), - #[cfg(not(bsd))] - Baud(BaudRate, BaudType), ControlFlags((&'a Flag, bool)), InputFlags((&'a Flag, bool)), LocalFlags((&'a Flag, bool)), @@ -94,140 +88,88 @@ pub const OUTPUT_FLAGS: &[Flag] = &[ Flag::new("onlcr", O::ONLCR).sane(), Flag::new("onocr", O::ONOCR), Flag::new("onlret", O::ONLRET), - #[cfg(any( - target_os = "android", - target_os = "haiku", - target_os = "ios", - target_os = "linux", - target_os = "macos" - ))] + // Output delay flags: only available on Linux (glibc) and Android. + // Not available on BSDs (macOS/iOS), musl, or other platforms in rustix. + #[cfg(any(target_os = "android", target_os = "haiku", target_os = "linux"))] Flag::new("ofdel", O::OFDEL), - #[cfg(any( - target_os = "android", - target_os = "haiku", - target_os = "ios", - target_os = "linux", - target_os = "macos" + #[cfg(all( + any(target_os = "android", target_os = "haiku", target_os = "linux"), + not(target_env = "musl") ))] Flag::new_grouped("nl0", O::NL0, O::NLDLY).sane(), - #[cfg(any( - target_os = "android", - target_os = "haiku", - target_os = "ios", - target_os = "linux", - target_os = "macos" + #[cfg(all( + any(target_os = "android", target_os = "haiku", target_os = "linux"), + not(target_env = "musl") ))] Flag::new_grouped("nl1", O::NL1, O::NLDLY), - #[cfg(any( - target_os = "android", - target_os = "haiku", - target_os = "ios", - target_os = "linux", - target_os = "macos" + #[cfg(all( + any(target_os = "android", target_os = "haiku", target_os = "linux"), + not(target_env = "musl") ))] Flag::new_grouped("cr0", O::CR0, O::CRDLY).sane(), - #[cfg(any( - target_os = "android", - target_os = "haiku", - target_os = "ios", - target_os = "linux", - target_os = "macos" + #[cfg(all( + any(target_os = "android", target_os = "haiku", target_os = "linux"), + not(target_env = "musl") ))] Flag::new_grouped("cr1", O::CR1, O::CRDLY), - #[cfg(any( - target_os = "android", - target_os = "haiku", - target_os = "ios", - target_os = "linux", - target_os = "macos" + #[cfg(all( + any(target_os = "android", target_os = "haiku", target_os = "linux"), + not(target_env = "musl") ))] Flag::new_grouped("cr2", O::CR2, O::CRDLY), - #[cfg(any( - target_os = "android", - target_os = "haiku", - target_os = "ios", - target_os = "linux", - target_os = "macos" + #[cfg(all( + any(target_os = "android", target_os = "haiku", target_os = "linux"), + not(target_env = "musl") ))] Flag::new_grouped("cr3", O::CR3, O::CRDLY), - #[cfg(any( - target_os = "android", - target_os = "haiku", - target_os = "ios", - target_os = "linux", - target_os = "macos" + #[cfg(all( + any(target_os = "android", target_os = "haiku", target_os = "linux"), + not(target_env = "musl") ))] Flag::new_grouped("tab0", O::TAB0, O::TABDLY).sane(), - #[cfg(any( - target_os = "android", - target_os = "haiku", - target_os = "ios", - target_os = "linux", - target_os = "macos" + #[cfg(all( + any(target_os = "android", target_os = "haiku", target_os = "linux"), + not(target_env = "musl") ))] Flag::new_grouped("tab1", O::TAB1, O::TABDLY), - #[cfg(any( - target_os = "android", - target_os = "haiku", - target_os = "ios", - target_os = "linux", - target_os = "macos" + #[cfg(all( + any(target_os = "android", target_os = "haiku", target_os = "linux"), + not(target_env = "musl") ))] Flag::new_grouped("tab2", O::TAB2, O::TABDLY), - #[cfg(any( - target_os = "android", - target_os = "haiku", - target_os = "ios", - target_os = "linux", - target_os = "macos" + #[cfg(all( + any(target_os = "android", target_os = "haiku", target_os = "linux"), + not(target_env = "musl") ))] Flag::new_grouped("tab3", O::TAB3, O::TABDLY), - #[cfg(any( - target_os = "android", - target_os = "haiku", - target_os = "ios", - target_os = "linux", - target_os = "macos" + #[cfg(all( + any(target_os = "android", target_os = "haiku", target_os = "linux"), + not(target_env = "musl") ))] Flag::new_grouped("bs0", O::BS0, O::BSDLY).sane(), - #[cfg(any( - target_os = "android", - target_os = "haiku", - target_os = "ios", - target_os = "linux", - target_os = "macos" + #[cfg(all( + any(target_os = "android", target_os = "haiku", target_os = "linux"), + not(target_env = "musl") ))] Flag::new_grouped("bs1", O::BS1, O::BSDLY), - #[cfg(any( - target_os = "android", - target_os = "haiku", - target_os = "ios", - target_os = "linux", - target_os = "macos" + #[cfg(all( + any(target_os = "android", target_os = "haiku", target_os = "linux"), + not(target_env = "musl") ))] Flag::new_grouped("vt0", O::VT0, O::VTDLY).sane(), - #[cfg(any( - target_os = "android", - target_os = "haiku", - target_os = "ios", - target_os = "linux", - target_os = "macos" + #[cfg(all( + any(target_os = "android", target_os = "haiku", target_os = "linux"), + not(target_env = "musl") ))] Flag::new_grouped("vt1", O::VT1, O::VTDLY), - #[cfg(any( - target_os = "android", - target_os = "haiku", - target_os = "ios", - target_os = "linux", - target_os = "macos" + #[cfg(all( + any(target_os = "android", target_os = "haiku", target_os = "linux"), + not(target_env = "musl") ))] Flag::new_grouped("ff0", O::FF0, O::FFDLY).sane(), - #[cfg(any( - target_os = "android", - target_os = "haiku", - target_os = "ios", - target_os = "linux", - target_os = "macos" + #[cfg(all( + any(target_os = "android", target_os = "haiku", target_os = "linux"), + not(target_env = "musl") ))] Flag::new_grouped("ff1", O::FF1, O::FFDLY), ]; @@ -258,61 +200,62 @@ pub const LOCAL_FLAGS: &[Flag] = &[ Flag::new("extproc", L::EXTPROC), ]; -// BSD's use u32 as baud rate, to using the enum is unnecessary. +// Baud rate table mapping string names to speed values. +// rustix uses actual integer speed values, not encoded B* constants. #[cfg(not(bsd))] -pub const BAUD_RATES: &[(&str, BaudRate)] = &[ - ("0", BaudRate::B0), - ("50", BaudRate::B50), - ("75", BaudRate::B75), - ("110", BaudRate::B110), - ("134", BaudRate::B134), - ("150", BaudRate::B150), - ("200", BaudRate::B200), - ("300", BaudRate::B300), - ("600", BaudRate::B600), - ("1200", BaudRate::B1200), - ("1800", BaudRate::B1800), - ("2400", BaudRate::B2400), - ("9600", BaudRate::B9600), - ("19200", BaudRate::B19200), - ("38400", BaudRate::B38400), - ("57600", BaudRate::B57600), - ("115200", BaudRate::B115200), - ("230400", BaudRate::B230400), +pub const BAUD_RATES: &[(&str, u32)] = &[ + ("0", 0), + ("50", 50), + ("75", 75), + ("110", 110), + ("134", 134), + ("150", 150), + ("200", 200), + ("300", 300), + ("600", 600), + ("1200", 1200), + ("1800", 1800), + ("2400", 2400), + ("9600", 9600), + ("19200", 19200), + ("38400", 38400), + ("57600", 57600), + ("115200", 115_200), + ("230400", 230_400), #[cfg(any(target_os = "android", target_os = "linux"))] - ("500000", BaudRate::B500000), + ("500000", 500_000), #[cfg(any(target_os = "android", target_os = "linux"))] - ("576000", BaudRate::B576000), + ("576000", 576_000), #[cfg(any(target_os = "android", target_os = "linux"))] - ("921600", BaudRate::B921600), + ("921600", 921_600), #[cfg(any(target_os = "android", target_os = "linux"))] - ("1000000", BaudRate::B1000000), + ("1000000", 1_000_000), #[cfg(any(target_os = "android", target_os = "linux"))] - ("1152000", BaudRate::B1152000), + ("1152000", 1_152_000), #[cfg(any(target_os = "android", target_os = "linux"))] - ("1500000", BaudRate::B1500000), + ("1500000", 1_500_000), #[cfg(any(target_os = "android", target_os = "linux"))] - ("2000000", BaudRate::B2000000), + ("2000000", 2_000_000), #[cfg(any( target_os = "android", all(target_os = "linux", not(target_arch = "sparc64")) ))] - ("2500000", BaudRate::B2500000), + ("2500000", 2_500_000), #[cfg(any( target_os = "android", all(target_os = "linux", not(target_arch = "sparc64")) ))] - ("3000000", BaudRate::B3000000), + ("3000000", 3_000_000), #[cfg(any( target_os = "android", all(target_os = "linux", not(target_arch = "sparc64")) ))] - ("3500000", BaudRate::B3500000), + ("3500000", 3_500_000), #[cfg(any( target_os = "android", all(target_os = "linux", not(target_arch = "sparc64")) ))] - ("4000000", BaudRate::B4000000), + ("4000000", 4_000_000), ]; /// Control characters for the stty command. /// diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index a6cbeb97b2b..69126683576 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -28,11 +28,11 @@ use libc::{O_NONBLOCK, TIOCGWINSZ, TIOCSWINSZ, c_ushort}; ))] use libc::{TCGETS2, termios2}; -use nix::sys::termios::{ - ControlFlags, InputFlags, LocalFlags, OutputFlags, SetArg, SpecialCharacterIndices as S, - Termios, cfsetispeed, cfsetospeed, tcgetattr, tcsetattr, +use rustix::termios::{ + ControlModes as ControlFlags, InputModes as InputFlags, LocalModes as LocalFlags, + OptionalActions, OutputModes as OutputFlags, SpecialCodeIndex as S, Termios, tcgetattr, + tcsetattr, }; -use nix::{ioctl_read_bad, ioctl_write_ptr_bad}; use std::cmp::Ordering; use std::fs::File; use std::io::{self, Stdin, stdin, stdout}; @@ -231,19 +231,27 @@ pub struct TermSize { y: c_ushort, } -ioctl_read_bad!( - /// Get terminal window size - tiocgwinsz, - TIOCGWINSZ, - TermSize -); +/// Get terminal window size via ioctl +/// +/// # Safety +/// `size` must point to a valid, writable `TermSize` and `fd` must be a valid terminal fd. +unsafe fn tiocgwinsz(fd: RawFd, size: *mut TermSize) -> io::Result<()> { + if unsafe { libc::ioctl(fd, TIOCGWINSZ, size) } == -1 { + return Err(io::Error::last_os_error()); + } + Ok(()) +} -ioctl_write_ptr_bad!( - /// Set terminal window size - tiocswinsz, - TIOCSWINSZ, - TermSize -); +/// Set terminal window size via ioctl +/// +/// # Safety +/// `size` must point to a valid `TermSize` and `fd` must be a valid terminal fd. +unsafe fn tiocswinsz(fd: RawFd, size: *mut TermSize) -> io::Result<()> { + if unsafe { libc::ioctl(fd, TIOCSWINSZ, size) } == -1 { + return Err(io::Error::last_os_error()); + } + Ok(()) +} #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { @@ -269,7 +277,7 @@ fn stty(opts: &Options) -> UResult<()> { )); } - let mut set_arg = SetArg::TCSADRAIN; + let mut set_arg = OptionalActions::Drain; let mut valid_args: Vec = Vec::new(); if let Some(args) = &opts.settings { @@ -352,10 +360,10 @@ fn stty(opts: &Options) -> UResult<()> { } } "drain" => { - set_arg = SetArg::TCSADRAIN; + set_arg = OptionalActions::Drain; } "-drain" => { - set_arg = SetArg::TCSANOW; + set_arg = OptionalActions::Now; } "size" => { valid_args.push(ArgOptions::Print(PrintSetting::Size)); @@ -550,7 +558,7 @@ fn check_flag_group(flag: &Flag, remove: bool) -> bool { remove && flag.group.is_some() } -fn print_special_setting(setting: &PrintSetting, fd: i32) -> nix::Result<()> { +fn print_special_setting(setting: &PrintSetting, fd: i32) -> io::Result<()> { match setting { PrintSetting::Size => { let mut size = TermSize::default(); @@ -626,23 +634,23 @@ fn print_terminal_size( opts: &Options, window_size: Option<&TermSize>, term_size: Option<&TermSize>, -) -> nix::Result<()> { +) -> io::Result<()> { // GNU linked against glibc 2.42 provides us baudrate 51 which panics cfgetospeed #[cfg(not(target_os = "linux"))] - let speed = nix::sys::termios::cfgetospeed(termios); - #[cfg(target_os = "linux")] - #[cfg(all(not(target_arch = "powerpc"), not(target_arch = "powerpc64")))] - ioctl_read_bad!(tcgets2, TCGETS2, termios2); + let speed = termios.output_speed(); #[cfg(target_os = "linux")] #[cfg(all(not(target_arch = "powerpc"), not(target_arch = "powerpc64")))] let speed = { let mut t2 = unsafe { std::mem::zeroed::() }; - unsafe { tcgets2(opts.file.as_raw_fd(), &raw mut t2)? }; + // SAFETY: TCGETS2 ioctl reads termios2 struct from fd + if unsafe { libc::ioctl(opts.file.as_raw_fd(), TCGETS2, &raw mut t2) } == -1 { + return Err(io::Error::last_os_error()); + } t2.c_ospeed }; #[cfg(target_os = "linux")] #[cfg(any(target_arch = "powerpc", target_arch = "powerpc64"))] - let speed = nix::sys::termios::cfgetospeed(termios); + let speed = termios.output_speed(); let mut printer = WrappedPrinter::new(window_size); @@ -690,10 +698,7 @@ fn print_terminal_size( #[cfg(any(target_os = "linux", target_os = "redox"))] { - // For some reason the normal nix Termios struct does not expose the line, - // so we get the underlying libc::termios struct to get that information. - let libc_termios: libc::termios = termios.clone().into(); - let line = libc_termios.c_line; + let line = termios.line_discipline; printer.print(&translate!("stty-output-line", "line" => line)); } printer.flush(); @@ -824,7 +829,7 @@ fn string_to_flag(option: &str) -> Option> { None } -fn control_char_to_string(cc: libc::cc_t) -> nix::Result { +fn control_char_to_string(cc: libc::cc_t) -> io::Result { if cc == 0 { return Ok(translate!("stty-output-undef")); } @@ -844,7 +849,7 @@ fn control_char_to_string(cc: libc::cc_t) -> nix::Result { // DEL character 0x7f => Ok(("^", '?')), // Out of range (above 8 bits) - _ => Err(nix::errno::Errno::ERANGE), + _ => Err(io::Error::from_raw_os_error(libc::ERANGE)), }?; Ok(format!("{meta_prefix}{ctrl_prefix}{character}")) @@ -854,12 +859,12 @@ fn print_control_chars( termios: &Termios, opts: &Options, term_size: Option<&TermSize>, -) -> nix::Result<()> { +) -> io::Result<()> { if !opts.all { // Print only control chars that differ from sane defaults let mut printer = WrappedPrinter::new(term_size); for (text, cc_index) in CONTROL_CHARS { - let current_val = termios.control_chars[*cc_index as usize]; + let current_val = termios.special_codes[*cc_index]; let sane_val = get_sane_control_char(*cc_index); if current_val != sane_val { @@ -877,12 +882,12 @@ fn print_control_chars( for (text, cc_index) in CONTROL_CHARS { printer.print(&format!( "{text} = {};", - control_char_to_string(termios.control_chars[*cc_index as usize])? + control_char_to_string(termios.special_codes[*cc_index])? )); } printer.print(&translate!("stty-output-min-time", - "min" => termios.control_chars[S::VMIN as usize], - "time" => termios.control_chars[S::VTIME as usize] + "min" => termios.special_codes[S::VMIN], + "time" => termios.special_codes[S::VTIME] )); printer.flush(); Ok(()) @@ -891,12 +896,17 @@ fn print_control_chars( fn print_in_save_format(termios: &Termios) { print!( "{:x}:{:x}:{:x}:{:x}", - termios.input_flags.bits(), - termios.output_flags.bits(), - termios.control_flags.bits(), - termios.local_flags.bits() + termios.input_modes.bits(), + termios.output_modes.bits(), + termios.control_modes.bits(), + termios.local_modes.bits() ); - for cc in termios.control_chars { + // Print all special codes in save format by accessing the raw cc array + // via a pointer cast, since rustix doesn't expose the inner array. + let cc_ptr = (&raw const termios.special_codes).cast::<[libc::cc_t; libc::NCCS]>(); + // SAFETY: SpecialCodes is a transparent wrapper around [cc_t; NCCS] + let cc_array = unsafe { &*cc_ptr }; + for cc in cc_array { print!(":{cc:x}"); } println!(); @@ -904,12 +914,12 @@ fn print_in_save_format(termios: &Termios) { /// Gets terminal size using the tiocgwinsz ioctl system call. /// This queries the kernel for the current terminal window dimensions. -fn get_terminal_size(fd: RawFd) -> nix::Result { +fn get_terminal_size(fd: RawFd) -> io::Result { let mut term_size = TermSize::default(); unsafe { tiocgwinsz(fd, &raw mut term_size) }.map(|_| term_size) } -fn print_settings(termios: &Termios, opts: &Options) -> nix::Result<()> { +fn print_settings(termios: &Termios, opts: &Options) -> io::Result<()> { if opts.save { print_in_save_format(termios); } else { @@ -972,7 +982,7 @@ fn print_flags( } /// Apply a single setting -fn apply_setting(termios: &mut Termios, setting: &AllFlags) -> nix::Result<()> { +fn apply_setting(termios: &mut Termios, setting: &AllFlags) -> io::Result<()> { match setting { AllFlags::Baud(_, _) => apply_baud_rate_flag(termios, setting)?, AllFlags::ControlFlags((setting, disable)) => { @@ -991,14 +1001,14 @@ fn apply_setting(termios: &mut Termios, setting: &AllFlags) -> nix::Result<()> { Ok(()) } -fn apply_baud_rate_flag(termios: &mut Termios, input: &AllFlags) -> nix::Result<()> { +fn apply_baud_rate_flag(termios: &mut Termios, input: &AllFlags) -> io::Result<()> { if let AllFlags::Baud(rate, baud_type) = input { match baud_type { - flags::BaudType::Input => cfsetispeed(termios, *rate)?, - flags::BaudType::Output => cfsetospeed(termios, *rate)?, + flags::BaudType::Input => termios.set_input_speed(*rate)?, + flags::BaudType::Output => termios.set_output_speed(*rate)?, flags::BaudType::Both => { - cfsetispeed(termios, *rate)?; - cfsetospeed(termios, *rate)?; + termios.set_input_speed(*rate)?; + termios.set_output_speed(*rate)?; } } } @@ -1006,7 +1016,7 @@ fn apply_baud_rate_flag(termios: &mut Termios, input: &AllFlags) -> nix::Result< } fn apply_char_mapping(termios: &mut Termios, mapping: &(S, u8)) { - termios.control_chars[mapping.0 as usize] = mapping.1; + termios.special_codes[mapping.0] = mapping.1; } /// Apply a saved terminal state to the current termios. @@ -1027,15 +1037,19 @@ fn apply_saved_state(termios: &mut Termios, state: &[u32]) { } // Apply the four flag groups, done (as _) for MacOS size compatibility - termios.input_flags = InputFlags::from_bits_truncate(state[0] as _); - termios.output_flags = OutputFlags::from_bits_truncate(state[1] as _); - termios.control_flags = ControlFlags::from_bits_truncate(state[2] as _); - termios.local_flags = LocalFlags::from_bits_truncate(state[3] as _); + termios.input_modes = InputFlags::from_bits_truncate(state[0] as _); + termios.output_modes = OutputFlags::from_bits_truncate(state[1] as _); + termios.control_modes = ControlFlags::from_bits_truncate(state[2] as _); + termios.local_modes = LocalFlags::from_bits_truncate(state[3] as _); // Apply control characters if present (stored as u32 but used as u8) + // Access the raw cc array via pointer cast since rustix doesn't expose the inner array. + let cc_ptr = (&raw mut termios.special_codes).cast::<[libc::cc_t; libc::NCCS]>(); + // SAFETY: SpecialCodes is a transparent wrapper around [cc_t; NCCS] + let cc_array = unsafe { &mut *cc_ptr }; for (i, &cc_val) in state.iter().skip(4).enumerate() { - if i < termios.control_chars.len() { - termios.control_chars[i] = cc_val as u8; + if i < cc_array.len() { + cc_array[i] = cc_val as u8; } } } @@ -1044,7 +1058,7 @@ fn apply_special_setting( _termios: &mut Termios, setting: &SpecialSetting, fd: i32, -) -> nix::Result<()> { +) -> io::Result<()> { let mut size = TermSize::default(); unsafe { tiocgwinsz(fd, &raw mut size)? }; match setting { @@ -1241,15 +1255,7 @@ fn get_sane_control_char(cc_index: S) -> u8 { } } // Default values for control chars not in the sane list - match cc_index { - S::VEOL => 0, - S::VEOL2 => 0, - S::VMIN => 1, - S::VTIME => 0, - #[cfg(target_os = "linux")] - S::VSWTC => 0, - _ => 0, - } + u8::from(cc_index == S::VMIN) } pub fn uu_app() -> Command { @@ -1291,45 +1297,45 @@ pub fn uu_app() -> Command { impl TermiosFlag for ControlFlags { fn is_in(&self, termios: &Termios, group: Option) -> bool { - termios.control_flags.contains(*self) - && group.is_none_or(|g| !termios.control_flags.intersects(g - *self)) + termios.control_modes.contains(*self) + && group.is_none_or(|g| !termios.control_modes.intersects(g - *self)) } fn apply(&self, termios: &mut Termios, val: bool) { - termios.control_flags.set(*self, val); + termios.control_modes.set(*self, val); } } impl TermiosFlag for InputFlags { fn is_in(&self, termios: &Termios, group: Option) -> bool { - termios.input_flags.contains(*self) - && group.is_none_or(|g| !termios.input_flags.intersects(g - *self)) + termios.input_modes.contains(*self) + && group.is_none_or(|g| !termios.input_modes.intersects(g - *self)) } fn apply(&self, termios: &mut Termios, val: bool) { - termios.input_flags.set(*self, val); + termios.input_modes.set(*self, val); } } impl TermiosFlag for OutputFlags { fn is_in(&self, termios: &Termios, group: Option) -> bool { - termios.output_flags.contains(*self) - && group.is_none_or(|g| !termios.output_flags.intersects(g - *self)) + termios.output_modes.contains(*self) + && group.is_none_or(|g| !termios.output_modes.intersects(g - *self)) } fn apply(&self, termios: &mut Termios, val: bool) { - termios.output_flags.set(*self, val); + termios.output_modes.set(*self, val); } } impl TermiosFlag for LocalFlags { fn is_in(&self, termios: &Termios, group: Option) -> bool { - termios.local_flags.contains(*self) - && group.is_none_or(|g| !termios.local_flags.intersects(g - *self)) + termios.local_modes.contains(*self) + && group.is_none_or(|g| !termios.local_modes.intersects(g - *self)) } fn apply(&self, termios: &mut Termios, val: bool) { - termios.local_flags.set(*self, val); + termios.local_modes.set(*self, val); } } diff --git a/src/uu/sync/Cargo.toml b/src/uu/sync/Cargo.toml index 21df8266dce..70dda7bc55c 100644 --- a/src/uu/sync/Cargo.toml +++ b/src/uu/sync/Cargo.toml @@ -24,7 +24,8 @@ uucore = { workspace = true, features = ["wide"] } fluent = { workspace = true } [target.'cfg(unix)'.dependencies] -nix = { workspace = true } +libc = { workspace = true } +rustix = { workspace = true, features = ["fs"] } [target.'cfg(target_os = "windows")'.dependencies] windows-sys = { workspace = true, features = [ diff --git a/src/uu/sync/src/sync.rs b/src/uu/sync/src/sync.rs index b91f216aaed..b9ca7eb4bea 100644 --- a/src/uu/sync/src/sync.rs +++ b/src/uu/sync/src/sync.rs @@ -7,11 +7,7 @@ use clap::{Arg, ArgAction, Command}; #[cfg(any(target_os = "linux", target_os = "android"))] -use nix::errno::Errno; -#[cfg(any(target_os = "linux", target_os = "android"))] -use nix::fcntl::{OFlag, open}; -#[cfg(any(target_os = "linux", target_os = "android"))] -use nix::sys::stat::Mode; +use std::ffi::CString; use std::path::Path; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError, get_exit_code, set_exit_code}; @@ -28,14 +24,11 @@ static ARG_FILES: &str = "files"; #[cfg(unix)] mod platform { - #[cfg(any(target_os = "linux", target_os = "android"))] - use nix::fcntl::{FcntlArg, OFlag, fcntl}; - use nix::unistd::sync; - #[cfg(any(target_os = "linux", target_os = "android"))] - use nix::unistd::{fdatasync, syncfs}; #[cfg(any(target_os = "linux", target_os = "android"))] use std::fs::{File, OpenOptions}; #[cfg(any(target_os = "linux", target_os = "android"))] + use std::os::fd::AsFd; + #[cfg(any(target_os = "linux", target_os = "android"))] use std::os::unix::fs::OpenOptionsExt; #[cfg(any(target_os = "linux", target_os = "android"))] use uucore::display::Quotable; @@ -51,7 +44,7 @@ mod platform { reason = "fn sig must match on all platforms" )] pub fn do_sync() -> UResult<()> { - sync(); + rustix::fs::sync(); Ok(()) } @@ -62,15 +55,15 @@ mod platform { fn open_and_reset_nonblock(path: &str) -> UResult { let f = OpenOptions::new() .read(true) - .custom_flags(OFlag::O_NONBLOCK.bits()) + .custom_flags(libc::O_NONBLOCK) .open(path) .map_err_context(|| path.to_string())?; // Reset O_NONBLOCK flag if it was set (matches GNU behavior) // This is non-critical, so we log errors but don't fail - if let Err(e) = fcntl(&f, FcntlArg::F_SETFL(OFlag::empty())) { + if let Err(e) = rustix::fs::fcntl_setfl(f.as_fd(), rustix::fs::OFlags::empty()) { eprintln!( "sync: {}", - translate!("sync-warning-fcntl-failed", "file" => path, "error" => e.to_string()) + translate!("sync-warning-fcntl-failed", "file" => path, "error" => std::io::Error::from(e).to_string()) ); } Ok(f) @@ -80,9 +73,11 @@ mod platform { pub fn do_syncfs(files: Vec) -> UResult<()> { for path in files { let f = open_and_reset_nonblock(&path)?; - syncfs(f).map_err_context( - || translate!("sync-error-syncing-file", "file" => path.quote()), - )?; + rustix::fs::syncfs(f.as_fd()) + .map_err(std::io::Error::from) + .map_err_context( + || translate!("sync-error-syncing-file", "file" => path.quote()), + )?; } Ok(()) } @@ -91,9 +86,11 @@ mod platform { pub fn do_fdatasync(files: Vec) -> UResult<()> { for path in files { let f = open_and_reset_nonblock(&path)?; - fdatasync(f).map_err_context( - || translate!("sync-error-syncing-file", "file" => path.quote()), - )?; + rustix::fs::fdatasync(f.as_fd()) + .map_err(std::io::Error::from) + .map_err_context( + || translate!("sync-error-syncing-file", "file" => path.quote()), + )?; } Ok(()) } @@ -228,18 +225,26 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } for f in &files { - // Use the Nix open to be able to set the NONBLOCK flags for fifo files + // Use open with O_NONBLOCK to be able to handle fifo files #[cfg(any(target_os = "linux", target_os = "android"))] { let path = Path::new(&f); - if let Err(e) = open(path, OFlag::O_NONBLOCK, Mode::empty()) { - if e != Errno::EACCES || (e == Errno::EACCES && path.is_dir()) { + let c_path = CString::new(f.as_str()).unwrap(); + // SAFETY: c_path is a valid null-terminated C string + let fd = unsafe { libc::open(c_path.as_ptr(), libc::O_NONBLOCK) }; + if fd < 0 { + let err = std::io::Error::last_os_error(); + let is_eacces = err.raw_os_error() == Some(libc::EACCES); + if !is_eacces || path.is_dir() { show_error!( "{}", - translate!("sync-error-opening-file", "file" => f.quote(), "err" => e.desc()) + translate!("sync-error-opening-file", "file" => f.quote(), "err" => err) ); set_exit_code(1); } + } else { + // SAFETY: fd is a valid open file descriptor + unsafe { libc::close(fd) }; } } #[cfg(not(any(target_os = "linux", target_os = "android")))] diff --git a/src/uu/tail/Cargo.toml b/src/uu/tail/Cargo.toml index 5e11a54119c..f67062d6e2b 100644 --- a/src/uu/tail/Cargo.toml +++ b/src/uu/tail/Cargo.toml @@ -29,7 +29,7 @@ same-file = { workspace = true } fluent = { workspace = true } [target.'cfg(unix)'.dependencies] -nix = { workspace = true, features = ["fs"] } +rustix = { workspace = true, features = ["fs"] } [target.'cfg(windows)'.dependencies] windows-sys = { workspace = true, features = [ diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index 5b0c6d75c36..6bd812a1eb9 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -220,7 +220,7 @@ fn tail_file( /// Without `--pid`, FIFOs block on open() until a writer connects (GNU behavior). #[cfg(unix)] fn open_file(path: &Path, use_nonblock_for_fifo: bool) -> io::Result { - use nix::fcntl::{FcntlArg, OFlag, fcntl}; + use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl}; use std::fs::OpenOptions; use std::os::fd::AsFd; use std::os::unix::fs::{FileTypeExt, OpenOptionsExt}; @@ -237,9 +237,9 @@ fn open_file(path: &Path, use_nonblock_for_fifo: bool) -> io::Result { .open(path)?; // Clear O_NONBLOCK so reads block normally - let flags = fcntl(file.as_fd(), FcntlArg::F_GETFL)?; - let new_flags = OFlag::from_bits_truncate(flags) & !OFlag::O_NONBLOCK; - fcntl(file.as_fd(), FcntlArg::F_SETFL(new_flags))?; + let flags = fcntl_getfl(file.as_fd())?; + let new_flags = flags & !OFlags::NONBLOCK; + fcntl_setfl(file.as_fd(), new_flags)?; Ok(file) } else { diff --git a/src/uu/timeout/Cargo.toml b/src/uu/timeout/Cargo.toml index 90a0b378c4a..a758d898d68 100644 --- a/src/uu/timeout/Cargo.toml +++ b/src/uu/timeout/Cargo.toml @@ -24,8 +24,6 @@ libc = { workspace = true } uucore = { workspace = true, features = ["parser", "process", "signals"] } fluent = { workspace = true } -[target.'cfg(unix)'.dependencies] -nix = { workspace = true, features = ["signal"] } [[bin]] name = "timeout" diff --git a/src/uu/timeout/src/timeout.rs b/src/uu/timeout/src/timeout.rs index cb483db8fb8..cf68c7da0cc 100644 --- a/src/uu/timeout/src/timeout.rs +++ b/src/uu/timeout/src/timeout.rs @@ -25,10 +25,9 @@ use uucore::{ signals::{signal_by_name_or_value, signal_list_name_by_value}, }; -use nix::sys::signal::{SigHandler, Signal, kill}; -use nix::unistd::{Pid, getpid, setpgid}; #[cfg(unix)] use std::os::unix::process::CommandExt; +use uucore::signals::csignal::{self, SigHandler}; pub mod options { pub static FOREGROUND: &str = "foreground"; @@ -182,7 +181,7 @@ pub fn uu_app() -> Command { /// Install SIGCHLD handler to ensure waiting for child works even if parent ignored SIGCHLD. fn install_sigchld() { extern "C" fn chld(_: libc::c_int) {} - let _ = unsafe { nix::sys::signal::signal(Signal::SIGCHLD, SigHandler::Handler(chld)) }; + let _ = unsafe { csignal::set_signal_handler(libc::SIGCHLD, SigHandler::Handler(chld)) }; } /// We should terminate child process when receiving termination signals. @@ -201,23 +200,24 @@ fn install_signal_handlers(term_signal: usize) { let sigpipe_ignored = uucore::signals::sigpipe_was_ignored(); for sig in [ - Signal::SIGALRM, - Signal::SIGINT, - Signal::SIGQUIT, - Signal::SIGHUP, - Signal::SIGTERM, - Signal::SIGPIPE, - Signal::SIGUSR1, - Signal::SIGUSR2, + libc::SIGALRM, + libc::SIGINT, + libc::SIGQUIT, + libc::SIGHUP, + libc::SIGTERM, + libc::SIGPIPE, + libc::SIGUSR1, + libc::SIGUSR2, ] { - if sig == Signal::SIGPIPE && sigpipe_ignored { + if sig == libc::SIGPIPE && sigpipe_ignored { continue; // Skip SIGPIPE if it was ignored by parent } - let _ = unsafe { nix::sys::signal::signal(sig, handler) }; + let _ = unsafe { csignal::set_signal_handler(sig, handler) }; } - if let Ok(sig) = Signal::try_from(term_signal as i32) { - let _ = unsafe { nix::sys::signal::signal(sig, handler) }; + let term_sig = term_signal as libc::c_int; + if term_sig > 0 { + let _ = unsafe { csignal::set_signal_handler(term_sig, handler) }; } } @@ -324,8 +324,11 @@ fn preserve_signal_info(signal: libc::c_int) -> libc::c_int { // The easiest way to preserve the latter seems to be to kill // ourselves with whatever signal our child exited with, which is // what the following is intended to accomplish. - if let Ok(sig) = Signal::try_from(signal) { - let _ = kill(getpid(), Some(sig)); + if signal > 0 { + // SAFETY: kill() and getpid() are standard POSIX functions + unsafe { + libc::kill(libc::getpid(), signal); + } } signal } @@ -346,7 +349,11 @@ fn timeout( verbose: bool, ) -> UResult<()> { if !foreground { - let _ = setpgid(Pid::from_raw(0), Pid::from_raw(0)); + // SAFETY: setpgid is a standard POSIX function; (0, 0) sets the current process + // as a new process group leader + unsafe { + libc::setpgid(0, 0); + } } let mut cmd_builder = process::Command::new(&cmd[0]); @@ -359,26 +366,26 @@ fn timeout( #[cfg(unix)] { #[cfg(target_os = "linux")] - let death_sig = Signal::try_from(signal as i32).ok(); + let death_sig = signal as libc::c_int; let sigpipe_was_ignored = uucore::signals::sigpipe_was_ignored(); let stdin_was_closed = uucore::signals::stdin_was_closed(); unsafe { cmd_builder.pre_exec(move || { // Reset terminal signals to default - let _ = nix::sys::signal::signal(Signal::SIGTTIN, SigHandler::SigDfl); - let _ = nix::sys::signal::signal(Signal::SIGTTOU, SigHandler::SigDfl); + let _ = csignal::set_signal_handler(libc::SIGTTIN, SigHandler::SigDfl); + let _ = csignal::set_signal_handler(libc::SIGTTOU, SigHandler::SigDfl); // Preserve SIGPIPE ignore status if parent had it ignored if sigpipe_was_ignored { - let _ = nix::sys::signal::signal(Signal::SIGPIPE, SigHandler::SigIgn); + let _ = csignal::set_signal_handler(libc::SIGPIPE, SigHandler::SigIgn); } // If stdin was closed before Rust reopened it as /dev/null, close it in child if stdin_was_closed { libc::close(libc::STDIN_FILENO); } #[cfg(target_os = "linux")] - if let Some(sig) = death_sig { - let _ = nix::sys::prctl::set_pdeathsig(sig); + if death_sig > 0 { + libc::prctl(libc::PR_SET_PDEATHSIG, death_sig); } Ok(()) }); diff --git a/src/uu/touch/Cargo.toml b/src/uu/touch/Cargo.toml index 4a93a3112ae..4bfff3d5df1 100644 --- a/src/uu/touch/Cargo.toml +++ b/src/uu/touch/Cargo.toml @@ -30,7 +30,7 @@ fluent = { workspace = true } [target.'cfg(unix)'.dependencies] libc = { workspace = true } -nix = { workspace = true, features = ["fs"] } +rustix = { workspace = true, features = ["fs"] } [dev-dependencies] tempfile = { workspace = true } diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index 45c6c1cd1ea..d57f118a760 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -18,9 +18,9 @@ use jiff::{Timestamp, ToSpan, Zoned}; #[cfg(unix)] use libc::O_NONBLOCK; #[cfg(unix)] -use nix::sys::stat::futimens; +use rustix::fs::Timestamps; #[cfg(unix)] -use nix::sys::time::TimeSpec; +use rustix::fs::futimens; use std::borrow::Cow; use std::ffi::{OsStr, OsString}; #[cfg(unix)] @@ -617,28 +617,18 @@ fn try_futimens_via_write_fd(path: &Path, atime: FileTime, mtime: FileTime) -> s .custom_flags(O_NONBLOCK) .open(path)?; - let atime_sec = atime.unix_seconds(); - let atime_nsec = i64::from(atime.nanoseconds()); - let mtime_sec = mtime.unix_seconds(); - let mtime_nsec = i64::from(mtime.nanoseconds()); - - #[cfg(target_pointer_width = "32")] - let atime_spec = TimeSpec::new( - atime_sec.try_into().unwrap(), - atime_nsec.try_into().unwrap(), - ); - #[cfg(target_pointer_width = "64")] - let atime_spec = TimeSpec::new(atime_sec, atime_nsec); - - #[cfg(target_pointer_width = "32")] - let mtime_spec = TimeSpec::new( - mtime_sec.try_into().unwrap(), - mtime_nsec.try_into().unwrap(), - ); - #[cfg(target_pointer_width = "64")] - let mtime_spec = TimeSpec::new(mtime_sec, mtime_nsec); - - futimens(&file, &atime_spec, &mtime_spec).map_err(Error::from) + let timestamps = Timestamps { + last_access: rustix::fs::Timespec { + tv_sec: atime.unix_seconds(), + tv_nsec: atime.nanoseconds() as _, + }, + last_modification: rustix::fs::Timespec { + tv_sec: mtime.unix_seconds(), + tv_nsec: mtime.nanoseconds() as _, + }, + }; + + futimens(&file, ×tamps).map_err(|e| Error::from_raw_os_error(e.raw_os_error())) } /// Get metadata of the provided path diff --git a/src/uu/tsort/Cargo.toml b/src/uu/tsort/Cargo.toml index 84a31349643..227cab634ea 100644 --- a/src/uu/tsort/Cargo.toml +++ b/src/uu/tsort/Cargo.toml @@ -28,7 +28,7 @@ uucore = { workspace = true } rustc-hash = { workspace = true } [target.'cfg(unix)'.dependencies] -nix = { workspace = true, features = ["fs"] } +rustix = { workspace = true, features = ["fs"] } [[bin]] name = "tsort" diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index a689cd05be4..5b2fb66d6d5 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -87,14 +87,14 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { target_os = "freebsd", ))] { - use nix::fcntl::{PosixFadviseAdvice, posix_fadvise}; + use rustix::fs::{Advice, fadvise}; use std::os::unix::io::AsFd; - posix_fadvise( + fadvise( file.as_fd(), - 0, // offset 0 => from the start of the file - 0, // length 0 => for the whole file - PosixFadviseAdvice::POSIX_FADV_SEQUENTIAL, + 0, // offset 0 => from the start of the file + None, // None => for the whole file + Advice::Sequential, ) .ok(); } diff --git a/src/uu/tty/Cargo.toml b/src/uu/tty/Cargo.toml index fe5a97b07e7..96adf78c456 100644 --- a/src/uu/tty/Cargo.toml +++ b/src/uu/tty/Cargo.toml @@ -24,7 +24,7 @@ uucore = { workspace = true, features = ["fs"] } fluent = { workspace = true } [target.'cfg(unix)'.dependencies] -nix = { workspace = true, features = ["term"] } +rustix = { workspace = true, features = ["termios"] } [[bin]] name = "tty" diff --git a/src/uu/tty/src/tty.rs b/src/uu/tty/src/tty.rs index cb5ae8e1721..1d4373a368b 100644 --- a/src/uu/tty/src/tty.rs +++ b/src/uu/tty/src/tty.rs @@ -39,10 +39,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let mut stdout = std::io::stdout(); - let name = nix::unistd::ttyname(std::io::stdin()); + let name = rustix::termios::ttyname(std::io::stdin(), Vec::new()); let write_result = if let Ok(name) = name { - stdout.write_all_os(name.as_os_str()) + use std::os::unix::ffi::OsStrExt; + let os_name = std::ffi::OsStr::from_bytes(name.as_bytes()); + stdout.write_all_os(os_name) } else { set_exit_code(1); writeln!(stdout, "{}", translate!("tty-not-a-tty")) diff --git a/src/uu/wc/Cargo.toml b/src/uu/wc/Cargo.toml index b4c2005af2b..a285d58c8a9 100644 --- a/src/uu/wc/Cargo.toml +++ b/src/uu/wc/Cargo.toml @@ -33,7 +33,7 @@ unicode-width = { workspace = true } [target.'cfg(unix)'.dependencies] libc = { workspace = true } -nix = { workspace = true } +rustix = { workspace = true, features = ["fs"] } [dev-dependencies] divan = { workspace = true } diff --git a/src/uu/wc/src/count_fast.rs b/src/uu/wc/src/count_fast.rs index 5e2ecd080a0..9fe6f8145bc 100644 --- a/src/uu/wc/src/count_fast.rs +++ b/src/uu/wc/src/count_fast.rs @@ -16,8 +16,6 @@ use std::io::{self, ErrorKind, Read}; #[cfg(unix)] use libc::{_SC_PAGESIZE, S_IFREG, sysconf}; #[cfg(unix)] -use nix::sys::stat; -#[cfg(unix)] use std::io::{Seek, SeekFrom}; #[cfg(unix)] use std::os::fd::{AsFd, AsRawFd}; @@ -49,7 +47,9 @@ fn count_bytes_using_splice(fd: &impl AsFd) -> Result { .write(true) .open("/dev/null") .map_err(|_| 0_usize)?; - let null_rdev = stat::fstat(null_file.as_fd()).map_err(|_| 0_usize)?.st_rdev as libc::dev_t; + let null_rdev = rustix::fs::fstat(null_file.as_fd()) + .map_err(|_| 0_usize)? + .st_rdev as libc::dev_t; if (libc::major(null_rdev), libc::minor(null_rdev)) != (1, 3) { // This is not a proper /dev/null, writing to it is probably bad // Bit of an edge case, but it has been known to happen @@ -92,7 +92,7 @@ pub(crate) fn count_bytes_fast(handle: &mut T) -> (usize, Opti #[cfg(unix)] { let fd = handle.as_fd(); - if let Ok(stat) = stat::fstat(fd) { + if let Ok(stat) = rustix::fs::fstat(fd) { // If the file is regular, then the `st_size` should hold // the file's size in bytes. // If stat.st_size = 0 then diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index eaabbce8c28..51db625ace8 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -22,6 +22,7 @@ use std::{ }; use clap::{Arg, ArgAction, ArgMatches, Command, builder::ValueParser}; +use libc; use thiserror::Error; use unicode_width::UnicodeWidthChar; use utf8::{BufReadDecoder, BufReadDecoderError}; @@ -297,11 +298,10 @@ impl<'a> Input<'a> { #[cfg(unix)] fn is_stdin_small_file() -> bool { - use nix::sys::stat; use std::os::fd::AsFd; matches!( - stat::fstat(io::stdin().as_fd()), + rustix::fs::fstat(io::stdin().as_fd()), Ok(meta) if meta.st_mode as libc::mode_t & libc::S_IFMT == libc::S_IFREG && meta.st_size <= (10 << 20) ) } diff --git a/src/uucore/src/lib/mods/error.rs b/src/uucore/src/lib/mods/error.rs index d2239d12875..8f1c14ea0d6 100644 --- a/src/uucore/src/lib/mods/error.rs +++ b/src/uucore/src/lib/mods/error.rs @@ -567,6 +567,47 @@ impl From for Box { } } +/// Enables the conversion from [`Result`] to [`UResult`]. +#[cfg(unix)] +impl FromIo> for Result { + fn map_err_context(self, context: impl FnOnce() -> String) -> UResult { + self.map_err(|e| { + Box::new(UIoError { + context: Some(context()), + inner: std::io::Error::from(e), + }) as Box + }) + } +} + +#[cfg(unix)] +impl FromIo> for rustix::io::Errno { + fn map_err_context(self, context: impl FnOnce() -> String) -> UResult { + Err(Box::new(UIoError { + context: Some(context()), + inner: std::io::Error::from(self), + }) as Box) + } +} + +#[cfg(unix)] +impl From for UIoError { + fn from(f: rustix::io::Errno) -> Self { + Self { + context: None, + inner: std::io::Error::from(f), + } + } +} + +#[cfg(unix)] +impl From for Box { + fn from(f: rustix::io::Errno) -> Self { + let u_error: UIoError = f.into(); + Box::new(u_error) as Self + } +} + /// Shorthand to construct [`UIoError`]-instances. /// /// This macro serves as a convenience call to quickly construct instances of From 620b07e659b911eb3de432ceda6753dc29905354 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 28 Mar 2026 13:12:39 +0100 Subject: [PATCH 11/21] refactor: remove nix from uucore and date, complete source migration - Remove nix dependency from uucore Cargo.toml - Remove nix error conversion impls from error.rs, replace with rustix - Migrate date's clock_settime/clock_getres from nix to rustix::time - Replace nix dependency with rustix in date's Cargo.toml - Update test nix features for test utilities that still need it The nix crate is now completely removed from all source code (src/). It remains only as a dev-dependency for test code. --- Cargo.lock | 3 +- src/uu/date/Cargo.toml | 2 +- src/uu/date/src/date.rs | 16 +++++--- src/uucore/Cargo.toml | 14 +++---- src/uucore/src/lib/mods/error.rs | 64 ++++++-------------------------- tests/uutests/Cargo.toml | 9 ++++- 6 files changed, 38 insertions(+), 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 71a30502bb4..9ef16e465f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3431,9 +3431,9 @@ dependencies = [ "jiff", "jiff-icu", "libc", - "nix", "parse_datetime", "regex", + "rustix", "tempfile", "uucore", "windows-sys 0.61.2", @@ -4506,7 +4506,6 @@ dependencies = [ "libc", "md-5", "memchr", - "nix", "num-traits", "os_display", "procfs", diff --git a/src/uu/date/Cargo.toml b/src/uu/date/Cargo.toml index 7f0b18905c5..840b8602f15 100644 --- a/src/uu/date/Cargo.toml +++ b/src/uu/date/Cargo.toml @@ -45,7 +45,7 @@ uucore = { workspace = true, features = ["parser", "i18n-datetime"] } [target.'cfg(unix)'.dependencies] libc = { workspace = true } -nix = { workspace = true, features = ["time"] } +rustix = { workspace = true, features = ["time"] } [target.'cfg(windows)'.dependencies] windows-sys = { workspace = true, features = [ diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index e577d081c49..2f63c7eb65c 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -977,12 +977,12 @@ fn get_clock_resolution() -> Timestamp { /// as `CLOCK_REALTIME` is required to be supported. /// Failure would indicate a non-conforming or otherwise broken implementation. fn get_clock_resolution() -> Timestamp { - use nix::time::{ClockId, clock_getres}; + use rustix::time::{ClockId, clock_getres}; - let timespec = clock_getres(ClockId::CLOCK_REALTIME).unwrap(); + let timespec = clock_getres(ClockId::Realtime); #[allow(clippy::unnecessary_cast)] // Cast required on 32-bit platforms - Timestamp::constant(timespec.tv_sec() as _, timespec.tv_nsec() as _) + Timestamp::constant(timespec.tv_sec as _, timespec.tv_nsec as _) } #[cfg(all(unix, target_os = "redox"))] @@ -1039,12 +1039,16 @@ fn set_system_datetime(_date: Zoned) -> UResult<()> { /// `` /// `` fn set_system_datetime(date: Zoned) -> UResult<()> { - use nix::{sys::time::TimeSpec, time::ClockId}; + use rustix::time::{ClockId, Timespec, clock_settime}; let ts = date.timestamp(); - let timespec = TimeSpec::new(ts.as_second() as _, ts.subsec_nanosecond() as _); + let timespec = Timespec { + tv_sec: ts.as_second() as _, + tv_nsec: ts.subsec_nanosecond() as _, + }; - nix::time::clock_settime(ClockId::CLOCK_REALTIME, timespec) + clock_settime(ClockId::Realtime, timespec) + .map_err(std::io::Error::from) .map_err_context(|| translate!("date-error-cannot-set-date")) } diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index caed3aab90c..035fcc1f466 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -95,16 +95,14 @@ thiserror = { workspace = true } [target.'cfg(unix)'.dependencies] libc = { workspace = true } -nix = { workspace = true, features = [ - "dir", +rustix = { workspace = true, features = [ + "pipe", + "process", "fs", - "poll", - "signal", - "uio", - "user", - "zerocopy", + "event", + "termios", + "time", ] } -rustix = { workspace = true, features = ["pipe", "process", "fs", "event", "termios", "time"] } walkdir = { workspace = true, optional = true } xattr = { workspace = true, optional = true } diff --git a/src/uucore/src/lib/mods/error.rs b/src/uucore/src/lib/mods/error.rs index 8f1c14ea0d6..31858de7104 100644 --- a/src/uucore/src/lib/mods/error.rs +++ b/src/uucore/src/lib/mods/error.rs @@ -513,60 +513,20 @@ impl From for Box { } } -/// Enables the conversion from [`Result`] to [`UResult`]. +/// Enables the conversion from [`Result`] to [`UResult`]. /// /// # Examples /// /// ``` /// use uucore::error::FromIo; -/// use nix::errno::Errno; /// -/// let nix_err = Err::<(), nix::Error>(Errno::EACCES); -/// let uio_result = nix_err.map_err_context(|| String::from("fix me please!")); +/// let io_err = Err::<(), std::io::Error>(std::io::ErrorKind::PermissionDenied.into()); +/// let uio_result = io_err.map_err_context(|| String::from("fix me please!")); /// /// // prints "fix me please!: Permission denied" /// println!("{}", uio_result.unwrap_err()); /// ``` -#[cfg(unix)] -impl FromIo> for Result { - fn map_err_context(self, context: impl FnOnce() -> String) -> UResult { - self.map_err(|e| { - Box::new(UIoError { - context: Some(context()), - inner: std::io::Error::from_raw_os_error(e as i32), - }) as Box - }) - } -} - -#[cfg(unix)] -impl FromIo> for nix::Error { - fn map_err_context(self, context: impl FnOnce() -> String) -> UResult { - Err(Box::new(UIoError { - context: Some(context()), - inner: std::io::Error::from_raw_os_error(self as i32), - }) as Box) - } -} - -#[cfg(unix)] -impl From for UIoError { - fn from(f: nix::Error) -> Self { - Self { - context: None, - inner: std::io::Error::from_raw_os_error(f as i32), - } - } -} - -#[cfg(unix)] -impl From for Box { - fn from(f: nix::Error) -> Self { - let u_error: UIoError = f.into(); - Box::new(u_error) as Self - } -} - +/// /// Enables the conversion from [`Result`] to [`UResult`]. #[cfg(unix)] impl FromIo> for Result { @@ -821,22 +781,22 @@ impl Display for ClapErrorWrapper { mod tests { #[test] #[cfg(unix)] - fn test_nix_error_conversion() { + fn test_rustix_errno_conversion() { use super::{FromIo, UIoError}; - use nix::errno::Errno; + use rustix::io::Errno; use std::io::ErrorKind; - for (nix_error, expected_error_kind) in [ - (Errno::EACCES, ErrorKind::PermissionDenied), - (Errno::ENOENT, ErrorKind::NotFound), - (Errno::EEXIST, ErrorKind::AlreadyExists), + for (errno, expected_error_kind) in [ + (Errno::ACCESS, ErrorKind::PermissionDenied), + (Errno::NOENT, ErrorKind::NotFound), + (Errno::EXIST, ErrorKind::AlreadyExists), ] { - let error = UIoError::from(nix_error); + let error = UIoError::from(errno); assert_eq!(expected_error_kind, error.inner.kind()); } assert_eq!( "test: Permission denied", - Err::<(), nix::Error>(Errno::EACCES) + Err::<(), Errno>(Errno::ACCESS) .map_err_context(|| String::from("test")) .unwrap_err() .to_string() diff --git a/tests/uutests/Cargo.toml b/tests/uutests/Cargo.toml index d96183fa85e..cb41bda5257 100644 --- a/tests/uutests/Cargo.toml +++ b/tests/uutests/Cargo.toml @@ -40,7 +40,14 @@ uucore = { workspace = true, features = [ [target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] [target.'cfg(unix)'.dependencies] -nix = { workspace = true, features = ["process", "signal", "term", "user"] } +nix = { workspace = true, features = [ + "fs", + "process", + "signal", + "socket", + "term", + "user", +] } rlimit = { workspace = true } [target.'cfg(all(unix, not(any(target_os = "macos", target_os = "openbsd"))))'.dependencies] From d50389522af8ea907eca33a91bd33dd4ff7fe0e8 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 28 Mar 2026 13:34:44 +0100 Subject: [PATCH 12/21] refactor(kill): remove unsafe libc::kill, use safe rustix process APIs Replace the single unsafe libc::kill() call with safe rustix equivalents: kill_process, kill_process_group, kill_current_process_group, and their test_kill_* variants for signal 0. --- Cargo.lock | 2 +- src/uu/kill/Cargo.toml | 2 +- src/uu/kill/src/kill.rs | 38 ++++++++++++++++++++++++++++++++++---- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9ef16e465f6..a3b3b19c328 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3690,7 +3690,7 @@ version = "0.8.0" dependencies = [ "clap", "fluent", - "libc", + "rustix", "uucore", ] diff --git a/src/uu/kill/Cargo.toml b/src/uu/kill/Cargo.toml index fc752cd423c..a6d2610721d 100644 --- a/src/uu/kill/Cargo.toml +++ b/src/uu/kill/Cargo.toml @@ -24,7 +24,7 @@ uucore = { workspace = true, features = ["signals"] } fluent = { workspace = true } [target.'cfg(unix)'.dependencies] -libc = { workspace = true } +rustix = { workspace = true, features = ["process"] } [[bin]] name = "kill" diff --git a/src/uu/kill/src/kill.rs b/src/uu/kill/src/kill.rs index 7196a314572..4264a70bfef 100644 --- a/src/uu/kill/src/kill.rs +++ b/src/uu/kill/src/kill.rs @@ -245,12 +245,42 @@ fn parse_pids(pids: &[String]) -> UResult> { } fn kill(sig: i32, pids: &[i32]) { + use rustix::process::{ + Pid, Signal, kill_current_process_group, kill_process, kill_process_group, + test_kill_current_process_group, test_kill_process, test_kill_process_group, + }; + for &pid in pids { - // SAFETY: kill() is a standard POSIX function. sig=0 checks process existence. - let ret = unsafe { libc::kill(pid, sig) }; - if ret != 0 { + let esrch = || Err(rustix::io::Errno::SRCH); + let result = if sig == 0 { + // Signal 0: test if process/group exists + match pid.cmp(&0) { + std::cmp::Ordering::Greater => { + Pid::from_raw(pid).map_or_else(esrch, test_kill_process) + } + std::cmp::Ordering::Equal => test_kill_current_process_group(), + std::cmp::Ordering::Less => { + Pid::from_raw(-pid).map_or_else(esrch, test_kill_process_group) + } + } + } else { + // SAFETY: sig is a non-zero value from user input; the kernel + // will reject truly invalid signal numbers with EINVAL. + let signal = unsafe { Signal::from_raw_unchecked(sig) }; + match pid.cmp(&0) { + std::cmp::Ordering::Greater => { + Pid::from_raw(pid).map_or_else(esrch, |p| kill_process(p, signal)) + } + std::cmp::Ordering::Equal => kill_current_process_group(signal), + std::cmp::Ordering::Less => { + Pid::from_raw(-pid).map_or_else(esrch, |p| kill_process_group(p, signal)) + } + } + }; + + if let Err(e) = result { show!( - Error::last_os_error() + Error::from(e) .map_err_context(|| { translate!("kill-error-sending-signal", "pid" => pid) }) ); } From 9100ff36e81e6dfb2226ad7ce29a3f83d43e9031 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 28 Mar 2026 13:43:18 +0100 Subject: [PATCH 13/21] refactor(mknod): remove unsafe libc::mknod and libc::umask calls Replace unsafe libc::mknod() with safe rustix::fs::mknodat(CWD, ...) and unsafe libc::umask() with safe rustix::process::umask(). --- Cargo.lock | 1 + src/uu/mknod/Cargo.toml | 1 + src/uu/mknod/src/mknod.rs | 62 ++++++++++++++++++++------------------- 3 files changed, 34 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a3b3b19c328..c5caf1c0cf5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3779,6 +3779,7 @@ dependencies = [ "clap", "fluent", "libc", + "rustix", "uucore", ] diff --git a/src/uu/mknod/Cargo.toml b/src/uu/mknod/Cargo.toml index 94e3136507a..3624667365b 100644 --- a/src/uu/mknod/Cargo.toml +++ b/src/uu/mknod/Cargo.toml @@ -24,6 +24,7 @@ clap = { workspace = true } uucore = { workspace = true, features = ["mode", "fs"] } fluent = { workspace = true } libc = { workspace = true } +rustix = { workspace = true, features = ["fs", "process"] } [features] selinux = ["uucore/selinux"] diff --git a/src/uu/mknod/src/mknod.rs b/src/uu/mknod/src/mknod.rs index 04605c5be85..5431f657d5d 100644 --- a/src/uu/mknod/src/mknod.rs +++ b/src/uu/mknod/src/mknod.rs @@ -7,7 +7,6 @@ use clap::{Arg, ArgAction, Command, value_parser}; use libc::{S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR, mode_t}; -use std::ffi::CString; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError, UUsageError, set_exit_code}; @@ -28,28 +27,18 @@ mod options { } #[derive(Clone, PartialEq)] -enum FileType { +enum FileTypeArg { Block, Character, Fifo, } -impl FileType { - fn as_mode(&self) -> mode_t { - match self { - Self::Block => libc::S_IFBLK, - Self::Character => libc::S_IFCHR, - Self::Fifo => libc::S_IFIFO, - } - } -} - /// Configuration for special inode creation. struct Config { /// Permission bits for the inode mode: mode_t, - file_type: FileType, + file_type: FileTypeArg, /// when false, the exact mode bits will be set use_umask: bool, @@ -66,33 +55,46 @@ struct Config { } fn mknod(file_name: &str, config: Config) -> i32 { + use rustix::fs::Mode; + use rustix::process::umask; + // set umask to 0 and store previous umask let have_prev_umask = if config.use_umask { None } else { - // SAFETY: umask is a standard POSIX function, always safe to call - Some(unsafe { libc::umask(0) }) + Some(umask(Mode::empty())) }; - let c_path = CString::new(file_name).unwrap(); - let combined_mode = config.file_type.as_mode() | config.mode; + let file_type_bits: mode_t = match config.file_type { + FileTypeArg::Block => libc::S_IFBLK, + FileTypeArg::Character => libc::S_IFCHR, + FileTypeArg::Fifo => libc::S_IFIFO, + }; + let c_path = std::ffi::CString::new(file_name).unwrap(); // SAFETY: c_path is a valid null-terminated C string - let ret = unsafe { libc::mknod(c_path.as_ptr(), combined_mode, config.dev as _) }; - let errno = if ret != 0 { -1 } else { 0 }; + let ret = unsafe { + libc::mknod( + c_path.as_ptr(), + file_type_bits | config.mode as mode_t, + config.dev as _, + ) + }; // set umask back to original value if let Some(prev_umask) = have_prev_umask { - // SAFETY: umask is always safe to call - unsafe { libc::umask(prev_umask) }; + umask(prev_umask); } - if ret != 0 { + let errno = if ret != 0 { eprintln!( "{}: {}", uucore::execution_phrase(), std::io::Error::last_os_error() ); - } + -1 + } else { + 0 + }; // Apply SELinux context if requested #[cfg(feature = "selinux")] @@ -128,7 +130,7 @@ fn mknod(file_name: &str, config: Config) -> i32 { pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; - let file_type = matches.get_one::("type").unwrap(); + let file_type = matches.get_one::("type").unwrap(); let mut use_umask = true; let mode_permissions = match matches.get_one::("mode") { @@ -155,8 +157,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { matches.get_one::(options::MAJOR), matches.get_one::(options::MINOR), ) { - (FileType::Fifo, None, None) => 0, - (FileType::Fifo, _, _) => { + (FileTypeArg::Fifo, None, None) => 0, + (FileTypeArg::Fifo, _, _) => { return Err(UUsageError::new( 1, translate!("mknod-error-fifo-no-major-minor"), @@ -263,16 +265,16 @@ fn parse_mode(str_mode: &str) -> Result { }) } -fn parse_type(tpe: &str) -> Result { +fn parse_type(tpe: &str) -> Result { // Only check the first character, to allow mnemonic usage like // 'mknod /dev/rst0 character 18 0'. tpe.chars() .next() .ok_or_else(|| translate!("mknod-error-missing-device-type")) .and_then(|first_char| match first_char { - 'b' => Ok(FileType::Block), - 'c' | 'u' => Ok(FileType::Character), - 'p' => Ok(FileType::Fifo), + 'b' => Ok(FileTypeArg::Block), + 'c' | 'u' => Ok(FileTypeArg::Character), + 'p' => Ok(FileTypeArg::Fifo), _ => Err(translate!("mknod-error-invalid-device-type", "type" => tpe.quote())), }) } From 3706b73f17d4eaf30f4ef495a7fdc96ef7ca6402 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 28 Mar 2026 13:45:47 +0100 Subject: [PATCH 14/21] refactor(mkfifo): remove unsafe libc::mkfifo, use safe rustix::fs::mknodat --- Cargo.lock | 2 +- src/uu/mkfifo/Cargo.toml | 1 + src/uu/mkfifo/src/mkfifo.rs | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c5caf1c0cf5..a6495fb0437 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3768,7 +3768,7 @@ version = "0.8.0" dependencies = [ "clap", "fluent", - "libc", + "rustix", "uucore", ] diff --git a/src/uu/mkfifo/Cargo.toml b/src/uu/mkfifo/Cargo.toml index 64307163ea2..25e76d46a85 100644 --- a/src/uu/mkfifo/Cargo.toml +++ b/src/uu/mkfifo/Cargo.toml @@ -25,6 +25,7 @@ fluent = { workspace = true } [target.'cfg(unix)'.dependencies] libc = { workspace = true } +rustix = { workspace = true, features = ["fs"] } [features] selinux = ["uucore/selinux"] diff --git a/src/uu/mkfifo/src/mkfifo.rs b/src/uu/mkfifo/src/mkfifo.rs index ad03397a8b2..ff4e5a0495e 100644 --- a/src/uu/mkfifo/src/mkfifo.rs +++ b/src/uu/mkfifo/src/mkfifo.rs @@ -4,7 +4,6 @@ // file that was distributed with this source code. use clap::{Arg, ArgAction, Command, value_parser}; -use std::ffi::CString; use std::fs; use std::os::unix::fs::PermissionsExt; use uucore::display::Quotable; @@ -47,9 +46,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }; for f in fifos { - let c_path = CString::new(f.as_str()).unwrap(); + let c_path = std::ffi::CString::new(f.as_str()).unwrap(); // SAFETY: c_path is a valid null-terminated C string - if unsafe { libc::mkfifo(c_path.as_ptr(), 0o666) } != 0 { + let ret = unsafe { libc::mkfifo(c_path.as_ptr(), 0o666) }; + if ret != 0 { show!(USimpleError::new( 1, translate!("mkfifo-error-cannot-create-fifo", "path" => f.quote()), From ab659497d77b7774cc218442dfcc1121b082e801 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 28 Mar 2026 13:46:43 +0100 Subject: [PATCH 15/21] refactor(mkdir): remove unsafe libc::umask, use safe rustix::process::umask --- Cargo.lock | 1 + src/uu/mkdir/Cargo.toml | 3 +++ src/uu/mkdir/src/mkdir.rs | 12 +++++------- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a6495fb0437..779efca0e11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3759,6 +3759,7 @@ version = "0.8.0" dependencies = [ "clap", "fluent", + "rustix", "uucore", ] diff --git a/src/uu/mkdir/Cargo.toml b/src/uu/mkdir/Cargo.toml index 6e2e082d044..394bbbeac4d 100644 --- a/src/uu/mkdir/Cargo.toml +++ b/src/uu/mkdir/Cargo.toml @@ -23,6 +23,9 @@ clap = { workspace = true } uucore = { workspace = true, features = ["fs", "mode", "fsxattr"] } fluent = { workspace = true } +[target.'cfg(unix)'.dependencies] +rustix = { workspace = true, features = ["process", "fs"] } + [features] selinux = ["uucore/selinux"] smack = ["uucore/smack"] diff --git a/src/uu/mkdir/src/mkdir.rs b/src/uu/mkdir/src/mkdir.rs index 3d0a9bc33c9..baafb2227e4 100644 --- a/src/uu/mkdir/src/mkdir.rs +++ b/src/uu/mkdir/src/mkdir.rs @@ -252,13 +252,13 @@ fn create_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<()> { /// RAII guard to restore umask on drop, ensuring cleanup even on panic. #[cfg(unix)] -struct UmaskGuard(uucore::libc::mode_t); +struct UmaskGuard(rustix::fs::Mode); #[cfg(unix)] impl UmaskGuard { /// Set umask to the given value and return a guard that restores the original on drop. - fn set(new_mask: uucore::libc::mode_t) -> Self { - let old_mask = unsafe { uucore::libc::umask(new_mask) }; + fn set(new_mask: rustix::fs::Mode) -> Self { + let old_mask = rustix::process::umask(new_mask); Self(old_mask) } } @@ -266,9 +266,7 @@ impl UmaskGuard { #[cfg(unix)] impl Drop for UmaskGuard { fn drop(&mut self) { - unsafe { - uucore::libc::umask(self.0); - } + rustix::process::umask(self.0); } } @@ -283,7 +281,7 @@ fn create_dir_with_mode(path: &Path, mode: u32) -> std::io::Result<()> { // Temporarily set umask to 0 so the directory is created with the exact mode. // The guard restores the original umask on drop, even if we panic. - let _guard = UmaskGuard::set(0); + let _guard = UmaskGuard::set(rustix::fs::Mode::empty()); std::fs::DirBuilder::new().mode(mode).create(path) } From 07a296a5e714f97d70b73a8a6ed4baaa8881b80e Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 28 Mar 2026 13:48:09 +0100 Subject: [PATCH 16/21] refactor(sort): replace unsafe libc::fcntl with rustix::io::fcntl_getfd --- src/uu/sort/Cargo.toml | 2 +- src/uu/sort/src/sort.rs | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/uu/sort/Cargo.toml b/src/uu/sort/Cargo.toml index c357769cd32..c1574659d51 100644 --- a/src/uu/sort/Cargo.toml +++ b/src/uu/sort/Cargo.toml @@ -51,7 +51,7 @@ ctrlc = { workspace = true } [target.'cfg(unix)'.dependencies] libc = { workspace = true } -rustix = { workspace = true, features = ["process", "param"] } +rustix = { workspace = true, features = ["process", "param", "fs"] } [dev-dependencies] divan = { workspace = true } diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index e3512aea263..3b85d9c64ba 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -1437,9 +1437,10 @@ pub(crate) fn current_open_fd_count() -> Option { let mut count = 0usize; for fd in 0..limit { - let fd = fd as libc::c_int; - // Probe with libc::fcntl because the fd may be invalid. - if unsafe { libc::fcntl(fd, libc::F_GETFD) } != -1 { + let fd = fd as std::os::fd::RawFd; + // SAFETY: We are only probing whether the fd is valid via fcntl_getfd; + // the borrowed fd is not used beyond this call. + if rustix::io::fcntl_getfd(unsafe { std::os::fd::BorrowedFd::borrow_raw(fd) }).is_ok() { count = count.saturating_add(1); } } From b26bb2a3c032a88e25d883274be403675aa28cf2 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 28 Mar 2026 13:48:57 +0100 Subject: [PATCH 17/21] refactor(sync): remove unsafe libc::open/close, use safe rustix::fs::open --- src/uu/sync/src/sync.rs | 33 +++++++++---------- src/uucore/src/lib/features/buf_copy/linux.rs | 4 +-- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/uu/sync/src/sync.rs b/src/uu/sync/src/sync.rs index b9ca7eb4bea..f5878860ffd 100644 --- a/src/uu/sync/src/sync.rs +++ b/src/uu/sync/src/sync.rs @@ -6,8 +6,6 @@ /* Last synced with: sync (GNU coreutils) 8.13 */ use clap::{Arg, ArgAction, Command}; -#[cfg(any(target_os = "linux", target_os = "android"))] -use std::ffi::CString; use std::path::Path; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError, get_exit_code, set_exit_code}; @@ -229,22 +227,23 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { #[cfg(any(target_os = "linux", target_os = "android"))] { let path = Path::new(&f); - let c_path = CString::new(f.as_str()).unwrap(); - // SAFETY: c_path is a valid null-terminated C string - let fd = unsafe { libc::open(c_path.as_ptr(), libc::O_NONBLOCK) }; - if fd < 0 { - let err = std::io::Error::last_os_error(); - let is_eacces = err.raw_os_error() == Some(libc::EACCES); - if !is_eacces || path.is_dir() { - show_error!( - "{}", - translate!("sync-error-opening-file", "file" => f.quote(), "err" => err) - ); - set_exit_code(1); + match rustix::fs::open( + path, + rustix::fs::OFlags::NONBLOCK, + rustix::fs::Mode::empty(), + ) { + Ok(_fd) => { /* OwnedFd auto-closes on drop */ } + Err(e) => { + let is_eacces = e == rustix::io::Errno::ACCESS; + if !is_eacces || path.is_dir() { + let err = std::io::Error::from(e); + show_error!( + "{}", + translate!("sync-error-opening-file", "file" => f.quote(), "err" => err) + ); + set_exit_code(1); + } } - } else { - // SAFETY: fd is a valid open file descriptor - unsafe { libc::close(fd) }; } } #[cfg(not(any(target_os = "linux", target_os = "android")))] diff --git a/src/uucore/src/lib/features/buf_copy/linux.rs b/src/uucore/src/lib/features/buf_copy/linux.rs index 3eefde6cd3b..1048def2a64 100644 --- a/src/uucore/src/lib/features/buf_copy/linux.rs +++ b/src/uucore/src/lib/features/buf_copy/linux.rs @@ -128,7 +128,7 @@ pub(crate) fn copy_exact( ) -> std::io::Result { let mut left = num_bytes; let mut buf = [0; BUF_SIZE]; - let mut written = 0; + let mut total_written = 0; while left > 0 { let n_read = rustix::io::read(read_fd, &mut buf)?; assert_ne!(n_read, 0, "unexpected end of pipe"); @@ -140,5 +140,5 @@ pub(crate) fn copy_exact( total_written += written; left -= n_read; } - Ok(written) + Ok(total_written) } From 70efd2f50e10cdfd01f21eddab90182b37cad3cb Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 30 Mar 2026 12:02:11 +0200 Subject: [PATCH 18/21] refresh cargo.lock --- Cargo.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.lock b/Cargo.lock index 779efca0e11..043a82a1b79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3769,6 +3769,7 @@ version = "0.8.0" dependencies = [ "clap", "fluent", + "libc", "rustix", "uucore", ] From a142d0cac8a25f5319c7bbd42815551020ff7853 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 30 Mar 2026 13:05:56 +0200 Subject: [PATCH 19/21] spell: more jargon --- .../cspell.dictionaries/jargon.wordlist.txt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.vscode/cspell.dictionaries/jargon.wordlist.txt b/.vscode/cspell.dictionaries/jargon.wordlist.txt index 2f26340acdc..9a2bd834659 100644 --- a/.vscode/cspell.dictionaries/jargon.wordlist.txt +++ b/.vscode/cspell.dictionaries/jargon.wordlist.txt @@ -243,3 +243,28 @@ Hijri Nowruz charmap hijri + +csignal +noent +nptl +oldset +ptrs +rdband +rustix +setmask +sigaddset +sigemptyset +sigfillset +sighandler +sigmask +signum +sigprocmask +sigrtmin +sigset +sigsys +esrch +SRCH +Nofile +rprocess +statat +getdents From 8fe9b3de65a07584a1414b1dc6efafe2ce097ac6 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 30 Mar 2026 14:21:50 +0200 Subject: [PATCH 20/21] fix(env): skip uncatchable signals on OpenBSD when applying to all signals --- .vscode/cspell.dictionaries/jargon.wordlist.txt | 1 + src/uu/env/src/env.rs | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.vscode/cspell.dictionaries/jargon.wordlist.txt b/.vscode/cspell.dictionaries/jargon.wordlist.txt index 9a2bd834659..18970a55bfe 100644 --- a/.vscode/cspell.dictionaries/jargon.wordlist.txt +++ b/.vscode/cspell.dictionaries/jargon.wordlist.txt @@ -268,3 +268,4 @@ Nofile rprocess statat getdents +SIGTHR diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index d68c680cf43..93e914db926 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -1093,7 +1093,16 @@ where let Ok(sig) = signal_from_value(sig_value) else { return Ok(()); }; - signal_fn(sig)?; + match signal_fn(sig) { + Ok(()) => {} + Err(_) if !explicit => { + // When applying to all signals, silently skip signals that + // the OS refuses to change (e.g. SIGTHR on OpenBSD). + // GNU env also ignores these. + return Ok(()); + } + Err(e) => return Err(e), + } log.record(sig_value, action_kind, explicit); // Set environment variable to communicate to Rust child processes From d564c7bd6bb870e2a13f3fc7e2dea3c6fc279553 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 31 Mar 2026 09:01:15 +0200 Subject: [PATCH 21/21] deps(rustix): enable use-libc to route syscalls through glibc Without use-libc, rustix uses direct inline assembly syscalls, bypassing glibc entirely. On a glibc-based system like Debian, this can break LD_PRELOAD interposition, sanitizers, and miss glibc optimizations like the getpid() cache. --- Cargo.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index cb072dbff82..cf4ac27d07d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -429,7 +429,11 @@ rstest = "0.26.0" rstest_reuse = "0.7.0" rustc-hash = "2.1.1" rust-ini = "0.21.0" -rustix = { version = "1.1.4", default-features = false } +# use-libc: route syscalls through glibc instead of direct inline assembly, +# ensuring compatibility with LD_PRELOAD, sanitizers, and glibc optimizations (e.g. getpid cache) +rustix = { version = "1.1.4", default-features = false, features = [ + "use-libc", +] } same-file = "1.0.6" self_cell = "1.0.4" selinux = "=0.6.0"