mirror of
https://github.com/uutils/shadow.git
synced 2026-06-10 16:14:57 -07:00
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:
@@ -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.)
|
||||
|
||||
[](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.
|
||||
|
||||
Executable
+28
@@ -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"
|
||||
Executable
+27
@@ -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
@@ -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}"
|
||||
Reference in New Issue
Block a user