From 30ee9c139ab823c8151a12bd53029fb5813481f4 Mon Sep 17 00:00:00 2001 From: Pierre Warnier Date: Sun, 6 Sep 2026 11:40:16 +0200 Subject: [PATCH] release: 0.4.0 gpasswd is a fifteenth tool, so a minor bump rather than a patch. Also corrects the three static-musl sections for it. gpasswd is setuid and authenticates a group administrator against the group password through crypt(3) rather than PAM, so unlike chfn and chsh it keeps working in the musl build; it resolves the caller through getpwuid_r, so it joins the five tools that do not see directory users there; and it can neither verify nor set a yescrypt group password. The counts around those paragraphs were left at fourteen tools. The README's multicall paragraph still named four setuid applets. --- CHANGELOG.md | 10 ++++++ Cargo.lock | 34 ++++++++++----------- Cargo.toml | 66 ++++++++++++++++++++-------------------- README.md | 8 ++--- docs/PLATFORM-SUPPORT.md | 12 +++++--- 5 files changed, 71 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 607aa42..3a07ec3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.4.0] - 2026-09-06 + ### Added - `gpasswd`, the fifteenth tool: it administers `/etc/group` and @@ -15,6 +17,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 is installed setuid; the superuser may additionally set the administrator and member lists with `-A` and `-M`. Contributed by @jlesage (#282) +### Changed + +- `docs/PLATFORM-SUPPORT.md` accounts for `gpasswd` in the three static-musl + gaps: like `newgrp` it authenticates against the group password through + crypt(3) rather than PAM, so it keeps working for a group administrator + there; it resolves the caller through `getpwuid_r`, so it does not see + directory users; and it can neither verify nor set a yescrypt group password + ## [0.3.1] - 2026-09-05 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 4668ad4..af0891a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -534,7 +534,7 @@ dependencies = [ [[package]] name = "shadow-core" -version = "0.3.1" +version = "0.4.0" dependencies = [ "clap", "landlock", @@ -695,7 +695,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uu_chage" -version = "0.3.1" +version = "0.4.0" dependencies = [ "clap", "rustix", @@ -706,7 +706,7 @@ dependencies = [ [[package]] name = "uu_chfn" -version = "0.3.1" +version = "0.4.0" dependencies = [ "clap", "rustix", @@ -717,7 +717,7 @@ dependencies = [ [[package]] name = "uu_chpasswd" -version = "0.3.1" +version = "0.4.0" dependencies = [ "clap", "rustix", @@ -729,7 +729,7 @@ dependencies = [ [[package]] name = "uu_chsh" -version = "0.3.1" +version = "0.4.0" dependencies = [ "clap", "rustix", @@ -740,7 +740,7 @@ dependencies = [ [[package]] name = "uu_gpasswd" -version = "0.3.1" +version = "0.4.0" dependencies = [ "clap", "rustix", @@ -752,7 +752,7 @@ dependencies = [ [[package]] name = "uu_groupadd" -version = "0.3.1" +version = "0.4.0" dependencies = [ "clap", "rustix", @@ -763,7 +763,7 @@ dependencies = [ [[package]] name = "uu_groupdel" -version = "0.3.1" +version = "0.4.0" dependencies = [ "clap", "rustix", @@ -774,7 +774,7 @@ dependencies = [ [[package]] name = "uu_groupmod" -version = "0.3.1" +version = "0.4.0" dependencies = [ "clap", "rustix", @@ -785,7 +785,7 @@ dependencies = [ [[package]] name = "uu_grpck" -version = "0.3.1" +version = "0.4.0" dependencies = [ "clap", "shadow-core", @@ -795,7 +795,7 @@ dependencies = [ [[package]] name = "uu_newgrp" -version = "0.3.1" +version = "0.4.0" dependencies = [ "clap", "rustix", @@ -807,7 +807,7 @@ dependencies = [ [[package]] name = "uu_passwd" -version = "0.3.1" +version = "0.4.0" dependencies = [ "clap", "rustix", @@ -818,7 +818,7 @@ dependencies = [ [[package]] name = "uu_pwck" -version = "0.3.1" +version = "0.4.0" dependencies = [ "clap", "shadow-core", @@ -828,7 +828,7 @@ dependencies = [ [[package]] name = "uu_shadow" -version = "0.3.1" +version = "0.4.0" dependencies = [ "clap", "clap_complete", @@ -854,7 +854,7 @@ dependencies = [ [[package]] name = "uu_useradd" -version = "0.3.1" +version = "0.4.0" dependencies = [ "clap", "rustix", @@ -865,7 +865,7 @@ dependencies = [ [[package]] name = "uu_userdel" -version = "0.3.1" +version = "0.4.0" dependencies = [ "clap", "rustix", @@ -876,7 +876,7 @@ dependencies = [ [[package]] name = "uu_usermod" -version = "0.3.1" +version = "0.4.0" dependencies = [ "clap", "rustix", diff --git a/Cargo.toml b/Cargo.toml index 2f7b69f..657aa0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ members = [ ] [workspace.package] -version = "0.3.1" +version = "0.4.0" edition = "2024" rust-version = "1.94.0" license = "MIT" @@ -45,7 +45,7 @@ categories = ["command-line-utilities"] [workspace.dependencies] # Internal crates -shadow-core = { path = "src/shadow-core", version = "0.3.1" } +shadow-core = { path = "src/shadow-core", version = "0.4.0" } # CLI clap = { version = "4", features = ["wrap_help"] } @@ -73,25 +73,25 @@ tempfile = "3" [dependencies] clap = { workspace = true } clap_complete = { workspace = true, optional = true } -shadow-core = { path = "src/shadow-core", version = "0.3.1" } +shadow-core = { path = "src/shadow-core", version = "0.4.0" } rustix = { workspace = true } # Tool crates (optional, enabled by features) -passwd = { optional = true, version = "0.3.1", package = "uu_passwd", path = "src/uu/passwd" } -pwck = { optional = true, version = "0.3.1", package = "uu_pwck", path = "src/uu/pwck" } -useradd = { optional = true, version = "0.3.1", package = "uu_useradd", path = "src/uu/useradd" } -userdel = { optional = true, version = "0.3.1", package = "uu_userdel", path = "src/uu/userdel" } -usermod = { optional = true, version = "0.3.1", package = "uu_usermod", path = "src/uu/usermod" } -chpasswd = { optional = true, version = "0.3.1", package = "uu_chpasswd", path = "src/uu/chpasswd" } -chage = { optional = true, version = "0.3.1", package = "uu_chage", path = "src/uu/chage" } -groupadd = { optional = true, version = "0.3.1", package = "uu_groupadd", path = "src/uu/groupadd" } -groupdel = { optional = true, version = "0.3.1", package = "uu_groupdel", path = "src/uu/groupdel" } -groupmod = { optional = true, version = "0.3.1", package = "uu_groupmod", path = "src/uu/groupmod" } -grpck = { optional = true, version = "0.3.1", package = "uu_grpck", path = "src/uu/grpck" } -chfn = { optional = true, version = "0.3.1", package = "uu_chfn", path = "src/uu/chfn" } -chsh = { optional = true, version = "0.3.1", package = "uu_chsh", path = "src/uu/chsh" } -newgrp = { optional = true, version = "0.3.1", package = "uu_newgrp", path = "src/uu/newgrp" } -gpasswd = { optional = true, version = "0.3.1", package = "uu_gpasswd", path = "src/uu/gpasswd" } +passwd = { optional = true, version = "0.4.0", package = "uu_passwd", path = "src/uu/passwd" } +pwck = { optional = true, version = "0.4.0", package = "uu_pwck", path = "src/uu/pwck" } +useradd = { optional = true, version = "0.4.0", package = "uu_useradd", path = "src/uu/useradd" } +userdel = { optional = true, version = "0.4.0", package = "uu_userdel", path = "src/uu/userdel" } +usermod = { optional = true, version = "0.4.0", package = "uu_usermod", path = "src/uu/usermod" } +chpasswd = { optional = true, version = "0.4.0", package = "uu_chpasswd", path = "src/uu/chpasswd" } +chage = { optional = true, version = "0.4.0", package = "uu_chage", path = "src/uu/chage" } +groupadd = { optional = true, version = "0.4.0", package = "uu_groupadd", path = "src/uu/groupadd" } +groupdel = { optional = true, version = "0.4.0", package = "uu_groupdel", path = "src/uu/groupdel" } +groupmod = { optional = true, version = "0.4.0", package = "uu_groupmod", path = "src/uu/groupmod" } +grpck = { optional = true, version = "0.4.0", package = "uu_grpck", path = "src/uu/grpck" } +chfn = { optional = true, version = "0.4.0", package = "uu_chfn", path = "src/uu/chfn" } +chsh = { optional = true, version = "0.4.0", package = "uu_chsh", path = "src/uu/chsh" } +newgrp = { optional = true, version = "0.4.0", package = "uu_newgrp", path = "src/uu/newgrp" } +gpasswd = { optional = true, version = "0.4.0", package = "uu_gpasswd", path = "src/uu/gpasswd" } [features] default = ["passwd", "pwck", "useradd", "userdel", "usermod", "chpasswd", "chage", @@ -122,21 +122,21 @@ path = "src/bin/completions.rs" required-features = ["completions"] [dev-dependencies] -chage = { version = "0.3.1", package = "uu_chage", path = "src/uu/chage" } -chfn = { version = "0.3.1", package = "uu_chfn", path = "src/uu/chfn" } -chpasswd = { version = "0.3.1", package = "uu_chpasswd", path = "src/uu/chpasswd" } -chsh = { version = "0.3.1", package = "uu_chsh", path = "src/uu/chsh" } -groupadd = { version = "0.3.1", package = "uu_groupadd", path = "src/uu/groupadd" } -groupdel = { version = "0.3.1", package = "uu_groupdel", path = "src/uu/groupdel" } -groupmod = { version = "0.3.1", package = "uu_groupmod", path = "src/uu/groupmod" } -newgrp = { version = "0.3.1", package = "uu_newgrp", path = "src/uu/newgrp" } -passwd = { version = "0.3.1", package = "uu_passwd", path = "src/uu/passwd" } -useradd = { version = "0.3.1", package = "uu_useradd", path = "src/uu/useradd" } -userdel = { version = "0.3.1", package = "uu_userdel", path = "src/uu/userdel" } -usermod = { version = "0.3.1", package = "uu_usermod", path = "src/uu/usermod" } -pwck = { version = "0.3.1", package = "uu_pwck", path = "src/uu/pwck" } -grpck = { version = "0.3.1", package = "uu_grpck", path = "src/uu/grpck" } -shadow-core = { path = "src/shadow-core", version = "0.3.1" } +chage = { version = "0.4.0", package = "uu_chage", path = "src/uu/chage" } +chfn = { version = "0.4.0", package = "uu_chfn", path = "src/uu/chfn" } +chpasswd = { version = "0.4.0", package = "uu_chpasswd", path = "src/uu/chpasswd" } +chsh = { version = "0.4.0", package = "uu_chsh", path = "src/uu/chsh" } +groupadd = { version = "0.4.0", package = "uu_groupadd", path = "src/uu/groupadd" } +groupdel = { version = "0.4.0", package = "uu_groupdel", path = "src/uu/groupdel" } +groupmod = { version = "0.4.0", package = "uu_groupmod", path = "src/uu/groupmod" } +newgrp = { version = "0.4.0", package = "uu_newgrp", path = "src/uu/newgrp" } +passwd = { version = "0.4.0", package = "uu_passwd", path = "src/uu/passwd" } +useradd = { version = "0.4.0", package = "uu_useradd", path = "src/uu/useradd" } +userdel = { version = "0.4.0", package = "uu_userdel", path = "src/uu/userdel" } +usermod = { version = "0.4.0", package = "uu_usermod", path = "src/uu/usermod" } +pwck = { version = "0.4.0", package = "uu_pwck", path = "src/uu/pwck" } +grpck = { version = "0.4.0", package = "uu_grpck", path = "src/uu/grpck" } +shadow-core = { path = "src/shadow-core", version = "0.4.0" } rustix = { workspace = true } tempfile = { workspace = true } diff --git a/README.md b/README.md index 1c6783f..5b61207 100644 --- a/README.md +++ b/README.md @@ -96,11 +96,11 @@ layout matching GNU shadow-utils. Only `passwd`, `chfn`, `chsh`, `newgrp`, sudo make install PREFIX=/usr/local ``` -Alternative: single multicall binary with symlinks. Smaller footprint (~14× +Alternative: single multicall binary with symlinks. Smaller footprint (~15× disk savings). The binary is installed setuid-root so that `passwd`, `chfn`, -`chsh` and `newgrp` can serve unprivileged callers; every other applet drops -back to the caller's uid before it runs, so the privilege model is the same -as the per-tool layout. Intended for container/embedded use cases. +`chsh`, `newgrp` and `gpasswd` can serve unprivileged callers; every other +applet drops back to the caller's uid before it runs, so the privilege model +is the same as the per-tool layout. Intended for container/embedded use cases. ```shell sudo make install-multicall PREFIX=/usr/local diff --git a/docs/PLATFORM-SUPPORT.md b/docs/PLATFORM-SUPPORT.md index bd28d4c..ed467d1 100644 --- a/docs/PLATFORM-SUPPORT.md +++ b/docs/PLATFORM-SUPPORT.md @@ -86,9 +86,10 @@ Effect, and this is the heaviest of the three gaps: than apply an unverified one, so without PAM they refuse every non-root invocation outright. -Unaffected: `passwd -S/-l/-u/-d/-e/-n/-x/-w/-i`, `newgrp` (which authenticates -against the group password through crypt(3), not PAM), and the other eleven -tools, which are root-only anyway and reach `/etc/shadow` directly. +Unaffected: `passwd -S/-l/-u/-d/-e/-n/-x/-w/-i`, and `newgrp` and `gpasswd`, +which authenticate against the group password through crypt(3) rather than +PAM -- so a group administrator can still use them. The other ten tools are +root-only anyway and reach the account files directly. ### 2. No NSS @@ -97,7 +98,7 @@ tools, which are root-only anyway and reach `/etc/shadow` directly. 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 five tools +and reads `/etc/passwd` directly. On a directory-joined host those six tools do not see network users at all. ### 3. No yescrypt (`$y$`) @@ -108,7 +109,8 @@ 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` fails against a `$y$` group password and `chpasswd -c +hashes, so `newgrp` and `gpasswd` fail against a `$y$` group password, +`gpasswd` cannot set one on a host configured for yescrypt, and `chpasswd -c YESCRYPT` is rejected. 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.