Switch the default install to build and install 14 standalone per-tool
binaries with a least-privilege setuid layout matching GNU shadow-utils.
Only passwd/chfn/chsh/newgrp are setuid-root; the other 10 tools are 0755.
The previous multicall install is preserved as `make install-multicall`
for container/embedded use cases where the ~14x disk savings matter and
the enlarged setuid attack surface is acceptable.
The old layout chmod'd symlinks pointing to the multicall binary, which
(because chmod follows symlinks) marked the underlying ELF setuid-root.
That meant all 14 tools ran with euid=0 when invoked via symlink. Each
tool's internal getuid() check was defense-in-depth, not OS-level least
privilege. The standalone layout gives the latter.
Raised by @oech3 in uutils/coreutils#11828.
- userdel: scope SignalBlocker to file mutations only, drop before
home directory removal so long-running deletions stay interruptible
- usermod: drop SignalBlocker before recursive_chown so large home
trees remain interruptible
- useradd: drop SignalBlocker after lock release, before subid/home
creation steps that may be long-running
- atomic: use PhantomData<Rc<()>> instead of PhantomData<*const ()>
for clearer !Send/!Sync opt-out on UmaskGuard
- newgrp: replace harden_process() with suppress_core_dumps() +
sanitized_env() to avoid leaking RLIMIT_FSIZE into exec'd shell
Security fixes:
- newgrp: add initgroups() before execv to reset supplementary groups (H-3)
- pam: use Zeroizing<String> for password buffers on all exit paths (M-5)
- atomic: retry-once on stale tmp file from crashed run (M-3)
- atomic: UmaskGuard opts out of Send/Sync via PhantomData (L-2)
- lock: flush + fsync in write_pid_file before hard_link (L-7)
Deduplication (~300 lines removed):
- Centralize caller_is_root(), current_username(), SignalBlocker in
shadow_core::hardening — remove 10+5+5 private copies across tools
- Add SignalBlocker to all 6 remaining mutating tools (M-4)
- Remove dead CantSort variant from grpck, fix pwck allow (M-8)
- Remove dead read_passwd from test_chfn (M-8)
Binary cleanup:
- shadow-rs multicall: use ExitCode instead of process::exit (M-6)
- completions: use ExitCode + Result pattern, remove unwrap/exit (H-3)
Documentation:
- crypt: thread-safety warning on public API (H-1)
- shadow: clarify unlock() behavior for * password (H-2)
- validate: document $ rejection deviation from GNU (L-4)
- selinux/audit: note shell-out implementation status (L-6)
- chpasswd/chage tests: TODO for --prefix integration (M-8)
- userdel: document inline skip_unless_root reason (L-9)
Testing:
- Add fuzz targets for group and gshadow parsers (M-7)
- skel: use create_dir for tighter error detection (L-8)
- crypt: compile-time modulo bias assertion (L-1)
crypt(3) returns "*0" or "*1" when the requested method is unsupported
(e.g. yescrypt on musl libc). Previously this was returned as a valid
hash, causing assertion failures in tests on Alpine.
Now hash_password() checks for the "*" error prefix and returns
ShadowError::Auth, allowing callers to handle gracefully.
Most feedback was already addressed in prior commits. The one remaining
fix: generate_salt() now rejects rounds for yescrypt with an error
instead of silently ignoring them.
Already addressed:
- ShadowError::Parse → Other for clock errors
- Explicit sha_rounds validation (range check, error on invalid)
- Reject -m (MD5) unconditionally, not silently ignored
- getrandom(2) syscall instead of /dev/urandom (chroot-safe)
- Strip rounds for yescrypt at caller level
- Round-trip tests for hash_password + verify_password
Replace 13 local copies across all test files with a shared import
from tests/common/mod.rs. Fix getuid/geteuid inconsistency in
test_useradd.rs and test_userdel.rs (effective UID is correct for
setuid-root tools).
Fixes#133
Add hash_password() and CryptMethod to shadow-core::crypt alongside
the existing verify_password(). Supports SHA-256, SHA-512, and yescrypt;
rejects MD5 and DES as insecure.
chpasswd now accepts plaintext mode (the default) and hashes passwords
via crypt(3) before writing. Previously only -e (pre-encrypted) worked.
Fixes#128
The function returned 0 on clock error, silently corrupting shadow
entries with sp_lstchg=0 (which means "force password change").
Now returns Result<i64, ShadowError> so callers handle the error.
Deduplicate local copies in useradd and chpasswd — both now delegate
to the shadow-core version.
Fixes#131
- sysroot: add try_resolve() that returns Option for user-controlled
paths; resolve() uses unreachable! since only called with hardcoded
paths like "/etc/passwd"
- useradd: clamp subid start to at least 100_000 even when existing
entries are below that threshold
- grpck, pwck, userdel, usermod: use `let _ =` for harden_process()
since these tools don't spawn child processes
CRITICAL fixes:
- crypt: constant-time hash comparison via `subtle::ConstantTimeEq`
to prevent timing side-channel attacks (#119)
- useradd: hold all locks (passwd, shadow, group) during entire
mutation window to prevent TOCTOU races (#120)
HIGH fixes:
- newgrp: read shell from passwd entry instead of tainted $SHELL (#121)
- sysroot: reject path traversal (.. components) in --root/--prefix (#122)
- userdel, usermod: replace .expect() with proper UResult error
handling — no panics in setuid-root code (#123)
- userdel: canonicalize home path before recursive delete (#124)
- newgrp: zeroize password memory on drop via Zeroizing<String> (#125)
- grpck, pwck, userdel, usermod: add harden_process() call (#127)
MEDIUM fix:
- useradd: find next available subid range instead of fixed formula,
prevents overlapping container ID namespaces (#129)
- README: fix passwd flag count (17→16), add Landlock mention, add
Hardened goal, update usermod description with -p, fix past tense
- CONTRIBUTING: fix unsafe description to match actual workspace policy
- CHANGELOG: add [Unreleased] section for post-v0.1.0 changes
- OPENBSD-REFERENCE: move all implemented items to correct section
- SECURITY-HARDENING: mark all 21 items as implemented
- man/usermod: add -p/--password flag documentation
- Add homepage.workspace to root package (missing vs subcrates)
- Gate Docker test-docker job to push only (seccomp:unconfined
and SYS_ADMIN are unsafe for untrusted PR contexts)
- Add Docker test matrix (debian/alpine/fedora) to GitHub Actions CI
- Add renovate.json for automated dependency updates
- Add rust-toolchain.toml for contributor convenience