diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..73c2487 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,318 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +**shadow-rs** is a memory-safe Rust reimplementation of Linux shadow-utils (`useradd`, `passwd`, `groupadd`, etc.) — the setuid-root tools that manage user accounts, passwords, and groups on every Linux system. Designed as a drop-in for the [uutils](https://github.com/uutils) ecosystem using `uucore`. + +Status: **Active development** — `passwd` is feature-complete (all 17 flags). Full strategy and phasing in `PLAN-shadow-rs.md`. + +## Key Documents + +- `PLAN-shadow-rs.md` — project strategy, phasing, outreach timeline, risk matrix +- `CONTRIBUTING.md` — contributor guide, clean-room policy, PR process +- `SECURITY.md` — vulnerability reporting +- `deny.toml` — cargo-deny license allowlist and advisory config +- `docs/OPENBSD-REFERENCE.md` — security patterns from OpenBSD's passwd (ISC license) +- `docs/SECURITY-HARDENING.md` — hardening roadmap (signals, privdrop, landlock, env sanitization) + +## Credits & Safe Reference Sources + +This project is developed under a strict GPL clean-room policy. All implementation +work references ONLY these permissively licensed sources: + +| Source | License | What we use it for | +|--------|---------|-------------------| +| [POSIX specification](https://pubs.opengroup.org/onlinepubs/9799919799/) | Open standard | Behavioral spec for each command | +| Man pages (man7.org) | Documentation | Command options, file formats, semantics | +| [FreeBSD src](https://cgit.freebsd.org/src/) | BSD-2-Clause | Reference implementation patterns | +| [OpenBSD src](https://cvsweb.openbsd.org/src/) | ISC | Security hardening patterns (pledge, unveil, privdrop) | +| [musl libc](https://musl.libc.org/) | MIT | pwd/grp/shadow C API understanding | +| [sudo-rs](https://github.com/trifectatechfoundation/sudo-rs) | Apache-2.0 / MIT | PAM integration patterns, privilege-dropping | +| [uutils/coreutils](https://github.com/uutils/coreutils) | MIT | `uucore` infrastructure, project conventions | + +**Review credits**: Code is reviewed by GitHub Copilot (automated PR review) and +Google Gemini CLI (manual security audits). Findings from both reviewers have +directly shaped the security posture of this project: + +- Gemini found: TOCTOU in locking, temp file permissions, PATH injection, + EPERM vs ESRCH in stale detection, GECOS whitespace corruption +- Copilot found: setuid-root privilege bypass (getuid vs geteuid), + trim_start vs trim for parser correctness, PAM error handling + +**Never reference**: `github.com/shadow-maint/shadow` C source (GPL-2.0+) + +## Critical Rules + +### GPL Clean-Room — ABSOLUTE + +**NEVER read, reference, copy from, or feed into an LLM any code from `github.com/shadow-maint/shadow`** (GPL-2.0+). This includes paraphrasing or translating their logic. Violation creates a derivative work and poisons the MIT-licensed project. + +Safe reference sources ONLY: +- POSIX specifications (pubs.opengroup.org) +- Man pages (man7.org) — behavioral spec, not implementation +- FreeBSD src (BSD-2-Clause) +- OpenBSD src (ISC) +- musl libc (MIT) +- sudo-rs (Apache-2.0 / MIT) — for PAM patterns and privilege-dropping + +When implementing a tool: read the POSIX spec and man page for behavioral requirements, then write an original implementation. Never search for or view the C source. + +### Rust Style Rules (uutils conventions) + +- **No `panic!`** — never use `.unwrap()` or `panic!`. Use `unreachable!` only with a justifying comment. +- **No `std::process::exit`** — utilities must be embeddable. Return `UResult<()>` from `uumain`. +- **No `unsafe`** — enforced by `unsafe_code = "deny"` in workspace lints. Only three FFI boundary modules have `#[allow(unsafe_code)]`: `shadow-core::pam` (PAM C library), `shadow-core::crypt` (POSIX crypt(3)), and `shadow-core::process` (setuid/sigprocmask/getpwuid_r). All other code must be 100% safe Rust. This is stricter than uutils (which allows unsafe for FFI but doesn't enforce at the cargo level). When edition 2024 makes something unsafe (like `set_var`), find a safe alternative instead of adding `unsafe {}`. +- **No dead code** — enforced by `dead_code = "deny"` in workspace lints. Remove unused code, don't `#[allow(dead_code)]` it. +- **`OsStr`/`Path` over `String`** for filesystem paths — Linux paths may not be valid UTF-8. Only convert to `String`/`str` when you know the data is always valid UTF-8. Use `bstr` crate if you need string operations on `OsStr`. +- **Comments explain "why", not "what"** — if you need to describe what code does, improve the naming instead. +- **Macros sparingly** — explore simpler alternatives first. + +### License + +**MIT** — every file. Only these dependency licenses are acceptable: MIT, Apache-2.0, ISC, BSD-2-Clause, BSD-3-Clause, CC0-1.0, Unicode-3.0, Zlib, MPL-2.0. No GPL/LGPL dependencies ever. + +## Build & Development Commands + +All builds and tests run inside Docker containers. Do not build or test on the host. + +```bash +# Docker workflow (primary) +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) +docker compose run --rm debian cargo clippy --workspace --all-targets -- -D warnings +docker compose run --rm debian cargo fmt --all --check +docker compose build --pull # rebuild all images with latest base + +# Cargo commands (inside container) +cargo build # build all crates +cargo test --workspace # run all tests across all crates +cargo test -p shadow-core # test only the core library +cargo test -p uu_passwd # test a single tool crate +cargo test -p uu_passwd -- test_name # run a specific test +cargo clippy --workspace --all-targets -- -D warnings # lint (CI enforces zero warnings) +cargo fmt --all # format all code +cargo fmt --all --check # format check (CI gate) +cargo deny --all-features check all # license + advisory + duplicate dep audit +cargo fuzz run fuzz_passwd_parse # run a fuzz target (nightly required) +``` + +### Docker Test Matrix + +| Target | Base Image | libc | PAM | SELinux | Why | +|--------|-----------|------|-----|---------|-----| +| `debian` | `rust:latest` (Trixie) | glibc | `libpam0g-dev` | headers only | Primary dev, reference platform | +| `alpine` | `rust:alpine` | musl | `linux-pam-dev` | none | musl libc differences, container use case | +| `fedora` | `fedora:latest` | glibc | `pam-devel` | enforcing | SELinux, different PAM stack | + +## Architecture + +Cargo workspace monorepo with four layers. Dependency flows strictly downward: + +``` +src/bin/shadow-rs.rs (multicall binary — dispatches by argv[0]) + │ + ▼ +src/uu/{tool}/ (individual tool crates) + │ + ┌────┴────┐ + ▼ ▼ +uucore shadow-core (shared infrastructure + domain library) +``` + +**`uucore`** (crates.io, v0.7) provides the common uutils infrastructure: +- `UResult<()>` / `UError` trait — error handling with exit code mapping +- `#[uucore::main]` attribute — generates `uumain` wrapper with SIGPIPE/localization +- `uucore::bin!()` macro — generates `main()` for standalone binaries +- `show_error!` / `show_warning!` macros — stderr output auto-prefixed with utility name +- `Args` trait — argument handling + +**`shadow-core`** (workspace member) provides domain-specific code: +- File parsers (`/etc/passwd`, `/etc/shadow`, `/etc/group`, `/etc/gshadow`, `/etc/login.defs`) +- Atomic file writes (lock → read → write tmp → fsync → rename → unlock → invalidate nscd) +- File locking (`.lock` files with timeout and stale detection) +- PAM integration (feature-gated) +- UID/GID allocation, username validation, nscd invalidation +- SELinux context handling (feature-gated) + +### Tool Crate Structure (mirrors uutils exactly) + +Each tool in `src/uu/{tool}/` follows this exact layout: + +``` +src/uu/passwd/ +├── Cargo.toml # package = "uu_passwd", [lib] path = "src/passwd.rs" +└── src/ + ├── main.rs # one-liner: uucore::bin!(uu_passwd); + └── passwd.rs # lib crate root: uumain() + uu_app() +``` + +**Cargo.toml pattern** (per-tool): +```toml +[package] +name = "uu_passwd" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lib] +path = "src/passwd.rs" + +[[bin]] +name = "passwd" +path = "src/main.rs" + +[dependencies] +clap = { workspace = true } +uucore = { workspace = true } +shadow-core = { workspace = true, features = ["shadow", "login-defs"] } +thiserror = { workspace = true } +``` + +**main.rs** — identical one-liner for every tool: +```rust +uucore::bin!(uu_passwd); +``` + +**{tool}.rs** — every tool must export exactly two public items: +```rust +use uucore::error::UResult; + +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let matches = uu_app().try_get_matches_from(args)?; + // tool implementation + Ok(()) +} + +pub fn uu_app() -> Command { + // clap Command definition +} +``` + +### Error Handling Pattern (uucore) + +Each tool defines a private error enum implementing `uucore::error::UError`: + +```rust +#[derive(Debug)] +enum PasswdError { + PermissionDenied, + FileBusy(String), + // ... +} + +impl UError for PasswdError { + fn code(&self) -> i32 { + match self { + Self::PermissionDenied => 1, + Self::FileBusy(_) => 5, + // ... + } + } +} +``` + +Return `Err(PasswdError::...)` and the `#[uucore::main]` wrapper prints it via `show_error!` and returns the exit code. Do NOT `eprintln!` and then return the error — that double-prints. + +For informational messages (not errors), use `uucore::show_error!` directly. + +**Option constants** go in a `mod options {}` block inside the tool file: +```rust +mod options { + pub const USER: &str = "user"; + pub const LOCK: &str = "lock"; +} +``` + +### Testing + +Four levels, all mandatory for new tools: + +- **Unit tests**: in-module `#[cfg(test)] mod tests {}` blocks +- **Property tests**: `proptest` round-trip verification for all parsers (parse → serialize → parse → compare) +- **Integration tests**: `tests/by-util/test_{tool}.rs` — run `uumain()` with synthetic files, assert exit codes and file contents +- **Fuzz targets**: `fuzz/fuzz_targets/` directory, `cargo-fuzz`, all parsers — must not panic on any input + +Integration tests that mutate files require root — guard with `skip_unless_root()` and run in Docker. + +### Atomic File Write Pattern + +Every file mutation must follow this sequence: +1. Acquire `.lock` file (with timeout + stale detection) +2. Read current file +3. Write to temp file in same directory +4. `fsync` the temp file +5. `rename` temp over original (atomic on POSIX) +6. Release `.lock` file +7. Invalidate nscd cache + +## File Header + +Every `.rs` file starts with: +```rust +// This file is part of the shadow-rs package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. +// spell-checker:ignore (terms to ignore) +``` + +## Workspace Dependency Management + +All third-party dependencies are declared centrally in the root `Cargo.toml` under `[workspace.dependencies]`. Tool crates reference them with `{ workspace = true }`. This ensures consistent versions across the workspace and makes auditing straightforward. + +## Conventions + +### Commits + +- **Atomic commits** — small, self-contained, one logical change per commit +- **Tool-prefixed messages** — `passwd: fix buffer handling`, `shadow-core: add subid parser`, `tests/passwd: add aging test` +- Non-tool commits: `README: update`, `CI: add Fedora matrix`, `deny: update skip list` +- Do not move code around in the same commit as a behavior change + +### GitHub Issues & Project Board + +All work is tracked on the [shadow-rs development](https://github.com/orgs/uutils/projects) kanban board. Every bug fix or feature must start with a GitHub issue. + +- Search existing issues before creating new ones +- Include: platform, version/commit, reproduction steps, expected vs actual behavior +- Reference the relevant tool in the title: `passwd: segfault on empty shadow entry` +- Label issues: `bug`, `security`, `enhancement`, `architecture`, and tool name (`passwd`, `shadow-core`) +- Add issues to the project board — they move through: **Todo → In Progress → Done** + +### Pull Requests + +All changes go through PRs — never push to main directly. + +- One issue per PR, reference the issue: `Fixes #N` +- Title prefixed with tool name, under 70 characters: `passwd: add --status flag` +- Keep PRs small and self-contained — one logical change +- Branch naming: `fix/N-short-description` or `feat/N-short-description` +- CI must pass (clippy, fmt, tests on all 3 distros) +- Pre-commit hook runs fmt + clippy; pre-push hook runs full test suite + +#### PR Review Process — MANDATORY + +PRs are automatically reviewed by GitHub Copilot. **You MUST follow this process exactly:** + +1. **Create the PR** — push branch, `gh pr create` +2. **Wait for Copilot review** — do NOT merge immediately. Check with `gh pr checks` and `gh api repos/uutils/shadow-rs/pulls/{N}/reviews` +3. **Read every review comment** — use `gh api repos/uutils/shadow-rs/pulls/{N}/comments` to see inline comments +4. **Address every comment** — fix the code, push new commits, and reply to each comment explaining what was changed +5. **Verify review is resolved** — all comments must be resolved before merging +6. **Only then merge** — `gh pr merge --merge --delete-branch` + +Never skip or rush the review. Never merge with unresolved comments. The review exists to catch bugs in security-critical setuid-root code. + +### CI Gates (local Docker, no cloud) + +Pre-commit (every commit): +- `cargo fmt --all --check` +- `cargo clippy --workspace --all-targets -- -D warnings` + +Pre-push (every push, all 3 distros): +- `cargo test --workspace` on Debian Trixie, Alpine, Fedora +- Install hooks: `./hooks/install.sh` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bb2367e..8d957eb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,7 +45,7 @@ then write an original implementation. Never search for or view the C source. ### Setup ```shell -git clone https://github.com/shadow-utils-rs/shadow-rs +git clone https://github.com/uutils/shadow-rs cd shadow-rs docker compose build ./hooks/install.sh # install pre-commit and pre-push hooks diff --git a/Cargo.toml b/Cargo.toml index 125de20..45f46e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,8 +39,8 @@ edition = "2024" rust-version = "1.94.0" license = "MIT" authors = ["shadow-rs contributors"] -repository = "https://github.com/shadow-utils-rs/shadow-rs" -homepage = "https://github.com/shadow-utils-rs/shadow-rs" +repository = "https://github.com/uutils/shadow-rs" +homepage = "https://github.com/uutils/shadow-rs" keywords = ["shadow-utils", "uutils", "shadow", "passwd", "linux"] categories = ["command-line-utilities"] diff --git a/README.md b/README.md index 3bef675..936fa25 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ # shadow-rs -[![License](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/shadow-utils-rs/shadow-rs/blob/main/LICENSE) -[![CI](https://github.com/shadow-utils-rs/shadow-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/shadow-utils-rs/shadow-rs/actions/workflows/ci.yml) -[![MSRV](https://img.shields.io/badge/MSRV-1.94.0-blue)](https://github.com/shadow-utils-rs/shadow-rs) +[![License](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/uutils/shadow-rs/blob/main/LICENSE) +[![CI](https://github.com/uutils/shadow-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/uutils/shadow-rs/actions/workflows/ci.yml) +[![MSRV](https://img.shields.io/badge/MSRV-1.94.0-blue)](https://github.com/uutils/shadow-rs) @@ -79,7 +79,7 @@ default-in-Ubuntu in under 3 years. shadow-rs follows that playbook. ### Build ```shell -git clone https://github.com/shadow-utils-rs/shadow-rs +git clone https://github.com/uutils/shadow-rs cd shadow-rs docker compose build debian docker compose run --rm debian cargo build --release diff --git a/SECURITY.md b/SECURITY.md index 3ed0b8f..9d721aa 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -15,7 +15,7 @@ We take security issues extremely seriously. Instead, please report vulnerabilities via GitHub's private vulnerability reporting feature: -1. Go to https://github.com/shadow-utils-rs/shadow-rs/security/advisories +1. Go to https://github.com/uutils/shadow-rs/security/advisories 2. Click "New draft security advisory" 3. Fill in the details diff --git a/shadow-rs.spec b/shadow-rs.spec index 52b6af4..0dffffa 100644 --- a/shadow-rs.spec +++ b/shadow-rs.spec @@ -3,7 +3,7 @@ Version: 0.0.1 Release: 1%{?dist} Summary: Memory-safe reimplementation of shadow-utils in Rust License: MIT -URL: https://github.com/shadow-utils-rs/shadow-rs +URL: https://github.com/uutils/shadow-rs Source0: %{name}-%{version}.tar.gz BuildRequires: rust >= 1.94.0