Files
shadow/CONTRIBUTING.md
T
Pierre Warnier b516b87be8 docs: the upstream suite is not GNU, and is mostly BSD-licensed (#322)
* docs: the upstream suite is not GNU, and is mostly BSD-licensed

The README called the original shadow-utils a GNU project and GPL-2.0+,
and CONTRIBUTING grounded the clean-room policy on that. Neither holds,
as #320 points out: shadow-maint/shadow is not a GNU project, and its
tree is mostly BSD-3-Clause with GPL-2.0-or-later files among it --
su.c, vipw.c, contributed scripts, some translated manuals. Checked
against Debian's copyright file for the package and Fedora's licence
tag, BSD-3-Clause AND GPL-2.0-or-later, not against the source.

The policy stands, on the accurate ground: this is an independent MIT
implementation that copies from neither part. Copying from the BSD part
would carry attribution terms this project does not take on; from the
GPL part, a derivative work; sorting files by licence would mean reading
them. So the rule is unchanged: do not read that tree.

Every "GNU" that meant upstream is now "upstream" or "the distribution's
package": README, CONTRIBUTING, Makefile, the man pages' section heading,
the e2e image and suite, test comments. make test-gnu-compat is make
test-upstream-compat and its script is renamed with it. The 2026-05-04
audit record gets an erratum at the top and is otherwise left as it was
run.

* docs: the last upstream-as-GNU mentions in the compat suite and the tool table

* tests: say upstream, not GNU, in the test comments and skip messages too

* tests: one more

* docs: keep the README's clean-room line as it was, reworded around it
2026-09-21 15:52:51 +02:00

8.6 KiB

Contributing to shadow-rs

Thanks for wanting to contribute to shadow-rs! This document explains everything you need to know.

Before you start, also check:

Warning

shadow-rs is original code and cannot contain any code from the upstream shadow-utils suite, shadow-maint/shadow. That tree is mostly BSD-3-Clause with GPL-2.0-or-later files among it (su.c, vipw.c, contributed scripts, some manuals); copying from the first part carries attribution terms this MIT project does not take on, and from the second would make this a derivative work. Rather than sort files by licence, we accept no change based on that source code at all, and to make sure that cannot happen, you must not read or link to it. This includes paraphrasing or translating its logic, and feeding it into an LLM for translation. It is not a GNU project, whatever older text here said.

Safe Reference Sources

When implementing a tool, reference ONLY these sources:

Source License Use
POSIX specification Open standard Behavioral spec
Man pages (man7.org) Documentation Command options, file formats
FreeBSD src BSD-2-Clause Implementation patterns
OpenBSD src ISC Implementation patterns
musl libc MIT pwd/grp/shadow C API
sudo-rs Apache-2.0 / MIT PAM, privilege-dropping

Process: Read the POSIX spec and man page for behavioral requirements, then write an original implementation. Never search for or view the C source.

Getting Started

Requirements

  • Docker and Docker Compose (all builds/tests run in containers)
  • Git

Setup

git clone https://github.com/uutils/shadow
cd shadow
docker compose build
./hooks/install.sh  # install pre-commit and pre-push hooks

The hooks are deliberately small. pre-commit checks formatting and runs clippy; pre-push runs make test once, on Debian, and skips entirely for a push that carries no new commits (a tag, a deletion, a branch the remote already has). Everything else — MSRV, cargo-deny, the static musl archive, the three-distro matrix as root, the upstream output comparison and the end-to-end deployment image — runs in CI on the pull request, in parallel and with retries. A hook that tried to match CI took minutes on every push and was routed around with --no-verify, which protects nothing.

There is a second hook system, .pre-commit-config.yaml, run by pre-commit.ci on the pull request. It does not overlap: whitespace, line endings, large files, and the spell checker. The two are kept apart on purpose — file hygiene needs no container, and the Rust checks need one.

Building and Testing

make check is the single definition of what CI gates on — formatting, clippy with and without pam, and the tests in both configurations. Run it in a container; do not build on the host.

docker compose run --rm debian make check           # everything CI gates on
docker compose run --rm debian make test-upstream-compat # compare against upstream
docker compose run --rm debian make test-arm64      # cross-build arm64 and run it

docker compose run --rm debian cargo build          # build
docker compose run --rm debian cargo test --workspace  # test on Debian
docker compose run --rm alpine cargo test --workspace  # test on Alpine (musl)
docker compose run --rm fedora cargo test --workspace  # test on Fedora (SELinux)

All three must pass. The alpine image tests musl with dynamic linking; the release also ships a static musl archive, built without PAM, which make dist-musl reproduces and CI builds on every pull request. What that build gives up is documented in docs/PLATFORM-SUPPORT.md.

After a tag has been pushed and the release pipeline has finished, run the published archives before announcing anything:

make verify-release TAG=0.5.0    # on the host: it drives docker and gh

It downloads the three archives, checks their digests, and runs each in a container of the kind it is meant for -- Debian 12 for the glibc archive, Alpine for the static one, qemu for arm64 -- installed setuid root and called by an unprivileged user through a symlink, which is how make install-multicall deploys them and the one thing no test suite does, since they all run as root. A green pipeline has shipped an archive that would not start and another whose setuid tools no user could run; this is what catches that.

Publishing to crates.io

The crates are owned by @sylvestre on crates.io; publishing needs an owner's token. The library is published as uu_shadow_core (its Rust name stays shadow_core; shadow-core on crates.io is somebody else's crate). Order matters, since each crate's dependencies must already be on the registry at the same version:

  1. uu_shadow_core
  2. every uu_<tool> crate that depends only on it
  3. uu_vigr, uu_pwunconv, uu_grpconv, uu_grpunconv, uu_newgidmap, which each wrap another tool's crate
  4. uu_shadow, the multicall binary, which depends on all of them

cargo publish --dry-run -p uu_shadow_core works from a checkout; the others can only be dry-run once their dependencies are published.

Linting

docker compose run --rm debian cargo clippy --workspace --all-targets -- -D warnings
docker compose run --rm debian cargo fmt --all --check

Design Goals

  • Drop-in replacement: same flags, same exit codes, same output format as the upstream shadow-utils suite. Differences with it are treated as bugs.
  • uutils compatible: tools use uucore (UResult<()>, #[uucore::main], show_error!) so they can be merged into the uutils ecosystem.
  • Memory safe: no .unwrap(), no panic!, no std::process::exit in library code.
  • Well-tested: unit tests, proptest, integration tests, fuzz targets.

Our Rust Style

Don't panic!

Never use .unwrap() or panic!. Use unreachable! only with a justifying comment. Return errors via UResult<()>.

Don't exit

Utilities must be embeddable. Return UResult<()> from uumain. The uucore::bin!() macro handles process::exit in the generated main().

unsafe

Denied at the workspace level (unsafe_code = "deny"). Only three FFI boundary modules are exempted: shadow_core::pam (PAM C library), shadow_core::crypt (POSIX crypt(3)) and shadow_core::process (setuid, signal masks, getpwuid_r). Every unsafe block must have a // SAFETY: comment.

str, OsStr & Path

Use OsStr and Path for filesystem paths. Only convert to String/str when you know the data is always valid UTF-8 (usernames, for example).

Comments

Comments explain why, not what. If you need to describe what code does, improve the naming instead.

Commits

  • Small, atomic commits — one logical change per commit
  • Tool-prefixed messages: passwd: fix buffer handling
  • Test commits: tests/passwd: add aging test
  • Do not move code in the same commit as a behavior change

Pull Requests

  • One issue per PR, reference it: Fixes #N
  • Title prefixed with tool name, under 70 characters
  • Branch naming: fix/N-short-description or feat/N-short-description
  • CI must pass (clippy, fmt, tests on all 3 distros)
  • Keep PRs small and self-contained

AI-Assisted Development

AI tools (Copilot, Claude, etc.) are allowed and treated like any other development tool.

  • All code must be understood, reviewed, and tested by human contributors
  • Clean-room rule is absolute — never feed GPL source into an LLM
  • Reference only: POSIX specs, man pages, BSD-licensed implementations
  • If a PR is substantially AI-generated, note it (transparency, not a blocker)
  • Quality is the gate — contributions are judged on correctness and test coverage, not authorship method

Licensing

shadow-rs is distributed under the terms of the MIT License.

Acceptable dependency licences are exactly the ones the dependency graph uses today: MIT, Apache-2.0, BSD-3-Clause, Unicode-3.0. The list is deliberately no wider than the graph, so a crate arriving under a new licence is a decision someone records in deny.toml rather than something that passes silently.

No GPL or LGPL dependencies, ever, and no MPL-2.0 either: it is file-level copyleft and this project ships MIT only.