Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
34 changes: 17 additions & 17 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 33 additions & 33 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ members = [
]

[workspace.package]
version = "0.3.1"
version = "0.4.0"
edition = "2024"
rust-version = "1.94.0"
license = "MIT"
Expand All @@ -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"] }
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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 }

Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 7 additions & 5 deletions docs/PLATFORM-SUPPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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$`)
Expand All @@ -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.
Expand Down