diff --git a/README.md b/README.md index 45ce3d7..7e3cebe 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,142 @@ + +
+ # 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) + +
+ +--- + +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. diff --git a/hooks/install.sh b/hooks/install.sh new file mode 100755 index 0000000..8622215 --- /dev/null +++ b/hooks/install.sh @@ -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" diff --git a/hooks/pre-commit b/hooks/pre-commit new file mode 100755 index 0000000..5e6500f --- /dev/null +++ b/hooks/pre-commit @@ -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}" diff --git a/hooks/pre-push b/hooks/pre-push new file mode 100755 index 0000000..6139ea8 --- /dev/null +++ b/hooks/pre-push @@ -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}"