Merge pull request #146 from shadow-utils-rs/feat/140-nix-to-rustix-phase1

shadow-core, tools: migrate nix to rustix — phase 1 (#140)
This commit is contained in:
Pierre Warnier
2026-04-22 11:45:49 +02:00
committed by GitHub
37 changed files with 340 additions and 179 deletions
+2 -2
View File
@@ -59,7 +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", "termios"] }
libc = "0.2"
# Security
@@ -205,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 -1
View File
@@ -16,7 +16,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(())
+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;
+15 -10
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)
)
}
@@ -296,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");
@@ -305,9 +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");
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())
}
}
+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 -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"] }
uucore = { workspace = true }
+4 -4
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,8 +536,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).
+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 }
uucore = { workspace = true }
+4 -4
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,8 +160,8 @@ where
F: FnOnce(&mut PasswdEntry) -> Result<(), String>,
{
// Consolidate real + effective UID to root for file operations.
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);
}
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 }
shadow-core = { workspace = true, features = ["shadow", "crypt"] }
uucore = { workspace = true }
zeroize = { workspace = true }
+4 -4
View File
@@ -304,8 +304,8 @@ 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() {
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.
@@ -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 -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 }
uucore = { workspace = true }
+4 -4
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,8 +174,8 @@ fn mutate_passwd<F>(root: &SysRoot, username: &str, mutate: F) -> UResult<()>
where
F: FnOnce(&mut PasswdEntry) -> Result<(), String>,
{
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);
}
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 }
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 }

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