shadow-core, tools: complete nix removal — phase 2 (#140)

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).
This commit is contained in:
Pierre Warnier
2026-04-22 11:26:49 +02:00
parent bc8ea13328
commit 32c94fbc76
21 changed files with 285 additions and 126 deletions
+2 -3
View File
@@ -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]
-1
View File
@@ -16,7 +16,6 @@ path = "src/lib.rs"
[dependencies]
libc = { workspace = true }
nix = { workspace = true }
rustix = { workspace = true }
thiserror = { workspace = true }
zeroize = { workspace = true }
+47 -32
View File
@@ -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<String, crate::error::ShadowError> {
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<String, crate::error::ShadowError> {
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<crate::passwd::PasswdEntry, crate::error::ShadowError> {
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<String, crate::error::ShadowError> {
/// 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<Self, crate::error::ShadowError> {
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);
}
}
+4
View File
@@ -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;
+7 -5
View File
@@ -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");
}
+10 -8
View File
@@ -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<zeroize::Zeroizing<String>> {
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<Self> {
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);
}
}
+149
View File
@@ -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<SavedSigSet> {
// 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())
}
}
-1
View File
@@ -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 }
+1 -1
View File
@@ -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).
-1
View File
@@ -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 }
+1 -1
View File
@@ -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()
-1
View File
@@ -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 }
+1 -1
View File
@@ -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.
-1
View File
@@ -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 }
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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 }
+25 -33
View File
@@ -70,14 +70,11 @@ impl UError for NewgrpError {
/// Get the current user's primary GID from the real UID.
fn get_current_gid() -> Result<u32, NewgrpError> {
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<u32, NewgrpError> {
/// 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<String> {
/// 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<Self, NewgrpError> {
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());
}
+1 -2
View File
@@ -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 }
+33 -31
View File
@@ -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<Self, PasswdError> {
let original_euid = nix::unistd::geteuid();
fn drop_to(uid: u32) -> Result<Self, PasswdError> {
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<String, PasswdError
}
// No user specified — default to current user.
let uid = nix::unistd::getuid();
match nix::unistd::User::from_uid(uid) {
Ok(Some(user)) => 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
);
}
+1 -1
View File
@@ -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");

Some files were not shown because too many files have changed in this diff Show More