shadow-core, tools: migrate nix to rustix — phase 1 (#140)

Replace straightforward nix API calls with rustix equivalents across
15 files. rustix is already a transitive dependency (via clap) and
provides raw syscall access without libc overhead.

Phase 1 covers simple 1:1 replacements:
- UID/GID: getuid/geteuid/getgid → rustix::process
- File ops: fsync, umask, chownat → rustix::fs
- Process: getpid, kill (stale lock detection) → rustix::process
- Filesystem: chroot, chdir → rustix::process

6 crates fully off nix: groupadd, groupdel, groupmod, useradd,
userdel, usermod.
4 crates partially migrated (nix kept for setuid()): chpasswd, chage,
chfn, chsh.
2 crates cleaned of unused nix/libc deps: pwck, grpck.

Phase 2 (hardening.rs, passwd.rs, newgrp.rs, pam.rs) deferred — these
have complex signal/termios/User::from_uid usage requiring more care.

Tested on debian/alpine/fedora — all pass.
This commit is contained in:
Pierre Warnier
2026-04-22 10:47:03 +02:00
parent 81b54eb930
commit bc8ea13328
27 changed files with 58 additions and 56 deletions
+1
View File
@@ -60,6 +60,7 @@ thiserror = "2"
# Unix/Linux
nix = { version = "0.30", features = ["user", "fs", "process", "signal", "term", "resource"] }
rustix = { version = "1", features = ["process", "fs"] }
libc = "0.2"
# Security
+1
View File
@@ -17,6 +17,7 @@ path = "src/lib.rs"
[dependencies]
libc = { workspace = true }
nix = { workspace = true }
rustix = { workspace = true }
thiserror = { workspace = true }
zeroize = { workspace = true }
subtle = { workspace = true }
+5 -8
View File
@@ -30,16 +30,13 @@ use crate::error::ShadowError;
/// `umask(2)` is a process-wide operation. This guard is NOT safe to use
/// from multiple threads concurrently. All shadow-rs tools are
/// single-threaded, so this is not an issue in practice.
struct UmaskGuard(
nix::sys::stat::Mode,
std::marker::PhantomData<std::rc::Rc<()>>,
);
struct UmaskGuard(rustix::fs::Mode, std::marker::PhantomData<std::rc::Rc<()>>);
impl UmaskGuard {
/// Set umask to zero and return a guard that restores the original.
fn zero() -> Self {
Self(
nix::sys::stat::umask(nix::sys::stat::Mode::empty()),
rustix::process::umask(rustix::fs::Mode::empty()),
std::marker::PhantomData,
)
}
@@ -47,7 +44,7 @@ impl UmaskGuard {
impl Drop for UmaskGuard {
fn drop(&mut self) {
nix::sys::stat::umask(self.0);
rustix::process::umask(self.0);
}
}
@@ -151,7 +148,7 @@ where
tmp_file
.flush()
.map_err(|e| ShadowError::IoPath(e, tmp_path.clone()))?;
nix::unistd::fsync(&tmp_file)
rustix::fs::fsync(&tmp_file)
.map_err(|e| ShadowError::IoPath(io::Error::from(e), tmp_path.clone()))?;
// Atomic rename.
@@ -162,7 +159,7 @@ where
// Fsync the parent directory to ensure the rename is durable.
if let Ok(dir_fd) = File::open(dir) {
let _ = nix::unistd::fsync(&dir_fd);
let _ = rustix::fs::fsync(&dir_fd);
}
Ok(())
+10 -7
View File
@@ -19,8 +19,6 @@ use std::path::{Path, PathBuf};
use std::thread;
use std::time::{Duration, Instant};
use nix::unistd;
use crate::error::ShadowError;
/// Default lock timeout (matches GNU shadow-utils `LOCK_TIMEOUT`).
@@ -187,7 +185,7 @@ fn write_pid_file(tmp_path: &Path) -> Result<(), ShadowError> {
}
};
let pid = unistd::getpid();
let pid = rustix::process::getpid();
write!(file, "{pid}").map_err(|e| {
ShadowError::Lock(format!("cannot write {}: {e}", tmp_path.display()).into())
})?;
@@ -195,7 +193,7 @@ fn write_pid_file(tmp_path: &Path) -> Result<(), ShadowError> {
file.flush().map_err(|e| {
ShadowError::Lock(format!("cannot flush {}: {e}", tmp_path.display()).into())
})?;
nix::unistd::fsync(&file).map_err(|e| {
rustix::fs::fsync(&file).map_err(|e| {
ShadowError::Lock(format!("cannot fsync {}: {e}", tmp_path.display()).into())
})?;
@@ -217,13 +215,16 @@ fn is_stale_lock(lock_path: &Path) -> bool {
return true;
}
let Some(pid) = rustix::process::Pid::from_raw(pid) else {
return true;
};
// Signal 0 checks if the process exists without actually sending a signal.
// Only ESRCH means "no such process". EPERM means the process exists but
// we lack permission to signal it — that is a valid lock holder.
let pid = nix::unistd::Pid::from_raw(pid);
matches!(
nix::sys::signal::kill(pid, None),
Err(nix::errno::Errno::ESRCH)
rustix::process::test_kill_process(pid),
Err(rustix::io::Errno::SRCH)
)
}
@@ -305,6 +306,8 @@ 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");
+3 -3
View File
@@ -91,7 +91,7 @@ mod tests {
#[test]
fn test_copy_files() {
if !nix::unistd::geteuid().is_root() {
if !rustix::process::geteuid().is_root() {
return;
}
@@ -116,7 +116,7 @@ mod tests {
#[test]
fn test_copy_subdirectory() {
if !nix::unistd::geteuid().is_root() {
if !rustix::process::geteuid().is_root() {
return;
}
@@ -135,7 +135,7 @@ mod tests {
#[test]
fn test_copy_symlink() {
if !nix::unistd::geteuid().is_root() {
if !rustix::process::geteuid().is_root() {
return;
}
+1
View File
@@ -21,6 +21,7 @@ path = "src/main.rs"
[dependencies]
clap = { workspace = true }
nix = { workspace = true }
rustix = { workspace = true }
shadow-core = { workspace = true, features = ["shadow"] }
uucore = { workspace = true }
+3 -3
View File
@@ -518,10 +518,10 @@ fn do_chroot(dir: &str) -> Result<(), ChageError> {
}
let path = Path::new(dir);
nix::unistd::chroot(path)
rustix::process::chroot(path)
.map_err(|e| ChageError::UnexpectedFailure(format!("cannot chroot to '{dir}': {e}")))?;
nix::unistd::chdir("/").map_err(|e| {
rustix::process::chdir("/").map_err(|e| {
ChageError::UnexpectedFailure(format!("cannot chdir to / after chroot: {e}"))
})?;
@@ -536,7 +536,7 @@ where
{
// Consolidate real + effective UID to root for file operations.
// Some filesystem configurations check real UID.
if nix::unistd::geteuid().is_root() {
if rustix::process::geteuid().is_root() {
let _ = nix::unistd::setuid(nix::unistd::Uid::from_raw(0));
}
+1
View File
@@ -21,6 +21,7 @@ path = "src/main.rs"
[dependencies]
clap = { workspace = true }
nix = { workspace = true }
rustix = { workspace = true }
shadow-core = { workspace = true }
uucore = { workspace = true }
+3 -3
View File
@@ -140,10 +140,10 @@ fn do_chroot(dir: &str) -> Result<(), ChfnError> {
}
let path = std::path::Path::new(dir);
nix::unistd::chroot(path)
rustix::process::chroot(path)
.map_err(|e| ChfnError::Error(format!("cannot chroot to '{dir}': {e}")))?;
nix::unistd::chdir("/")
rustix::process::chdir("/")
.map_err(|e| ChfnError::Error(format!("cannot chdir to / after chroot: {e}")))?;
Ok(())
@@ -160,7 +160,7 @@ where
F: FnOnce(&mut PasswdEntry) -> Result<(), String>,
{
// Consolidate real + effective UID to root for file operations.
if nix::unistd::geteuid().is_root() {
if rustix::process::geteuid().is_root() {
let _ = nix::unistd::setuid(nix::unistd::Uid::from_raw(0));
}
+1
View File
@@ -21,6 +21,7 @@ path = "src/main.rs"
[dependencies]
clap = { workspace = true }
nix = { workspace = true }
rustix = { workspace = true }
shadow-core = { workspace = true, features = ["shadow", "crypt"] }
uucore = { workspace = true }
zeroize = { workspace = true }
+3 -3
View File
@@ -304,7 +304,7 @@ fn apply_password_changes(
hash_config: Option<&(shadow_core::crypt::CryptMethod, Option<u32>)>,
) -> UResult<()> {
// Consolidate real + effective UID to root for file operations.
if nix::unistd::geteuid().is_root() {
if rustix::process::geteuid().is_root() {
let _ = nix::unistd::setuid(nix::unistd::Uid::from_raw(0));
}
@@ -419,10 +419,10 @@ fn do_chroot(dir: &str) -> Result<(), ChpasswdError> {
}
let path = Path::new(dir);
nix::unistd::chroot(path)
rustix::process::chroot(path)
.map_err(|e| ChpasswdError::UnexpectedFailure(format!("cannot chroot to '{dir}': {e}")))?;
nix::unistd::chdir("/").map_err(|e| {
rustix::process::chdir("/").map_err(|e| {
ChpasswdError::UnexpectedFailure(format!("cannot chdir to / after chroot: {e}"))
})?;
+1
View File
@@ -21,6 +21,7 @@ path = "src/main.rs"
[dependencies]
clap = { workspace = true }
nix = { workspace = true }
rustix = { workspace = true }
shadow-core = { workspace = true }
uucore = { workspace = true }
+3 -3
View File
@@ -80,10 +80,10 @@ fn do_chroot(dir: &str) -> Result<(), ChshError> {
}
let path = std::path::Path::new(dir);
nix::unistd::chroot(path)
rustix::process::chroot(path)
.map_err(|e| ChshError::Error(format!("cannot chroot to '{dir}': {e}")))?;
nix::unistd::chdir("/")
rustix::process::chdir("/")
.map_err(|e| ChshError::Error(format!("cannot chdir to / after chroot: {e}")))?;
Ok(())
@@ -174,7 +174,7 @@ fn mutate_passwd<F>(root: &SysRoot, username: &str, mutate: F) -> UResult<()>
where
F: FnOnce(&mut PasswdEntry) -> Result<(), String>,
{
if nix::unistd::geteuid().is_root() {
if rustix::process::geteuid().is_root() {
let _ = nix::unistd::setuid(nix::unistd::Uid::from_raw(0));
}
+1 -1
View File
@@ -20,7 +20,7 @@ path = "src/main.rs"
[dependencies]
clap = { workspace = true }
nix = { workspace = true }
rustix = { workspace = true }
shadow-core = { workspace = true, features = ["shadow", "group", "gshadow", "login-defs"] }
uucore = { workspace = true }
+1 -1
View File
@@ -456,7 +456,7 @@ mod tests {
}
fn skip_unless_root() -> bool {
!nix::unistd::geteuid().is_root()
!rustix::process::geteuid().is_root()
}
#[test]
+1 -1
View File
@@ -20,7 +20,7 @@ path = "src/main.rs"
[dependencies]
clap = { workspace = true }
nix = { workspace = true }
rustix = { workspace = true }
shadow-core = { workspace = true, features = ["shadow", "group", "gshadow", "login-defs"] }
uucore = { workspace = true }
+1 -1
View File
@@ -252,7 +252,7 @@ mod tests {
}
fn skip_unless_root() -> bool {
!nix::unistd::geteuid().is_root()
!rustix::process::geteuid().is_root()
}
#[test]
+1 -1
View File
@@ -20,7 +20,7 @@ path = "src/main.rs"
[dependencies]
clap = { workspace = true }
nix = { workspace = true }
rustix = { workspace = true }
shadow-core = { workspace = true, features = ["shadow", "group", "gshadow", "login-defs"] }
uucore = { workspace = true }
+1 -1
View File
@@ -329,7 +329,7 @@ mod tests {
}
fn skip_unless_root() -> bool {
!nix::unistd::geteuid().is_root()
!rustix::process::geteuid().is_root()
}
#[test]
-2
View File
@@ -20,8 +20,6 @@ path = "src/main.rs"
[dependencies]
clap = { workspace = true }
libc = { workspace = true }
nix = { workspace = true }
shadow-core = { workspace = true, features = ["shadow", "group", "gshadow", "login-defs"] }
uucore = { workspace = true }

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