The README and the dist config both claimed musl is unavailable because
crypt(3) is not part of the static musl target. That is wrong: musl
implements crypt() inside libc —
$ nm .../x86_64-unknown-linux-musl/.../libc.a | grep ' T crypt'
0000000000000000 T crypt
The link failure comes from `#[link(name = "crypt")]` emitting -lcrypt,
which finds no libcrypt.a in Rust's musl sysroot and falls through to the
host's glibc libxcrypt. Gating that attribute on the target env is enough
to produce a working static binary.
The genuine blocker is PAM: it dlopens its modules, which a fully static
binary cannot do, so a musl artifact would ship without interactive
password changes. State that instead, and point at #224.
* ci: reject AI tooling artifacts in pull requests
The project's public face carries no AI tooling attribution, but nothing
enforced it: a session trailer reached a merged commit message and a pull
request body before anyone noticed.
Add a check that runs on pull requests and inspects only what the PR
changes — commit messages, PR title and body, added lines, and any
attempt to track the local-only guidance and review-export files that
are meant to stay gitignored. History is untouched and unrelated files
are not scanned. The clean-room audit records are exempt: they must
describe their own methodology to serve as compliance evidence.
Also drop the tooling credits from README and CHANGELOG.
* ci: fix base-ref fetch in the artifact check
`--depth=0` is rejected by git, and the extra fetch was redundant with
the full-history checkout. Fetch the base branch plainly and diff against
FETCH_HEAD.
Releases carried no binaries (v0.2.1 shipped zero assets). Add dist
(formerly cargo-dist) 0.32.0 to build and upload them on tag push.
Scope: only the workspace root package is distributed, producing the
single shadow-rs multicall binary that provides all 14 tools via argv[0]
— the same layout as `make install-multicall`. The 14 per-tool crates
carry `dist = false` so a release is one archive per target rather than
fifteen. `[dist.binaries]` pins the archive to `shadow-rs`, keeping the
feature-gated `shadow-rs-completions` helper out of it.
Targets are glibc-only for now: shadow-core::crypt links crypt(3) via
`#[link(name = "crypt")]`, and the self-contained musl target provides
no libcrypt, so an x86_64-unknown-linux-musl cross-build fails at link
time. (Alpine CI passes only because it installs a system libcrypt.)
`[dist.dependencies.apt]` installs libcrypt-dev on the runner for the
same reason.
`[profile.dist]` inherits the hardened release profile, so published
binaries keep LTO, stripped symbols and panic = "abort".
README documents deploying the archive (symlinks + setuid) and why musl
is not published yet.
Verified locally: `dist plan` and `dist build --artifacts=local` produce
uu_shadow-x86_64-unknown-linux-gnu.tar.gz containing shadow-rs plus
README/CHANGELOG/LICENSE and a .sha256; the extracted binary reports
`shadow-rs (uutils shadow-rs) 0.2.1`.
Closes#207.
Claude-Session: https://claude.ai/code/session_011f42R6SMAfQXufTgdjSuhU
Address Copilot review round 2:
- Remove PROFILE variable and hardcode --release / target/release/.
Cargo maps "dev" profile to target/debug/ (not target/dev/), so
PROFILE=dev would silently break install. Nobody installs debug
builds of setuid-root tools.
- Fix stale comments in Makefile and README that referenced the old
"chmod on symlink follows through" approach. The current code uses
install -Dm4755 directly.
Switch the default install to build and install 14 standalone per-tool
binaries with a least-privilege setuid layout matching GNU shadow-utils.
Only passwd/chfn/chsh/newgrp are setuid-root; the other 10 tools are 0755.
The previous multicall install is preserved as `make install-multicall`
for container/embedded use cases where the ~14x disk savings matter and
the enlarged setuid attack surface is acceptable.
The old layout chmod'd symlinks pointing to the multicall binary, which
(because chmod follows symlinks) marked the underlying ELF setuid-root.
That meant all 14 tools ran with euid=0 when invoked via symlink. Each
tool's internal getuid() check was defense-in-depth, not OS-level least
privilege. The standalone layout gives the latter.
Raised by @oech3 in uutils/coreutils#11828.
- README: fix passwd flag count (17→16), add Landlock mention, add
Hardened goal, update usermod description with -p, fix past tense
- CONTRIBUTING: fix unsafe description to match actual workspace policy
- CHANGELOG: add [Unreleased] section for post-v0.1.0 changes
- OPENBSD-REFERENCE: move all implemented items to correct section
- SECURITY-HARDENING: mark all 21 items as implemented
- man/usermod: add -p/--password flag documentation
All 14 shadow-utils tools are now implemented.
groupadd: create groups with auto GID allocation, system groups, force mode.
groupdel: delete groups, checks for primary group usage.
groupmod: modify GID, rename, password changes.
grpck: verify /etc/group and /etc/gshadow integrity.
chfn: change GECOS sub-fields (name, room, phone).
chsh: change login shell with /etc/shells validation.
newgrp: change effective group with crypt(3) password verification.
449 tests, zero clippy warnings, all 14 tools in multicall binary.
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/