mirror of
https://github.com/uutils/shadow.git
synced 2026-10-16 02:48:14 -07:00
* chore(deps): update rust docker tag to v1.99 * docs: the test matrix table follows the Rust image bump Renovate bumps the three Dockerfiles; the table in PLATFORM-SUPPORT.md names the same images and was still at 1.98. * tests: show the value when asserting that something is empty Rust 1.99's clippy adds assert_is_empty: a bare assert!(x.is_empty()) says only that the check failed, not what x held. Twenty-two test assertions across the workspace were of that form; each now carries the value in its message, which is what the lint is after and reads better than its own suggestion of comparing against `[] as [T; 0]`. Test code only. The CI clippy job runs on the runner's toolchain, which has not reached 1.99 yet, so the pull request was green while `make check` in the rebuilt image was not. * tests: the expiry output is already a string --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Pierre Warnier <pierre@warnier.net>
168 lines
8.5 KiB
Markdown
168 lines
8.5 KiB
Markdown
# Platform support
|
|
|
|
What shadow-rs builds and runs on, which archives a release publishes, and
|
|
exactly where one build behaves differently from another.
|
|
|
|
The distinction that matters most is the C library. Every release publishes
|
|
glibc archives for two architectures and one static musl archive, and the two
|
|
kinds are **not interchangeable**: the glibc archives are the full build; the
|
|
static musl archive is a self-contained binary for containers and embedded
|
|
images that gives up three capabilities, each stated below. Its name carries a
|
|
`-static` suffix so the two are never confused.
|
|
|
|
## Release archives
|
|
|
|
| Archive | Architecture | libc | Linking | `pam` feature | Use it for |
|
|
|---|---|---|---|---|---|
|
|
| `uu_shadow-x86_64-unknown-linux-gnu.tar.gz` | x86-64 | glibc | dynamic (libpam, libcrypt, libc) | on | Any regular distribution install. The full build. |
|
|
| `uu_shadow-aarch64-unknown-linux-gnu.tar.gz` | arm64 | glibc | dynamic (libpam, libcrypt, libc) | on | The same build for arm64 servers and Apple-silicon containers. |
|
|
| `uu_shadow-x86_64-unknown-linux-musl-static.tar.gz` | x86-64 | musl | static, no runtime dependencies | off | Minimal containers and embedded images with a local `/etc/passwd`, no directory service and no PAM stack. |
|
|
|
|
Every archive ships a `.sha256` file in `sha256sum -c` format and contains the
|
|
`shadow-rs` multicall binary with `LICENSE`, `README.md` and `CHANGELOG.md`;
|
|
the musl archive also carries this document, so the trade-off travels with the
|
|
binary.
|
|
|
|
The glibc archives are dist's regular target builds with `features = ["pam"]`.
|
|
**arm64 is built on a native arm64 runner, not cross-compiled**, which is what
|
|
makes it straightforward: `shadow-core::crypt` links crypt(3) and the `pam`
|
|
feature links libpam, so a cross build would need an arm64 sysroot carrying
|
|
both, while a native runner installs them from apt like any other build.
|
|
|
|
The musl archive is produced differently. dist cannot vary features per target
|
|
and `pam` must be off for musl (see below), so it is built by `make dist-musl`
|
|
and attached as an extra artifact of the same release. Anyone can reproduce it
|
|
with that one command.
|
|
|
|
Both glibc archives are built inside an `ubuntu:22.04` container, so they run
|
|
on any system with **glibc 2.34 or newer** — Debian 12, Ubuntu 22.04, RHEL 9
|
|
and anything later. The runner itself is 24.04, because 22.04 runners are
|
|
being retired; building on it directly raised the floor to 2.39, since Rust's
|
|
`std` links `pidfd_spawnp` when the build host has it. Older systems than that
|
|
should use the static musl archive, which has no libc dependency at all.
|
|
|
|
## Test matrix
|
|
|
|
CI runs the full suite on three images:
|
|
|
|
| Image | Base | libc | PAM | SELinux |
|
|
|---|---|---|---|---|
|
|
| `debian` | `rust:1.99-trixie` | glibc | Linux-PAM | headers |
|
|
| `alpine` | `rust:1.99-alpine3.23` | musl (dynamic) | Linux-PAM | none |
|
|
| `fedora` | `fedora:44` | glibc | Linux-PAM | enforcing |
|
|
|
|
The base images are pinned. An unpinned tag lands a new toolchain on `main`
|
|
with no pull request to run it against, and clippy gains lints between Rust
|
|
releases; Renovate proposes the bumps instead.
|
|
|
|
The `alpine` image links musl dynamically and therefore *does* have PAM; it
|
|
tests musl's libc behaviour (no NSS, no yescrypt), not the static archive. The
|
|
static archive has its own job, `Static musl archive`, which on every pull
|
|
request runs `make dist-musl`, smoke-tests the result, runs `shadow-core`'s
|
|
unit tests against static musl, and checks that requesting `pam` there is
|
|
rejected at compile time. A tag is never the first time the archive is built.
|
|
|
|
## What the static musl build gives up
|
|
|
|
A static musl binary **builds and runs**; the whole test suite passes on musl.
|
|
What it cannot do is inherent to static linking and to musl, not to shadow-rs,
|
|
and for account-management tools none of it is cosmetic.
|
|
|
|
### 1. No PAM
|
|
|
|
Linux-PAM loads its modules with `dlopen(3)`. A fully static binary cannot do
|
|
that: musl's static `dlopen` is a stub that returns *"Dynamic loading not
|
|
supported"*, and no distribution ships a `libpam.a` to link against in the
|
|
first place. `shadow-core` refuses the `pam` feature on static musl with a
|
|
compile-time error, so the build cannot be misconfigured into a binary whose
|
|
authentication path never works.
|
|
|
|
Effect, and this is the heaviest of the three gaps:
|
|
|
|
- `passwd`'s interactive password change is unavailable and says so
|
|
(*"PAM support is not compiled in"*).
|
|
- `chfn` and `chsh` become **root-only**. They authenticate the caller through
|
|
PAM before applying a change, and a setuid-root tool must fail closed rather
|
|
than apply an unverified one, so without PAM they refuse every non-root
|
|
invocation outright.
|
|
- `login` has no way to authenticate at all, so the applet prints *PAM
|
|
support is not compiled in* and exits 1. A static image that needs a
|
|
getty-driven login must use the glibc archive. utmp and wtmp recording is
|
|
also a no-op under musl, which has stub `pututxline` and `updwtmpx`;
|
|
`who(1)` and `last(1)` see nothing there.
|
|
- `expiry` still reports, and `-c` still refuses an expired account; the
|
|
forced change it performs on an expired password goes through PAM, so the
|
|
static build says so and points at `passwd(1)` instead.
|
|
|
|
Unaffected: `passwd -S/-l/-u/-d/-e/-n/-x/-w/-i`; `newgrp`, `sg` and
|
|
`gpasswd`, which authenticate against the group password through crypt(3)
|
|
rather than PAM -- so a group administrator can still use them; `chage`,
|
|
`newuidmap` and `newgidmap`, which never authenticate through PAM; and the
|
|
seventeen root-only tools, `useradd` through `grpunconv`, which reach the
|
|
account files directly.
|
|
|
|
### 2. No NSS
|
|
|
|
`shadow_core` resolves the calling user through `getpwuid_r`, used by
|
|
`passwd`, `chfn`, `chsh`, `chage`, `newgrp`, `sg`, `gpasswd`, `expiry`,
|
|
`newuidmap` and `newgidmap`; `login` resolves the name typed at the prompt
|
|
through `getpwnam`.
|
|
|
|
glibc answers such lookups through its NSS module system, so it sees users from
|
|
LDAP, SSSD, Active Directory or systemd-userdb. musl has no NSS module system
|
|
and reads `/etc/passwd` directly. On a directory-joined host those eleven tools
|
|
do not see network users at all.
|
|
|
|
### 3. No yescrypt (`$y$`)
|
|
|
|
musl's crypt(3) implements DES, MD5, SHA-256, SHA-512 (including `rounds=`)
|
|
and bcrypt, byte-identical to libxcrypt for all of them. It does not implement
|
|
yescrypt.
|
|
|
|
This is not a marginal format: **Debian 12+ and Ubuntu 24.04 use yescrypt as
|
|
the default password hash.** A musl build can neither verify nor produce `$y$`
|
|
hashes, so `newgrp`, `sg` and `gpasswd` fail against a `$y$` group password,
|
|
`gpasswd` cannot set one on a host configured for yescrypt, and `-c YESCRYPT`
|
|
is rejected by `chpasswd`, `chgpasswd` and `newusers`. The prefix guard in
|
|
`shadow_core::crypt` reports the unsupported method explicitly; it never falls
|
|
back to a weaker hash silently. The default method is SHA-512, which is
|
|
unaffected.
|
|
|
|
### Where the static build is the right choice
|
|
|
|
Minimal containers and embedded images: a local `/etc/passwd`, no directory
|
|
service, no PAM stack. There none of the three gaps costs anything, and a
|
|
single binary with no runtime dependencies is worth having. Anywhere one of
|
|
them matters, use the glibc archive.
|
|
|
|
## Building for musl
|
|
|
|
```shell
|
|
make dist-musl
|
|
```
|
|
|
|
The target adds the `x86_64-unknown-linux-musl` toolchain component, builds the
|
|
multicall binary in release mode with the lockfile enforced and without `pam`,
|
|
verifies that the binary has no `DT_NEEDED` entry, and writes the archive and
|
|
its checksum to `target/dist-musl/`.
|
|
|
|
Two details in the code make that work:
|
|
|
|
- **crypt(3) linkage.** On glibc systems crypt(3) lives in libcrypt (libxcrypt
|
|
on Debian, Fedora and derivatives), so `shadow_core::crypt` asks for
|
|
`-lcrypt`. musl implements `crypt()` inside libc and ships no libcrypt;
|
|
Rust's self-contained musl sysroot has no `libcrypt.a` either, so the request
|
|
would fall through to the host's glibc-built libxcrypt and fail on glibc-only
|
|
symbols. The attribute is therefore `#[cfg_attr(not(target_env = "musl"),
|
|
link(name = "crypt"))]`. (Alpine builds never hit this because `musl-dev`
|
|
ships an empty `libcrypt.a` that absorbs the flag.)
|
|
- **`pam` guard.** `shadow_core::pam` emits `compile_error!` when built for
|
|
musl with `crt-static`, the default for the musl target. Dynamic musl, as in
|
|
the Alpine test image, is unaffected and links libpam normally.
|
|
|
|
libxcrypt itself is **LGPL-2.1+** and cannot be vendored to fill the yescrypt
|
|
gap: shadow-rs takes no GPL or LGPL dependencies. Pure-Rust alternatives exist
|
|
(`sha-crypt`, `yescrypt`, both MIT/Apache-2.0) and match libxcrypt output, but
|
|
the yescrypt crate is self-declared unaudited, which rules it out of a
|
|
setuid-root path for now.
|