README: rewrite following uutils format, add local Docker git hooks

README.md: project overview with CVE motivation, goals, tool status
table, Docker build/test/lint instructions, architecture diagram,
test matrix, contributing guidelines with GPL clean-room warning.

hooks/: local CI via Docker (no GitHub Actions, no cloud):
- pre-commit: cargo fmt + clippy on Debian (~5s)
- pre-push: fmt + clippy + tests on Debian/Alpine/Fedora (~30s)
- install.sh: symlinks hooks into .git/hooks/
This commit is contained in:
Pierre Warnier
2026-03-23 13:13:36 +01:00
parent 7cc7c463cb
commit 202acc16c9
4 changed files with 234 additions and 1 deletions
+141 -1
View File
@@ -1,2 +1,142 @@
<!-- spell-checker:ignore reimplementation setuid nscd subuid subgid gshadow -->
<div align="center">
# shadow-rs
Memory-safe Rust reimplementation of Linux shadow-utils (useradd, passwd, groupadd, etc.)
[![License](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/shadow-utils-rs/shadow-rs/blob/main/LICENSE)
</div>
---
shadow-rs is a memory-safe reimplementation of the Linux
[shadow-utils](https://github.com/shadow-maint/shadow) in
[Rust](http://www.rust-lang.org). 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). There is **no Rust
reimplementation** — not in uutils, not in Prossimo/Trifecta, not on crates.io.
[sudo-rs](https://github.com/trifectatechfoundation/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.
- **Memory safe**: eliminate entire classes of vulnerabilities (buffer overflows,
use-after-free, uninitialized memory) that affect the C original.
- **Well-tested**: unit tests, property-based tests, integration tests in
isolated namespaces, fuzz targets for all parsers.
- **Auditable**: small dependency tree, `cargo-deny` license and advisory
checks, no GPL dependencies.
## Status
| Tool | Status |
|------|--------|
| `passwd` | `-S`, `-l`, `-u`, `-d`, `-e`, `-n`, `-x`, `-w`, `-i`, `-P`, `-a` implemented. PAM password change in progress. |
| `pwck` | Planned (Phase 1) |
| `useradd` | Planned (Phase 2) |
| `userdel` | Planned (Phase 2) |
| `usermod` | Planned (Phase 2) |
| `chpasswd` | Planned (Phase 2) |
| `chage` | Planned (Phase 2) |
| `groupadd` | Planned (Phase 3) |
| `groupdel` | Planned (Phase 3) |
| `groupmod` | Planned (Phase 3) |
| `grpck` | Planned (Phase 3) |
| `chfn` | Planned (Phase 3) |
| `chsh` | Planned (Phase 3) |
| `newgrp` | Planned (Phase 3) |
## Building
### Requirements
- Rust (stable toolchain)
- Linux (PAM headers, SELinux headers optional)
- Docker + Docker Compose (for testing)
### Build
```shell
git clone https://github.com/shadow-utils-rs/shadow-rs
cd shadow-rs
docker compose build debian
docker compose run --rm debian cargo build --release
```
### 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:
```shell
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
```shell
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 with three layers:
```
src/bin/shadow-rs.rs multicall binary (dispatches by argv[0])
|
src/uu/{tool}/ individual tool crates (passwd, useradd, ...)
|
src/shadow-core/ shared library (parsers, atomic writes, locking, PAM)
```
**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](https://github.com/uutils/coreutils) 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 |
## Contributing
See [CONTRIBUTING.md](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](https://github.com/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](LICENSE).
GNU shadow-utils is licensed under the GPL 2.0 or later.
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Install shadow-rs git hooks.
#
# Usage: ./hooks/install.sh
#
# This symlinks the hooks into .git/hooks/ so they run automatically
# on commit and push. No GitHub Actions, no cloud — everything local.
set -euo pipefail
REPO_ROOT="$(git rev-parse --show-toplevel)"
HOOKS_DIR="$REPO_ROOT/hooks"
GIT_HOOKS_DIR="$REPO_ROOT/.git/hooks"
for hook in pre-commit pre-push; do
src="$HOOKS_DIR/$hook"
dst="$GIT_HOOKS_DIR/$hook"
if [ -f "$src" ]; then
chmod +x "$src"
ln -sf "$src" "$dst"
echo "installed: $hook -> $dst"
fi
done
echo ""
echo "Git hooks installed. They run in Docker — make sure images are built:"
echo " docker compose build"
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# shadow-rs pre-commit hook — runs fmt check and clippy in Docker.
# Install: ./hooks/install.sh
#
# Fast checks only (no full test suite — that's pre-push).
# Runs in ~5s on a warm Docker cache.
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
echo "pre-commit: checking formatting..."
if ! docker compose run --rm -T debian cargo fmt --all --check 2>&1; then
echo -e "${RED}FAILED${NC}: cargo fmt --all --check"
echo "Run: docker compose run --rm debian cargo fmt --all"
exit 1
fi
echo "pre-commit: running clippy..."
if ! docker compose run --rm -T debian cargo clippy --workspace --all-targets -- -D warnings 2>&1; then
echo -e "${RED}FAILED${NC}: cargo clippy"
exit 1
fi
echo -e "${GREEN}pre-commit: all checks passed${NC}"
Executable
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# shadow-rs pre-push hook — runs full test suite on all three distros.
# Install: ./hooks/install.sh
#
# Runs fmt, clippy, and tests on Debian, Alpine, and Fedora.
# Takes ~30s on a warm Docker cache.
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
run_check() {
local distro=$1
shift
echo -e "${YELLOW}[$distro]${NC} $*"
if ! docker compose run --rm -T "$distro" "$@" 2>&1; then
echo -e "${RED}FAILED${NC} on $distro: $*"
exit 1
fi
}
echo "pre-push: full CI checks across all distros"
echo "==========================================="
# Format + clippy (Debian only — same Rust version everywhere)
run_check debian cargo fmt --all --check
run_check debian cargo clippy --workspace --all-targets -- -D warnings
# Tests on all three distros
run_check debian cargo test --workspace
run_check alpine cargo test --workspace
run_check fedora cargo test --workspace
echo ""
echo -e "${GREEN}pre-push: all checks passed on debian/alpine/fedora${NC}"