* 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>
8.5 KiB
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").chfnandchshbecome 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.loginhas 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 stubpututxlineandupdwtmpx;who(1)andlast(1)see nothing there.expirystill reports, and-cstill refuses an expired account; the forced change it performs on an expired password goes through PAM, so the static build says so and points atpasswd(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
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::cryptasks for-lcrypt. musl implementscrypt()inside libc and ships no libcrypt; Rust's self-contained musl sysroot has nolibcrypt.aeither, 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 becausemusl-devships an emptylibcrypt.athat absorbs the flag.) pamguard.shadow_core::pamemitscompile_error!when built for musl withcrt-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.