From 32c94fbc7603362e7a9127d078f88b5af45c9857 Mon Sep 17 00:00:00 2001 From: Pierre Warnier Date: Wed, 22 Apr 2026 11:26:49 +0200 Subject: [PATCH] =?UTF-8?q?shadow-core,=20tools:=20complete=20nix=20remova?= =?UTF-8?q?l=20=E2=80=94=20phase=202=20(#140)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fully remove nix as a direct dependency of the shadow-rs workspace. New module shadow_core::process provides process-wide POSIX wrappers via libc for setuid/setgid/initgroups/execv/sigprocmask. These MUST use libc (not rustix) because rustix intentionally provides only per-thread versions, which are incorrect for setuid-root tools. Migrations: - hardening.rs: setrlimit → rustix::process, sigprocmask → process module, User::from_uid → /etc/passwd parser lookup - pam.rs: nix::sys::termios → rustix::termios - passwd.rs: full migration (seteuid, setuid, getuid, User lookup) - newgrp.rs: full migration (termios, User lookup, setgid, initgroups, execv) - chpasswd/chage/chfn/chsh: setuid → shadow_core::process - lock.rs tests: fcntl_getfd → rustix::io libc stays for: PAM FFI, crypt(3) FFI, process-wide POSIX wrappers. nix remains only as a transitive dep of uucore (upstream). --- Cargo.toml | 5 +- src/shadow-core/Cargo.toml | 1 - src/shadow-core/src/hardening.rs | 79 +++++++++------- src/shadow-core/src/lib.rs | 4 + src/shadow-core/src/lock.rs | 12 +-- src/shadow-core/src/pam.rs | 18 ++-- src/shadow-core/src/process.rs | 149 +++++++++++++++++++++++++++++++ src/uu/chage/Cargo.toml | 1 - src/uu/chage/src/chage.rs | 2 +- src/uu/chfn/Cargo.toml | 1 - src/uu/chfn/src/chfn.rs | 2 +- src/uu/chpasswd/Cargo.toml | 1 - src/uu/chpasswd/src/chpasswd.rs | 2 +- src/uu/chsh/Cargo.toml | 1 - src/uu/chsh/src/chsh.rs | 2 +- src/uu/newgrp/Cargo.toml | 2 +- src/uu/newgrp/src/newgrp.rs | 58 ++++++------ src/uu/passwd/Cargo.toml | 3 +- src/uu/passwd/src/passwd.rs | 64 ++++++------- tests/by-util/test_useradd.rs | 2 +- tests/common/mod.rs | 2 +- 21 files changed, 285 insertions(+), 126 deletions(-) create mode 100644 src/shadow-core/src/process.rs diff --git a/Cargo.toml b/Cargo.toml index d21c864..125de20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,8 +59,7 @@ uucore = "0.7" thiserror = "2" # Unix/Linux -nix = { version = "0.30", features = ["user", "fs", "process", "signal", "term", "resource"] } -rustix = { version = "1", features = ["process", "fs"] } +rustix = { version = "1", features = ["process", "fs", "termios"] } libc = "0.2" # Security @@ -206,7 +205,7 @@ usermod = { version = "0.0.1", package = "uu_usermod", path = "src/uu/usermod" } pwck = { version = "0.0.1", package = "uu_pwck", path = "src/uu/pwck" } grpck = { version = "0.0.1", package = "uu_grpck", path = "src/uu/grpck" } shadow-core = { path = "src/shadow-core", features = ["shadow"] } -nix = { workspace = true } +rustix = { workspace = true } tempfile = { workspace = true } [profile.release] diff --git a/src/shadow-core/Cargo.toml b/src/shadow-core/Cargo.toml index 690fdcb..8bb8a97 100644 --- a/src/shadow-core/Cargo.toml +++ b/src/shadow-core/Cargo.toml @@ -16,7 +16,6 @@ path = "src/lib.rs" [dependencies] libc = { workspace = true } -nix = { workspace = true } rustix = { workspace = true } thiserror = { workspace = true } zeroize = { workspace = true } diff --git a/src/shadow-core/src/hardening.rs b/src/shadow-core/src/hardening.rs index 2dde351..cb1f33e 100644 --- a/src/shadow-core/src/hardening.rs +++ b/src/shadow-core/src/hardening.rs @@ -14,8 +14,16 @@ /// A core dump from a setuid-root process could expose password hashes /// and plaintext passwords. pub fn suppress_core_dumps() { - let _ = nix::sys::resource::setrlimit(nix::sys::resource::Resource::RLIMIT_CORE, 0, 0); - // PR_SET_DUMPABLE via nix::sys::prctl (no raw unsafe needed). + use rustix::process::{Resource, Rlimit, setrlimit}; + + let _ = setrlimit( + Resource::Core, + Rlimit { + current: Some(0), + maximum: Some(0), + }, + ); + // PR_SET_DUMPABLE via prctl (no raw unsafe needed). // nix doesn't expose prctl directly, so we skip it rather than use unsafe. // RLIMIT_CORE=0 is sufficient to prevent core dumps. } @@ -25,10 +33,14 @@ pub fn suppress_core_dumps() { /// A malicious caller could `ulimit -f 1` before invoking a setuid-root /// tool, causing `/etc/shadow` to be truncated mid-write. pub fn raise_file_size_limit() { - let _ = nix::sys::resource::setrlimit( - nix::sys::resource::Resource::RLIMIT_FSIZE, - nix::sys::resource::RLIM_INFINITY, - nix::sys::resource::RLIM_INFINITY, + use rustix::process::{Resource, Rlimit, setrlimit}; + + let _ = setrlimit( + Resource::Fsize, + Rlimit { + current: None, + maximum: None, + }, ); } @@ -121,19 +133,35 @@ pub fn harden_process() -> Vec<(String, String)> { /// `geteuid()` is 0 for all callers, but the real UID identifies who /// actually invoked the program. pub fn caller_is_root() -> bool { - nix::unistd::getuid().is_root() + rustix::process::getuid().is_root() } /// Return the current user's username from the real UID. pub fn current_username() -> Result { - let uid = nix::unistd::getuid(); - match nix::unistd::User::from_uid(uid) { - Ok(Some(user)) => Ok(user.name), - Ok(None) => Err(crate::error::ShadowError::Other( + let uid = rustix::process::getuid().as_raw(); + lookup_username_by_uid(uid) +} + +/// Look up a username by UID from `/etc/passwd`. +pub fn lookup_username_by_uid(uid: u32) -> Result { + let entries = crate::passwd::read_passwd_file(std::path::Path::new("/etc/passwd"))?; + match entries.iter().find(|e| e.uid == uid) { + Some(entry) => Ok(entry.name.clone()), + None => Err(crate::error::ShadowError::Other( format!("cannot determine username for uid {uid}").into(), )), - Err(e) => Err(crate::error::ShadowError::Other( - format!("cannot determine username: {e}").into(), + } +} + +/// Look up a passwd entry by UID from `/etc/passwd`. +pub fn lookup_passwd_entry_by_uid( + uid: u32, +) -> Result { + let entries = crate::passwd::read_passwd_file(std::path::Path::new("/etc/passwd"))?; + match entries.into_iter().find(|e| e.uid == uid) { + Some(entry) => Ok(entry), + None => Err(crate::error::ShadowError::Other( + format!("no passwd entry for uid {uid}").into(), )), } } @@ -149,35 +177,22 @@ pub fn current_username() -> Result { /// inconsistent state or holding a stale lock. The original signal mask /// is restored when the guard is dropped. pub struct SignalBlocker { - old_mask: nix::sys::signal::SigSet, + saved: crate::process::SavedSigSet, } impl SignalBlocker { /// Block `SIGINT`, `SIGTERM`, `SIGHUP` to prevent partial file writes. pub fn block_critical() -> Result { - use nix::sys::signal::{SigSet, SigmaskHow, Signal}; + let saved = crate::process::block_critical_signals().map_err(|e| { + crate::error::ShadowError::Other(format!("cannot block signals: {e}").into()) + })?; - let mut block_set = SigSet::empty(); - block_set.add(Signal::SIGINT); - block_set.add(Signal::SIGTERM); - block_set.add(Signal::SIGHUP); - - let mut old_mask = SigSet::empty(); - nix::sys::signal::sigprocmask(SigmaskHow::SIG_BLOCK, Some(&block_set), Some(&mut old_mask)) - .map_err(|e| { - crate::error::ShadowError::Other(format!("cannot block signals: {e}").into()) - })?; - - Ok(Self { old_mask }) + Ok(Self { saved }) } } impl Drop for SignalBlocker { fn drop(&mut self) { - let _ = nix::sys::signal::sigprocmask( - nix::sys::signal::SigmaskHow::SIG_SETMASK, - Some(&self.old_mask), - None, - ); + let _ = crate::process::restore_signals(&self.saved); } } diff --git a/src/shadow-core/src/lib.rs b/src/shadow-core/src/lib.rs index 3e0fefd..643fb19 100644 --- a/src/shadow-core/src/lib.rs +++ b/src/shadow-core/src/lib.rs @@ -40,6 +40,10 @@ pub mod crypt; #[cfg(feature = "selinux")] pub mod selinux; +// Process-level POSIX wrappers (setuid, sigprocmask, etc.) — FFI requires unsafe. +#[allow(unsafe_code)] +pub mod process; + pub mod atomic; pub mod audit; pub mod hardening; diff --git a/src/shadow-core/src/lock.rs b/src/shadow-core/src/lock.rs index 984ecd0..76194f9 100644 --- a/src/shadow-core/src/lock.rs +++ b/src/shadow-core/src/lock.rs @@ -297,6 +297,8 @@ mod tests { #[test] fn test_lock_file_has_cloexec() { + use std::os::unix::io::AsFd; + // Rust's stdlib sets O_CLOEXEC by default on Linux. // Verify the lock file FD won't leak to child processes. let dir = tempfile::tempdir().expect("tempdir creation failed"); @@ -306,11 +308,11 @@ mod tests { let lock = FileLock::acquire(&file).expect("failed to acquire lock"); let f = fs::File::open(&lock.lock_path).expect("failed to open lock file"); - // Uses nix for this test since rustix lacks fcntl_getfd. The nix - // dependency remains in shadow-core for hardening.rs and pam.rs. - let flags = - nix::fcntl::fcntl(&f, nix::fcntl::FcntlArg::F_GETFD).expect("fcntl F_GETFD failed"); - assert!(flags & libc::FD_CLOEXEC != 0, "FD should have CLOEXEC set"); + let flags = rustix::io::fcntl_getfd(f.as_fd()).expect("fcntl F_GETFD failed"); + assert!( + flags.contains(rustix::io::FdFlags::CLOEXEC), + "FD should have CLOEXEC set" + ); lock.release().expect("failed to release lock"); } diff --git a/src/shadow-core/src/pam.rs b/src/shadow-core/src/pam.rs index ebcb5e8..899bd04 100644 --- a/src/shadow-core/src/pam.rs +++ b/src/shadow-core/src/pam.rs @@ -372,7 +372,7 @@ fn prompt_for_input( /// /// Opens `/dev/tty` once with read+write mode (not stdin) to ensure we talk /// to the real terminal even if stdin has been redirected. Uses -/// `nix::sys::termios` to disable `ECHO` for password prompts and restores +/// `rustix::termios` to disable `ECHO` for password prompts and restores /// the original settings afterward (including on error, via a drop guard). fn read_from_tty(message: &PamMessage, echo: bool) -> io::Result> { let mut tty = File::options().read(true).write(true).open("/dev/tty")?; @@ -482,26 +482,28 @@ fn free_responses(responses: *mut PamResponse, count: usize) { /// RAII guard that disables terminal echo and restores it on drop. /// -/// Uses `nix::sys::termios` to manipulate the terminal's local flags. The +/// Uses `rustix::termios` to manipulate the terminal's local flags. The /// original settings are saved and restored when the guard is dropped, even /// if the caller returns early or panics. struct EchoGuard { fd: libc::c_int, - original: nix::sys::termios::Termios, + original: rustix::termios::Termios, } impl EchoGuard { /// Disable echo on the given terminal file. fn disable(tty: &File) -> io::Result { - use nix::sys::termios::{self, LocalFlags, SetArg}; + use std::os::unix::io::AsFd; let fd = tty.as_raw_fd(); - let original = termios::tcgetattr(tty).map_err(io::Error::other)?; + let original = rustix::termios::tcgetattr(tty.as_fd()).map_err(io::Error::other)?; let mut noecho = original.clone(); - noecho.local_flags &= !(LocalFlags::ECHO | LocalFlags::ECHONL); + noecho.local_modes &= + !(rustix::termios::LocalModes::ECHO | rustix::termios::LocalModes::ECHONL); - termios::tcsetattr(tty, SetArg::TCSANOW, &noecho).map_err(io::Error::other)?; + rustix::termios::tcsetattr(tty.as_fd(), rustix::termios::OptionalActions::Now, &noecho) + .map_err(io::Error::other)?; Ok(Self { fd, original }) } @@ -516,7 +518,7 @@ impl Drop for EchoGuard { use std::os::unix::io::BorrowedFd; let fd = unsafe { BorrowedFd::borrow_raw(self.fd) }; let _ = - nix::sys::termios::tcsetattr(fd, nix::sys::termios::SetArg::TCSANOW, &self.original); + rustix::termios::tcsetattr(fd, rustix::termios::OptionalActions::Now, &self.original); } } diff --git a/src/shadow-core/src/process.rs b/src/shadow-core/src/process.rs new file mode 100644 index 0000000..840c854 --- /dev/null +++ b/src/shadow-core/src/process.rs @@ -0,0 +1,149 @@ +// This file is part of the shadow-rs package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. +// spell-checker:ignore setuid seteuid setgid initgroups sigprocmask + +//! Process-level POSIX wrappers for setuid-root tools. +//! +//! These functions call libc directly because rustix intentionally does not +//! provide process-wide `setuid`/`setgid`/`sigprocmask` (they require libc +//! coordination for thread safety). The `libc` crate is already a dependency +//! for PAM FFI. +//! +//! This is one of the few modules that permits `unsafe` — all unsafe is +//! confined to well-understood POSIX C library calls. + +use std::ffi::CStr; +use std::io; + +// --------------------------------------------------------------------------- +// UID / GID manipulation (process-wide via libc) +// --------------------------------------------------------------------------- + +/// `setuid(uid)` — set the real and effective user ID of the calling process. +/// +/// This calls the libc `setuid()` which is process-wide (unlike the raw +/// syscall which is per-thread on Linux). +pub fn setuid(uid: u32) -> io::Result<()> { + // SAFETY: setuid is a standard POSIX function. The only precondition + // is that uid is a valid UID value, which u32 always satisfies. + let ret = unsafe { libc::setuid(uid) }; + if ret == 0 { + Ok(()) + } else { + Err(io::Error::last_os_error()) + } +} + +/// `seteuid(uid)` — set the effective user ID of the calling process. +/// +/// This calls the libc `seteuid()` which is process-wide. +pub fn seteuid(uid: u32) -> io::Result<()> { + // SAFETY: seteuid is a standard POSIX function. + let ret = unsafe { libc::seteuid(uid) }; + if ret == 0 { + Ok(()) + } else { + Err(io::Error::last_os_error()) + } +} + +/// `setgid(gid)` — set the real and effective group ID of the calling process. +/// +/// This calls the libc `setgid()` which is process-wide. +pub fn setgid(gid: u32) -> io::Result<()> { + // SAFETY: setgid is a standard POSIX function. + let ret = unsafe { libc::setgid(gid) }; + if ret == 0 { + Ok(()) + } else { + Err(io::Error::last_os_error()) + } +} + +/// `initgroups(user, gid)` — initialize the supplementary group list. +/// +/// Sets the supplementary groups for `user` plus `gid`. +pub fn initgroups(user: &CStr, gid: u32) -> io::Result<()> { + // SAFETY: initgroups is a standard POSIX function. `user` is a valid + // null-terminated CStr. + let ret = unsafe { libc::initgroups(user.as_ptr(), gid) }; + if ret == 0 { + Ok(()) + } else { + Err(io::Error::last_os_error()) + } +} + +// --------------------------------------------------------------------------- +// exec +// --------------------------------------------------------------------------- + +/// `execv(path, argv)` — replace the current process image. +/// +/// On success this function never returns. On failure it returns an error. +pub fn execv(path: &CStr, argv: &[&CStr]) -> io::Error { + // Build a null-terminated array of pointers for execv. + let mut argv_ptrs: Vec<*const libc::c_char> = argv.iter().map(|s| s.as_ptr()).collect(); + argv_ptrs.push(std::ptr::null()); + + // SAFETY: execv is a standard POSIX function. The argv array is + // null-terminated and all CStr pointers are valid. + unsafe { + libc::execv(path.as_ptr(), argv_ptrs.as_ptr()); + } + // execv only returns on error. + io::Error::last_os_error() +} + +// --------------------------------------------------------------------------- +// Signal blocking (process-wide via libc) +// --------------------------------------------------------------------------- + +/// A saved signal mask, used by [`block_signals`] and [`restore_signals`]. +/// +/// Wraps a `libc::sigset_t`. +pub struct SavedSigSet { + set: libc::sigset_t, +} + +/// Block `SIGINT`, `SIGTERM`, `SIGHUP` and return the previous signal mask. +/// +/// Prevents these signals from interrupting a lock-modify-write sequence. +pub fn block_critical_signals() -> io::Result { + // SAFETY: sigemptyset, sigaddset, and sigprocmask are standard POSIX + // functions. We initialize the sigset_t with sigemptyset before use. + unsafe { + let mut block_set: libc::sigset_t = std::mem::zeroed(); + libc::sigemptyset(&raw mut block_set); + libc::sigaddset(&raw mut block_set, libc::SIGINT); + libc::sigaddset(&raw mut block_set, libc::SIGTERM); + libc::sigaddset(&raw mut block_set, libc::SIGHUP); + + let mut old_set: libc::sigset_t = std::mem::zeroed(); + let ret = libc::sigprocmask(libc::SIG_BLOCK, &raw const block_set, &raw mut old_set); + if ret != 0 { + return Err(io::Error::last_os_error()); + } + + Ok(SavedSigSet { set: old_set }) + } +} + +/// Restore a previously saved signal mask. +pub fn restore_signals(saved: &SavedSigSet) -> io::Result<()> { + // SAFETY: sigprocmask with SIG_SETMASK restores a previously captured mask. + let ret = unsafe { + libc::sigprocmask( + libc::SIG_SETMASK, + &raw const saved.set, + std::ptr::null_mut(), + ) + }; + if ret == 0 { + Ok(()) + } else { + Err(io::Error::last_os_error()) + } +} diff --git a/src/uu/chage/Cargo.toml b/src/uu/chage/Cargo.toml index 8b07ae1..2e39749 100644 --- a/src/uu/chage/Cargo.toml +++ b/src/uu/chage/Cargo.toml @@ -20,7 +20,6 @@ path = "src/main.rs" [dependencies] clap = { workspace = true } -nix = { workspace = true } rustix = { workspace = true } shadow-core = { workspace = true, features = ["shadow"] } uucore = { workspace = true } diff --git a/src/uu/chage/src/chage.rs b/src/uu/chage/src/chage.rs index 0ddb722..8594d78 100644 --- a/src/uu/chage/src/chage.rs +++ b/src/uu/chage/src/chage.rs @@ -537,7 +537,7 @@ where // Consolidate real + effective UID to root for file operations. // Some filesystem configurations check real UID. if rustix::process::geteuid().is_root() { - let _ = nix::unistd::setuid(nix::unistd::Uid::from_raw(0)); + let _ = shadow_core::process::setuid(0); } // Block signals for the entire critical section (lock -> write -> unlock). diff --git a/src/uu/chfn/Cargo.toml b/src/uu/chfn/Cargo.toml index 08b9255..2a6736f 100644 --- a/src/uu/chfn/Cargo.toml +++ b/src/uu/chfn/Cargo.toml @@ -20,7 +20,6 @@ path = "src/main.rs" [dependencies] clap = { workspace = true } -nix = { workspace = true } rustix = { workspace = true } shadow-core = { workspace = true } uucore = { workspace = true } diff --git a/src/uu/chfn/src/chfn.rs b/src/uu/chfn/src/chfn.rs index 8512434..ec0876c 100644 --- a/src/uu/chfn/src/chfn.rs +++ b/src/uu/chfn/src/chfn.rs @@ -161,7 +161,7 @@ where { // Consolidate real + effective UID to root for file operations. if rustix::process::geteuid().is_root() { - let _ = nix::unistd::setuid(nix::unistd::Uid::from_raw(0)); + let _ = shadow_core::process::setuid(0); } let _signals = shadow_core::hardening::SignalBlocker::block_critical() diff --git a/src/uu/chpasswd/Cargo.toml b/src/uu/chpasswd/Cargo.toml index a511ed0..cbc1d6a 100644 --- a/src/uu/chpasswd/Cargo.toml +++ b/src/uu/chpasswd/Cargo.toml @@ -20,7 +20,6 @@ path = "src/main.rs" [dependencies] clap = { workspace = true } -nix = { workspace = true } rustix = { workspace = true } shadow-core = { workspace = true, features = ["shadow", "crypt"] } uucore = { workspace = true } diff --git a/src/uu/chpasswd/src/chpasswd.rs b/src/uu/chpasswd/src/chpasswd.rs index aebb25f..38db226 100644 --- a/src/uu/chpasswd/src/chpasswd.rs +++ b/src/uu/chpasswd/src/chpasswd.rs @@ -305,7 +305,7 @@ fn apply_password_changes( ) -> UResult<()> { // Consolidate real + effective UID to root for file operations. if rustix::process::geteuid().is_root() { - let _ = nix::unistd::setuid(nix::unistd::Uid::from_raw(0)); + let _ = shadow_core::process::setuid(0); } // Block signals for the entire critical section. diff --git a/src/uu/chsh/Cargo.toml b/src/uu/chsh/Cargo.toml index 020bc95..c6864c3 100644 --- a/src/uu/chsh/Cargo.toml +++ b/src/uu/chsh/Cargo.toml @@ -20,7 +20,6 @@ path = "src/main.rs" [dependencies] clap = { workspace = true } -nix = { workspace = true } rustix = { workspace = true } shadow-core = { workspace = true } uucore = { workspace = true } diff --git a/src/uu/chsh/src/chsh.rs b/src/uu/chsh/src/chsh.rs index af02636..925655c 100644 --- a/src/uu/chsh/src/chsh.rs +++ b/src/uu/chsh/src/chsh.rs @@ -175,7 +175,7 @@ where F: FnOnce(&mut PasswdEntry) -> Result<(), String>, { if rustix::process::geteuid().is_root() { - let _ = nix::unistd::setuid(nix::unistd::Uid::from_raw(0)); + let _ = shadow_core::process::setuid(0); } let _signals = shadow_core::hardening::SignalBlocker::block_critical() diff --git a/src/uu/newgrp/Cargo.toml b/src/uu/newgrp/Cargo.toml index a6fa58e..a7acb4a 100644 --- a/src/uu/newgrp/Cargo.toml +++ b/src/uu/newgrp/Cargo.toml @@ -20,7 +20,7 @@ path = "src/main.rs" [dependencies] clap = { workspace = true } -nix = { workspace = true } +rustix = { workspace = true } zeroize = { workspace = true } shadow-core = { workspace = true, features = ["group", "gshadow", "crypt"] } uucore = { workspace = true } diff --git a/src/uu/newgrp/src/newgrp.rs b/src/uu/newgrp/src/newgrp.rs index 67a9787..03aea25 100644 --- a/src/uu/newgrp/src/newgrp.rs +++ b/src/uu/newgrp/src/newgrp.rs @@ -70,14 +70,11 @@ impl UError for NewgrpError { /// Get the current user's primary GID from the real UID. fn get_current_gid() -> Result { - let uid = nix::unistd::getuid(); - match nix::unistd::User::from_uid(uid) { - Ok(Some(user)) => Ok(user.gid.as_raw()), - Ok(None) => Err(NewgrpError::Error(format!( - "cannot determine current user for uid {uid}" - ))), + let uid = rustix::process::getuid().as_raw(); + match shadow_core::hardening::lookup_passwd_entry_by_uid(uid) { + Ok(entry) => Ok(entry.gid), Err(e) => Err(NewgrpError::Error(format!( - "cannot determine current user: {e}" + "cannot determine current user for uid {uid}: {e}" ))), } } @@ -87,14 +84,13 @@ fn get_current_gid() -> Result { /// Reads the shell field from `/etc/passwd` for the given UID rather /// than trusting `$SHELL`, which is attacker-controlled in a /// setuid-root context. -fn get_shell(uid: nix::unistd::Uid) -> String { - match nix::unistd::User::from_uid(uid) { - Ok(Some(user)) => { - let shell = user.shell.to_string_lossy().to_string(); - if shell.is_empty() { +fn get_shell(uid: u32) -> String { + match shadow_core::hardening::lookup_passwd_entry_by_uid(uid) { + Ok(entry) => { + if entry.shell.is_empty() { "/bin/sh".to_string() } else { - shell + entry.shell } } _ => "/bin/sh".to_string(), @@ -127,7 +123,7 @@ fn group_has_password(gshadow_path: &Path, group_name: &str) -> Option { /// RAII guard that restores terminal echo on drop. struct EchoGuard { tty: std::fs::File, - old_termios: nix::sys::termios::Termios, + old_termios: rustix::termios::Termios, } impl EchoGuard { @@ -135,14 +131,14 @@ impl EchoGuard { fn disable(tty: std::fs::File) -> Result { use std::os::unix::io::AsFd; - let old_termios = nix::sys::termios::tcgetattr(tty.as_fd()) + let old_termios = rustix::termios::tcgetattr(tty.as_fd()) .map_err(|e| NewgrpError::Error(format!("cannot get terminal attributes: {e}")))?; let mut new_termios = old_termios.clone(); - new_termios.local_flags &= !nix::sys::termios::LocalFlags::ECHO; - nix::sys::termios::tcsetattr( + new_termios.local_modes &= !rustix::termios::LocalModes::ECHO; + rustix::termios::tcsetattr( tty.as_fd(), - nix::sys::termios::SetArg::TCSANOW, + rustix::termios::OptionalActions::Now, &new_termios, ) .map_err(|e| NewgrpError::Error(format!("cannot disable echo: {e}")))?; @@ -154,9 +150,9 @@ impl EchoGuard { impl Drop for EchoGuard { fn drop(&mut self) { use std::os::unix::io::AsFd; - let _ = nix::sys::termios::tcsetattr( + let _ = rustix::termios::tcsetattr( self.tty.as_fd(), - nix::sys::termios::SetArg::TCSANOW, + rustix::termios::OptionalActions::Now, &self.old_termios, ); } @@ -288,21 +284,20 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }; // Set the new GID. - let gid = nix::unistd::Gid::from_raw(target_gid); - nix::unistd::setgid(gid) + shadow_core::process::setgid(target_gid) .map_err(|e| NewgrpError::Error(format!("cannot set group ID to {target_gid}: {e}")))?; // Reset supplementary groups. POSIX requires newgrp to reinitialize // the group list. Without this, the new shell inherits stale groups. let username_cstr = std::ffi::CString::new(username.as_str()) .map_err(|_| NewgrpError::Error("invalid username".into()))?; - nix::unistd::initgroups(&username_cstr, gid) + shadow_core::process::initgroups(&username_cstr, target_gid) .map_err(|e| NewgrpError::Error(format!("cannot initialize groups: {e}")))?; // Drop back to the real UID (in case we are setuid-root). - let real_uid = nix::unistd::getuid(); - if nix::unistd::geteuid() != real_uid { - nix::unistd::setuid(real_uid) + let real_uid = rustix::process::getuid().as_raw(); + if rustix::process::geteuid().as_raw() != real_uid { + shadow_core::process::setuid(real_uid) .map_err(|e| NewgrpError::Error(format!("cannot drop privileges: {e}")))?; } @@ -320,12 +315,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let login_cstr = CString::new(login_name.as_str()) .map_err(|_| NewgrpError::Error("invalid shell name".into()))?; - // SAFETY: execv replaces the current process. The CStrings are valid - // and null-terminated. If execv fails, we return an error. - match nix::unistd::execv(&shell_cstr, &[login_cstr]) { - Ok(infallible) => match infallible {}, - Err(e) => Err(NewgrpError::Error(format!("cannot exec {shell}: {e}")).into()), - } + // execv replaces the current process. If it fails, we return an error. + let err = shadow_core::process::execv(&shell_cstr, &[&login_cstr]); + Err(NewgrpError::Error(format!("cannot exec {shell}: {err}")).into()) } /// Build the clap `Command` for `newgrp`. @@ -461,7 +453,7 @@ mod tests { #[test] fn test_get_shell_default() { // This test is environment-dependent but should at least not panic. - let uid = nix::unistd::getuid(); + let uid = rustix::process::getuid().as_raw(); let shell = get_shell(uid); assert!(!shell.is_empty()); } diff --git a/src/uu/passwd/Cargo.toml b/src/uu/passwd/Cargo.toml index 116c145..43df0aa 100644 --- a/src/uu/passwd/Cargo.toml +++ b/src/uu/passwd/Cargo.toml @@ -20,8 +20,7 @@ path = "src/main.rs" [dependencies] clap = { workspace = true } -libc = { workspace = true } -nix = { workspace = true } +rustix = { workspace = true } shadow-core = { workspace = true, features = ["shadow", "login-defs", "landlock"] } uucore = { workspace = true } diff --git a/src/uu/passwd/src/passwd.rs b/src/uu/passwd/src/passwd.rs index 9f14060..b14bcff 100644 --- a/src/uu/passwd/src/passwd.rs +++ b/src/uu/passwd/src/passwd.rs @@ -479,16 +479,16 @@ fn cmd_status(root: &SysRoot, target_user: Option<&str>) -> UResult<()> { /// actual caller, not root. The destructor re-elevates. #[cfg_attr(not(feature = "pam"), allow(dead_code))] struct PrivDrop { - original_euid: nix::unistd::Uid, + original_euid: u32, } impl PrivDrop { /// Drop effective UID to the given UID. #[cfg_attr(not(feature = "pam"), allow(dead_code))] - fn drop_to(uid: nix::unistd::Uid) -> Result { - let original_euid = nix::unistd::geteuid(); + fn drop_to(uid: u32) -> Result { + let original_euid = rustix::process::geteuid().as_raw(); if original_euid != uid { - nix::unistd::seteuid(uid).map_err(|e| { + shadow_core::process::seteuid(uid).map_err(|e| { PasswdError::UnexpectedFailure(format!("cannot drop privileges: {e}")) })?; } @@ -498,7 +498,7 @@ impl PrivDrop { impl Drop for PrivDrop { fn drop(&mut self) { - if let Err(e) = nix::unistd::seteuid(self.original_euid) { + if let Err(e) = shadow_core::process::seteuid(self.original_euid) { // Failing to restore privileges is a critical error — log it loudly. // We can't return an error from Drop, so at least make it visible. let _ = writeln!( @@ -543,7 +543,7 @@ fn cmd_pam_change(matches: &clap::ArgMatches, _target_user: &str) -> UResult<()> // Drop privileges to caller's real UID during PAM conversation. // Re-elevate automatically when _priv_drop goes out of scope. - let _priv_drop = PrivDrop::drop_to(nix::unistd::getuid())?; + let _priv_drop = PrivDrop::drop_to(rustix::process::getuid().as_raw())?; // Non-root users changing their own password must authenticate first. if !shadow_core::hardening::caller_is_root() { @@ -571,7 +571,7 @@ fn cmd_pam_change(matches: &clap::ArgMatches, _target_user: &str) -> UResult<()> audit::log_user_event( "CHNG_PASSWD", _target_user, - nix::unistd::getuid().as_raw(), + rustix::process::getuid().as_raw(), true, ); @@ -598,16 +598,8 @@ fn resolve_target_user(matches: &clap::ArgMatches) -> Result Ok(user.name), - Ok(None) => Err(PasswdError::UnexpectedFailure(format!( - "cannot determine current username for uid {uid}" - ))), - Err(e) => Err(PasswdError::UnexpectedFailure(format!( - "cannot determine current username: {e}" - ))), - } + shadow_core::hardening::current_username() + .map_err(|e| PasswdError::UnexpectedFailure(e.to_string())) } /// Perform `chroot(2)` into the specified directory. @@ -622,10 +614,10 @@ fn do_chroot(dir: &str) -> Result<(), PasswdError> { } let path = std::path::Path::new(dir); - nix::unistd::chroot(path) + rustix::process::chroot(path) .map_err(|e| PasswdError::UnexpectedFailure(format!("cannot chroot to '{dir}': {e}")))?; - nix::unistd::chdir("/").map_err(|e| { + rustix::process::chdir("/").map_err(|e| { PasswdError::UnexpectedFailure(format!("cannot chdir to / after chroot: {e}")) })?; @@ -693,8 +685,8 @@ where { // Consolidate real + effective UID to root for file operations. // Some filesystem configurations check real UID. - if nix::unistd::geteuid().is_root() { - let _ = nix::unistd::setuid(nix::unistd::Uid::from_raw(0)); + if rustix::process::geteuid().is_root() { + let _ = shadow_core::process::setuid(0); } // Block signals for the entire critical section (lock → write → unlock). @@ -763,7 +755,7 @@ where audit::log_user_event( "CHNG_PASSWD", username, - nix::unistd::getuid().as_raw(), + rustix::process::getuid().as_raw(), true, ); @@ -1045,7 +1037,7 @@ mod tests { /// and cross-user status tests now require euid 0. In CI these run inside /// a Docker container as root. fn skip_unless_root() -> bool { - !nix::unistd::geteuid().is_root() + !rustix::process::geteuid().is_root() } /// Helper to create a temp dir with an etc/shadow file. @@ -1431,23 +1423,33 @@ mod tests { #[test] fn test_core_dump_suppression() { + use rustix::process::{Resource, getrlimit}; // After calling suppress_core_dumps(), RLIMIT_CORE should be 0. shadow_core::hardening::suppress_core_dumps(); - let (soft, _hard) = - nix::sys::resource::getrlimit(nix::sys::resource::Resource::RLIMIT_CORE).unwrap(); - assert_eq!(soft, 0, "RLIMIT_CORE should be 0 after suppression"); + let rlim = getrlimit(Resource::Core); + assert_eq!( + rlim.current, + Some(0), + "RLIMIT_CORE should be 0 after suppression" + ); } #[test] fn test_raise_file_size_limit() { + use rustix::process::{Resource, getrlimit}; shadow_core::hardening::raise_file_size_limit(); - let (soft, _hard) = - nix::sys::resource::getrlimit(nix::sys::resource::Resource::RLIMIT_FSIZE).unwrap(); + let rlim = getrlimit(Resource::Fsize); // In environments where the hard limit is already restricted (containers, - // CI), we may not reach RLIM_INFINITY. Verify it's at least very large. + // CI), we may not reach RLIM_INFINITY. `None` means unlimited. + // Verify it's at least very large or unlimited. + let is_large = match rlim.current { + None => true, + Some(v) => v >= 1024 * 1024 * 1024, + }; assert!( - soft >= 1024 * 1024 * 1024 || soft == nix::sys::resource::RLIM_INFINITY, - "RLIMIT_FSIZE should be raised (got {soft})" + is_large, + "RLIMIT_FSIZE should be raised (got {:?})", + rlim.current ); } diff --git a/tests/by-util/test_useradd.rs b/tests/by-util/test_useradd.rs index d24a1e4..9b11aef 100644 --- a/tests/by-util/test_useradd.rs +++ b/tests/by-util/test_useradd.rs @@ -110,7 +110,7 @@ fn test_defaults_flag() { // but we only care that clap parses it without error. If not root, we expect // exit 1 (permission denied). let code = run(&["useradd", "-D"]); - if nix::unistd::getuid().is_root() { + if rustix::process::getuid().is_root() { assert_eq!(code, 0, "-D should exit 0 when root"); } else { assert_eq!(code, 1, "-D should exit 1 when not root"); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index eabdfc9..54220ab 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -11,5 +11,5 @@ /// /// Returns `true` if the test should be skipped. pub fn skip_unless_root() -> bool { - !nix::unistd::geteuid().is_root() + !rustix::process::geteuid().is_root() }