* cli: identify shadow-rs as part of uutils in --help and --version Per #161, every tool now reports `(uutils shadow-rs) <ver>` in its --version output and links to the project in --help via a shared shadow_core::cli module. While auditing the same uu_app() blocks for residual similarity to GNU shadow-utils man-page phrasing, the medium-confidence findings from a clean-room similarity check were also rewritten: 118 clap .help() and .about() strings across all 14 tools now use phrasing derived from the in-tree behavior of each flag, not from upstream documentation. Output strings that scripts grep for (chage -l column headings, pwck/grpck diagnostic lines) were left intentionally untouched, since drop-in compatibility is part of the project's contract. - shadow-core: new cli module with VERSION and AFTER_HELP constants - 14 tools: wire .version() and .after_help() in every uu_app() - 14 tools: rewrite 118 flag-help and .about() strings - shadow-rs multicall: handle --help/--version/-h/-V with project ID - fmt + clippy + tests pass on debian, alpine, fedora (574 passed, 0 failed) Closes #161 * docs: record clean-room audit results (2026-05-04) Adds an evidence document for the clean-room similarity audit that prompted the flag-help rewrite in the previous commit. Headline: 0 verbatim matches in 486 user-facing strings. The audit protocol is reproducible and described in the doc, including the schema-constrained output and post-run command-log scan that ensure no upstream string crosses back into the workspace. * cli: correct help/version text flagged in review - multicall --version/--help now use the shared (uutils shadow-rs) identifier instead of (uutils), matching the tool crates; clarify the symlink invocation line in the help banner. - --root help for the SysRoot tools (userdel, useradd, usermod, groupadd, groupdel, groupmod, pwck, grpck) no longer claims a chroot(2): these only resolve system files under a path prefix. Reword to match, and rename the metavar CHROOT_DIR -> ROOT_DIR. - userdel --force: documented as accepted-for-compatibility (no effect) rather than promising behavior the code does not implement. - groupadd -K: scope the help to the GID-range keys actually honored. - passwd --repository: note only the local files backend is supported. - passwd --stdin: reads password input generally (auth may consume the current password too), not just the new password. - chpasswd -m: state that MD5 is rejected, not supported. - usermod --append: restore the note that -a is only effective with -G. - clean-room audit doc: cite CONTRIBUTING.md (the in-tree policy) instead of the gitignored CLAUDE.md.
shadow-rs is a memory-safe reimplementation of the Linux
shadow-utils in
Rust. shadow-utils (useradd, passwd,
groupadd, etc.) is the suite of setuid-root tools that manages user accounts,
passwords, and groups on every Linux system.
Why
shadow-utils runs as root or setuid-root on every Linux system. It parses
user-supplied input, writes to /etc/passwd, /etc/shadow, /etc/group, and
has had recent CVEs (CVE-2023-4641: password leak in memory, CVE-2024-56433:
subuid collision enabling account takeover). Until shadow-rs, there was no
Rust reimplementation — not in uutils, not in Prossimo/Trifecta, not on
crates.io.
sudo-rs proved the model: an independent Rust rewrite of a privilege-boundary tool can go from zero to default-in-Ubuntu in under 3 years. shadow-rs follows that playbook.
Goals
- Drop-in replacement: same flags, same exit codes, same output format as GNU shadow-utils. Differences are treated as bugs.
- uutils compatible: built on
uucorewith the standarduumain()/uu_app()API contract. Designed to merge into the uutils ecosystem. - Memory safe: eliminate entire classes of vulnerabilities (buffer overflows,
use-after-free, uninitialized memory) that affect the C original. Passwords
zeroed in memory via
zeroize. - Well-tested: unit tests, property-based tests (
proptest), integration tests, fuzz targets for all parsers. Tested on Debian, Alpine (musl), and Fedora (SELinux). - Hardened: Landlock filesystem sandboxing, signal blocking during critical sections, core dump suppression, environment sanitization, privilege drop during PAM.
- Auditable: small dependency tree,
cargo-denylicense and advisory checks, no GPL dependencies.
Status
| Tool | Status |
|---|---|
passwd |
All 16 flags implemented. Drop-in for GNU passwd. PAM password change, Landlock sandboxing, --root, --quiet, --stdin. Output bit-for-bit identical with GNU. |
pwck |
All checks implemented. Drop-in for GNU pwck. Bit-for-bit identical output. |
useradd |
Implemented. UID/GID allocation, home dir + skel, shadow entry, group creation. |
userdel |
Implemented. Remove from all system files, optional home/mail cleanup. |
usermod |
Implemented. Modify all properties, group membership, lock/unlock, set pre-hashed password. |
chpasswd |
Implemented. Batch password change from stdin. |
chage |
Implemented. Password aging management, -l list mode. |
groupadd |
Implemented. Auto GID allocation, system groups, force mode. |
groupdel |
Implemented. Primary group usage check. |
groupmod |
Implemented. GID change, rename, password. |
grpck |
Implemented. Group/gshadow integrity verification. |
chfn |
Implemented. GECOS sub-field modification. |
chsh |
Implemented. Shell change with /etc/shells validation. |
newgrp |
Implemented. Effective group change with crypt verification. |
Building
Requirements
- Rust (stable toolchain)
- Linux (PAM headers, SELinux headers optional)
- Docker + Docker Compose (for testing)
Build
git clone https://github.com/uutils/shadow-rs
cd shadow-rs
docker compose build debian
docker compose run --rm debian cargo build --release
Install
Default install: 14 standalone per-tool binaries with least-privilege setuid
layout matching GNU shadow-utils. Only passwd, chfn, chsh, newgrp are
installed setuid-root; the other 10 are plain 0755.
sudo make install PREFIX=/usr/local
Alternative: single multicall binary with symlinks. Smaller footprint (~14×
disk savings) but larger setuid attack surface — the binary is installed
setuid-root, so all 14 applets run with euid=root when invoked via symlink.
Intended for container/embedded use cases.
sudo make install-multicall PREFIX=/usr/local
Test
All builds and tests run inside Docker containers to isolate from the host system. Three distros are tested to catch libc and PAM differences:
docker compose run --rm debian cargo test --workspace # Debian Trixie (glibc)
docker compose run --rm alpine cargo test --workspace # Alpine (musl libc)
docker compose run --rm fedora cargo test --workspace # Fedora (SELinux enforcing)
Lint
docker compose run --rm debian cargo clippy --workspace --all-targets -- -D warnings
docker compose run --rm debian cargo fmt --all --check
Architecture
Cargo workspace monorepo built on uucore:
src/bin/shadow-rs.rs multicall binary (dispatches by argv[0])
|
src/uu/{tool}/ individual tool crates (passwd, useradd, ...)
|
┌────┴────┐
uucore shadow-core shared infrastructure + domain library
Tools use uucore for the standard uutils API (UResult, #[uucore::main],
show_error!) and shadow-core for domain-specific functionality.
shadow-core provides:
- File parsers for
/etc/passwd,/etc/shadow,/etc/group,/etc/gshadow,/etc/login.defs,/etc/subuid,/etc/subgid - Atomic file writes (lock, write tmp, fsync, rename, unlock, invalidate nscd)
- PAM integration (feature-gated)
- Username/groupname validation
- UID/GID allocation
- SELinux context handling (feature-gated)
Each tool crate exports uumain() and uu_app(), following
uutils conventions exactly so a future
merge is frictionless.
Docker Test Matrix
| Target | Base | libc | PAM | SELinux |
|---|---|---|---|---|
debian |
rust:latest (Trixie) |
glibc | Linux-PAM | headers |
alpine |
rust:alpine |
musl | Linux-PAM | none |
fedora |
fedora:latest |
glibc | Linux-PAM | enforcing |
Credits
Security patterns from OpenBSD
(ISC license). PAM integration patterns from
sudo-rs (Apache-2.0/MIT).
uutils infrastructure via uucore (MIT).
Code written by Claude Code (Anthropic), reviewed by GitHub Copilot and Google Gemini CLI.
Contributing
See CONTRIBUTING.md for guidelines.
Important: shadow-rs is developed under a strict GPL clean-room policy. Do not read, reference, or feed into an LLM any code from shadow-maint/shadow (GPL-2.0+). Reference only: POSIX specs, man pages, BSD-licensed implementations (FreeBSD, OpenBSD, musl), and sudo-rs.
License
shadow-rs is licensed under the MIT License.
GNU shadow-utils is licensed under the GPL 2.0 or later.