mirror of
https://github.com/uutils/shadow.git
synced 2026-10-16 02:48:14 -07:00
main
13
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
64ac0752f0 |
chore(deps): update rust docker tag to v1.99 (#327)
* 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> |
||
|
|
0516184c31 |
release: 0.5.0 (#303)
Fourteen tools since 0.4.0 -- sg, chgpasswd, newusers, vipw, vigr, pwconv, pwunconv, grpconv, grpunconv, login, newuidmap, newgidmap and expiry, plus usermod's subordinate-id options -- so a minor bump. The platform document's static-musl sections are brought up to the twenty-eight tools: login joins the tools PAM takes away, expiry keeps reporting there and only its forced change needs PAM, chage and the two id-map tools never authenticate through PAM, and the root-only set is seventeen. The NSS list grows to eleven with expiry, newuidmap, newgidmap and login's getpwnam. -c YESCRYPT is refused by chgpasswd and newusers as it is by chpasswd. Also the first release whose static archive works setuid for a user, which is recorded under Fixed. |
||
|
|
7e28940874 |
expiry, and usermod's subordinate-id options (#299)
* usermod: add and remove subordinate id ranges GNU usermod has -v/--add-subuids, -V/--del-subuids, -w/--add-subgids and -W/--del-subgids; ours had none of them, which came to light while writing the tests for newuidmap. The range helpers live in shadow_core::subid so whatever next touches /etc/subuid reads the same file the same way. The behaviour was established by running the GNU tool: a range the user already holds is not added twice; a removal trims an entry it overlaps, splits one it cuts through the middle of, deletes one it covers, and ignores a range never held; nothing checks other users' entries, since an administrator may share a range on purpose; and an invalid range -- reversed, not two numbers, above the 32-bit ids the kernel maps -- is exit 3 with the GNU wording, before any file is written. Entries are keyed by the login name given on the command line and are not renamed by -l, as GNU's are not: a range is a grant to a name. -V is GNU's letter for the deletion, so clap's own -V for --version had to go; --version stays. * expiry: check and enforce password expiration expiry is the twenty-eighth tool and the last the Scope table lists as in scope. It judges the caller's own /etc/shadow line as login would: silent when all is well, a warning inside warn_days, a forced change through PAM when the password has expired, a refusal when the account has. Shell profiles run it so a session that did not come through login -- a display manager, an ssh key -- still meets the aging policy. The rules are in shadow_core::shadow::Aging, one function over the aging fields with checked arithmetic, since every input is read from a file anyone who can write /etc/shadow chooses. Its order is login's: account expiry first, then a last change of 0 or an aged-out password, then the warning window, with a maximum age of 10000 or more meaning never as chage -l prints it. A locked password is a policy of its own and is not reported. The GNU tool is setgid shadow -- enough to read the file, no more -- and the change goes through PAM as the caller. The per-tool install ships it the same way, a new install class. The single setuid binary keeps euid 0 for it, having nothing narrower to offer. Under --prefix it reports and never changes anything, since a PAM change would act on this system's account of the same name; that is also what lets the required-change path be asserted in the integration tests without a prompt, while the e2e suite drives the prompt itself on a real user under a pty and checks the hash changed. groupmems is not coming. Debian 13 and Fedora do not ship it, and on Ubuntu 24.04 every action fails against PAM's default for want of a /etc/pam.d/groupmems: a tool one distribution ships and none can use. The Scope table says so. Verified: make check clean with the unprivileged run; 935 tests on alpine and 950 on fedora with PAM; 410 e2e assertions against a real install; 59 GNU comparisons; 35 arm64 assertions under emulation. * shadow-core: stop asking nscd to invalidate a shadow cache it never had Every passwd, chage and chpasswd ended with `nscd -i shadow`, and on a host running nscd that printed "nscd: 'shadow' is not a known database" on the user's terminal each time. nscd caches passwd, group, hosts, services and netgroup; the passwd entries it holds carry the `x` placeholder, so a password change leaves nothing of nscd's stale. Only the databases nscd knows are passed to it now. sss_cache still gets -U for a shadow change, which is right: sssd does cache what a password change affects. |
||
|
|
aa987bb390 |
newuidmap, newgidmap: set the id maps of a user namespace (#298)
The setuid helpers that write /proc/<pid>/uid_map and gid_map: what Podman and rootless Docker call for every container they start. The kernel lets an unprivileged process map exactly one id into a namespace it created, its own; everything a container needs beyond that takes the privilege these carry, and they hand it out only within the ranges /etc/subuid and /etc/subgid grant the caller. Fedora ships them in shadow-utils; with these two, every tool the two reference distributions ship from the suite exists here. What is checked before anything is written was established by running the GNU helpers against a real user namespace, never by reading them: the operands must be a target and plain-decimal triples, a zero count is the overflow message; every outside range must sit inside one granted range -- an entry by login name or by numeric uid -- or be the caller's own id mapped once; root is exempt from neither, as newuidmap(1) says; and the target process must belong to the caller by real uid, account uid and the owner of /proc/<pid>, with all three named when they disagree. Overlapping ranges are refused by name; the kernel would refuse them with a bare EINVAL, so the message costs nothing and changes no accepted outcome. The map file is opened through a descriptor held on /proc/<pid>, so a pid recycled between the check and the write cannot redirect it, and the `fd:N` form takes such a descriptor from a caller that did its own checks. It is re-opened through /proc/self/fd rather than adopted, which keeps the tool free of unsafe. The tests do the real thing. A test user creates a namespace with unshare and the helper runs as that user, unprivileged, against it: the own-id mapping lands and the kernel's uid_map reads it back, the second write is refused by the kernel and reported, the fd form works, and every refusal is checked against the same live target. That runs on glibc and musl alike. The Debian image gains the uidmap package so tests/gnu-compat.sh can hold the helpers against GNU's. They take no --help -- any bad command line is the usage text and exit 1 -- so the comparison is on the refusals the two share rather than on help. Verified: make check clean with the unprivileged run; 912 tests on alpine and 934 on fedora with PAM; 393 e2e assertions against a real install, the kernel-visible writes included; 57 GNU comparisons; 34 arm64 assertions under emulation. |
||
|
|
4ac1bdfe32 |
login: begin a session on the system (#297)
* shadow-core: take /etc/.pwd.lock before the per-file lock FileLock took the per-file `.lock` first and `/etc/.pwd.lock` second. Two tools working on different files at once could then meet in the classic shape: one holding passwd.lock and waiting for .pwd.lock, the other holding shadow.lock and waiting for passwd.lock's holder to let go of .pwd.lock. Neither can move; both sit out the fifteen-second timeout; both fail -- on a system that was merely busy. Sampling the lock files every 250 ms while the whole suite ran showed it plainly: a chpasswd held shadow.lock for 28 s and a useradd held passwd, group and gshadow for 14 s, each waiting on the other, and seven tests failed with "timed out" on one lock or the other. Three runs after the change: no failures, and the suite back from 35-50 s to 17 s. .pwd.lock now comes first, the order lckpwdf(3) and the GNU tools use and the only one that cannot deadlock: whoever holds it is the only process asking for per-file locks, so it never waits for anyone who is waiting for it. If the per-file lock then cannot be taken, the .pwd.lock reference is given back, so nothing is held on failure. * useradd: lock the shadow file with the others, and write it first useradd wrote /etc/passwd and the group files in one transaction, then opened /etc/shadow on its own afterwards -- under a comment claiming the first locks were still held, which they were not: commit_all releases them. A shadow lock that timed out at that point left the account half made: a passwd line with no shadow line, which a second useradd then reported as already existing. That is exactly what turned up when the login tests put the suite under load. The shadow file is now locked with passwd, group and gshadow before anything is written, and written first, for the reason pwconv writes shadow first: a failure between the writes must never leave a passwd line whose hash is nowhere. The old write_shadow_entry helper stays for its unit test only. A regression test puts a directory where the shadow file should be and checks that useradd fails with /etc/passwd untouched. Before this change the passwd line was written and the failure came after. * login: begin a session on the system login is the twenty-fifth tool and the one uutils/login was archived towards. It is what getty runs on a terminal: it prompts for a name and, through the `login` PAM service, a password; refuses after LOGIN_RETRIES failures or LOGIN_TIMEOUT seconds; opens the PAM session and takes its environment; records the login in utmp and wtmp; hands the terminal to the user; runs the login shell as the user; and when the shell exits, closes the session and records the logout. Which login to match was a question. Debian 13 and Ubuntu 25.10 take login from util-linux, but every release in service -- Ubuntu 20.04, 22.04 and 24.04, Debian 12 -- ships shadow's; the two were run side by side on real pseudo-terminals and agree on everything that matters. This implements the option set they share (-p, -f, -h, name) plus util-linux's -H and --help, which conflict with nothing. There is no -h for help: -h has been the remote host since telnetd. -r, the rlogin protocol, is refused. util-linux's vhangup is not reproduced; shadow's login, which the installed base runs, does not do it either. A wrong password and an unknown name get the same words, after the same delay. -f skips authentication and with it the auth stack, pam_nologin included, as both tools do. A missing home falls back to / with a notice when DEFAULT_HOME allows. The shell runs as a child so the session can be closed afterwards; it gets a clean signal mask, login keeps its own, and groups, gid and uid are dropped in that order in one pre_exec. shadow-core grew what login needed and nothing had: pam setcred, open_session, close_session, user and environment; utmp/wtmp recording; getpwnam through NSS; a getty-style spawn_with_controlling_tty; spawn_as_user; and a watchdog. Without the pam feature the applet says PAM is not compiled in and exits: a login that cannot authenticate is not a login, and the static musl archive is documented accordingly. The tests drive the real binary on a pseudo-terminal the way getty would, answering each prompt once it appears: autologin, the environment with and without -p, a wrong then a right password, an unknown user, the retry limit, the missing home, and the terminal handover. Writing them under the full parallel suite is what exposed the two fixes committed before this one. Verified: make check clean with the unprivileged run; 892 tests on alpine and 882 on fedora; 368 e2e assertions against a real install, including the password path on a pty; 52 GNU comparisons; 32 arm64 assertions under emulation. * docker: give the Fedora image the login PAM service Fedora ships /etc/pam.d/login in util-linux, which the test image did not install. With no service file PAM falls back to `other`, which on Fedora denies everything: `login -f` failed its account check and the password path was refused before any prompt, so every login test failed there and only there. util-linux is now installed, which is what any real Fedora has. The login tests then run through Fedora's own stack -- pam_selinux, pam_loginuid, pam_namespace -- and pass, which makes it the second PAM configuration the session code is exercised against rather than a gap. |
||
|
|
651ed5c62b |
sg: run a command with a different primary group (#291)
sg is the sixteenth tool, and the first of the nine that both Debian and
Fedora ship and we did not. It runs a single command with a different
primary group: newgrp for one command instead of for a whole shell.
sg and newgrp are not merely similar. They decide who may enter a group by
the same rules and enter it the same way, and the distributions ship sg as a
symlink to the setuid newgrp binary -- one implementation, dispatched on
argv[0]. So the shared half moves into shadow_core::group_switch and both
tools call it, which is what keeps them from drifting apart. The multicall
binary already dispatches on argv[0], so it needs only the new applet.
sg execs the command rather than forking and waiting for it. The shell then
is the process, so the command's exit status reaches the caller unaltered:
`sg staff -c 'exit 7'` exits 7 and a command that cannot be run exits 127,
without sg reconstructing a wait status. This matches what the GNU tool does
and costs a process.
Writing the shared module turned up two things wrong in newgrp:
- It refused a group with no password without prompting, and prompted when
the group had one. Whether a prompt appeared therefore told any caller
which groups have passwords set, which is a list of the ones worth
attacking. Both tools now prompt either way and refuse afterwards.
- It rebuilt the supplementary group list with initgroups() alone, which
builds it from the member lists in /etc/group -- where a primary group
never appears. `newgrp staff` therefore dropped the group the caller
started in, costing them access to their own files until they left the
shell. The original group is added back, which needs setgroups(2) and so
has to happen before the setuid privilege is given up.
Both were established by running the GNU tools, not by reading them.
sg is setuid for the reason newgrp is: a caller who is not a member has to
be checked against the group password in /etc/gshadow, which is root-only.
Removing sg from the setuid list fails six assertions in the e2e suite --
including for a plain member, since initgroups() itself needs the privilege
-- and nothing else in the suite covers it.
Verified: 763 tests on debian, alpine and fedora; 255 e2e assertions against
a real setuid install, covering a member, a non-member with the group
password, a wrong password, a passwordless group and exit-status
propagation; 43 GNU comparisons; 19 arm64 assertions under emulation.
|
||
|
|
e33793cc74 |
release: 0.4.0 (#289)
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. |
||
|
|
8361bf512b |
gpasswd: added implementation of gpasswd (#282)
* gpasswd: added implementation of gpasswd
* gpasswd: rebase onto main and port to the current APIs
Rebases jlesage's implementation onto a main that moved a long way under
it, and carries it onto the interfaces that landed since the branch was
cut. The tool's behaviour is unchanged and still matches GNU on every
path probed.
What the rebase had to resolve:
- shadow-rs.spec and debian/ were revived by the merge; both were
deleted because neither could build.
- The standalone dispatch() and hand-written --list predate the single
applets() table, which now drives both. gpasswd joins the table.
- The five shadow-core parser features it asked for no longer exist.
- The fourteen [[test]] targets are one; test_gpasswd joins it.
- README, Makefile and PLATFORM-SUPPORT described the pre-#239
privilege model, in which every applet ran with euid 0.
What changed beyond the rebase:
- gpasswd keeps its privilege in the multicall build. It is setuid in
GNU and the branch marks it setuid for the per-tool install, but
SETUID_APPLETS still listed four names, so the multicall build would
have dropped privilege and no group administrator could have used it
-- silently.
- Comments in /etc/group survive. It read entries only and wrote them
back, which erases every comment, blank line and NIS compatibility
line in the file. The locked-file transaction keeps them, and commits
group and gshadow together so a bad value cannot leave the pair
disagreeing.
- The duplicated EchoGuard and password reader are gone, replaced by
shadow_core::tty, which blocks interrupts for the read so Ctrl-C at
the prompt cannot leave the terminal with echo off.
- The print macros are replaced. They panic when the stream is closed,
which a setuid-root tool must not do part way through a change.
These were the only five in the repository and no lint caught them,
so clippy::print_stdout and print_stderr are now denied
workspace-wide, with the test crate exempt.
The GNU comparison suite gains gpasswd: the unknown-group, unknown-user
and non-member exit codes, all three of which match.
CONTRIBUTING's licence list also said ISC, BSD-2-Clause, CC0-1.0, Zlib
and MPL-2.0 were acceptable, which stopped being true when deny.toml was
narrowed to the graph. It now says what deny.toml enforces.
---------
Co-authored-by: Pierre Warnier <pierre@warnier.net>
|
||
|
|
e09e4bccac |
release: build the glibc archives on 22.04, and drop a test that could hang (#284)
Two defects in what shipped yesterday. 0.3.0's glibc archives require GLIBC_2.39 and will not start on Debian 12, Ubuntu 22.04 or RHEL 9 -- all of which ran 0.2.2. Pinning the release runner to Ubuntu 24.04 in #272 let Rust's std link pidfd_spawnp and pidfd_getpid; those two are the only symbols in the binary above 2.34. The build now happens inside an ubuntu:22.04 container on the same runner, which is where the container support in dist earns its keep: the runner stays current while the floor goes back to GLIBC_2.34, exactly what 0.2.2 required. Verified by building in that container and dumping the versioned symbols, and by running the result on a glibc 2.35 host where the 0.3.0 archive refuses to start. The static musl archive was never affected. The PAM test for an unconfigured service could hang rather than fail. On Debian /etc/pam.d/other falls through to common-auth, which is pam_unix, so it prompts: the conversation then blocks reading stdin. CI never saw it because its stdin is not a terminal, but `make check` on a developer's machine hung indefinitely. The test is removed with a note saying why, and the permit and deny tests -- which are the ones that matter and which never converse -- stay. |
||
|
|
9cd5eef4ac |
Install layout, aarch64 archives, pinned images, and a pre-push hook worth keeping (#272)
* hooks: stop reproducing CI in pre-push The hook ran fmt, clippy and the test suite on Debian, Alpine and Fedora in series, on every push. That is several minutes for a push that may carry nothing: pushing the 0.2.2 tag moved one object and still blocked for over two minutes. The predictable result was that every push in the last batch of work used --no-verify, and a hook that is always skipped protects nothing while still costing time whenever someone forgets to skip it. It now runs the workspace tests once, on Debian, and returns immediately for a push that carries no new commits -- a tag, a branch deletion, or a branch the remote already has. git hands the hook one line per ref on stdin; the ranges it names are asked for a single commit, and if there are none there is nothing to test. Everything the hook dropped is already a required check on the pull request, running in parallel with retries: fmt, clippy and the tests with and without pam, MSRV, cargo-deny, the static musl archive, the three-distro matrix as root, and the end-to-end deployment image. Closes #231. * install: put the user-facing tools in bin, not sbin All fourteen binaries went to $(PREFIX)/sbin, which is not on a normal user's PATH: passwd was 'command not found' for exactly the people it is setuid for. GNU shadow splits them, verified against the Debian package -- passwd, chage, chfn, chsh and newgrp in /usr/bin, the other nine in /usr/sbin. Both install targets do the same now, and the multicall symlinks in bin point at the absolute path of the binary in sbin. chage is in bin for 'chage -l', the one mode a user may run on their own account. The deployment suite follows each tool to the directory it belongs in, and asserts that all five are reachable by name for an unprivileged user, which is the property the split exists for. make test also runs the suite with pam now. install ships binaries built with it, so a run without it covered a build nobody ships. * docker: pin the base images, drop the unused nextest download rust:latest, rust:alpine and fedora:latest meant a new toolchain or distro release landed on main with no pull request and no signal, and clippy gains lints between Rust releases. They are pinned to what the images resolve to today -- rust 1.98 on Trixie and Alpine 3.23, Fedora 44 -- and Renovate's Dockerfile manager proposes the bumps, so a toolchain change arrives as a reviewable PR that CI has run. Each image also downloaded cargo-nextest from get.nexte.st/latest on every build: unpinned, unverified, and never invoked -- nothing in the repository runs 'cargo nextest'. cargo-deny, which is used, keeps its pinned version and checksum. * deny: allow exactly the licences the graph uses The allow list carried ISC, BSD-2-Clause, CC0-1.0 and Zlib, none of which appears anywhere in the dependency graph. A list wider than the graph means a crate arriving under a new licence passes silently; the point of the check is that such an arrival is a decision someone records. It is now MIT, Apache-2.0, BSD-3-Clause (subtle, for the constant-time comparison in shadow-core::crypt) and Unicode-3.0 (tinystr and unicode-ident, via uucore's fluent stack), taken from 'cargo deny --all-features list'. Two crates carry a copyleft option in a dual licence and are accepted under the other half; that is now written down rather than left for the next reader to work out. All four checks pass with the narrower list. * ci: run the advisory workflow on a schedule only It also ran on every push touching a manifest or the lockfile, where the Cargo Deny job in ci.yml had already checked the same advisory database alongside licences and bans. Two tools reporting the same finding on the same commit is noise. What the pull request job cannot do is notice an advisory published against code nobody has touched, which is the reason to keep a scheduled run. That is now all this workflow does, plus manual dispatch. * packaging: remove the RPM spec and debian directory Neither can build. debian/ has no changelog at all, so dpkg-buildpackage stops before it starts: dpkg-buildpackage: error: cannot open file debian/changelog and shadow-rs.spec pins version 0.2.0 against a 0.2.2 workspace, points at the pre-transfer repository URL, and lists a %files layout that 'make install' does not produce -- it names the multicall binary, which only install-multicall builds, and puts every tool in sbin, which is no longer where half of them go. Shipping packaging that claims to work and does not is worse than shipping none, and this would have produced a package declaring 'Provides: passwd, login' on a system whose real passwd it replaced. Releases are the archives dist publishes; a distribution packager writes these against their own policy anyway. * release: publish an aarch64 archive The pipeline shipped x86_64 only. arm64 servers and Apple-silicon containers are common enough that a missing archive is a real gap. It builds on a native arm64 runner rather than cross-compiling, 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. dist has picked a native runner for aarch64 since 0.30. Both runners are pinned to 24.04. dist's defaults are ubuntu-22.04 and ubuntu-22.04-arm, and 22.04 enters deprecation on 2026-09-17 with the image unsupported from 2027-04-17; the pin keeps the two archives on the same generation and off an image that is about to start failing. The higher runner raises the glibc floor for the dynamically linked archives, which is what the static musl archive is there for. release.yml is regenerated by 'dist generate'; 'dist plan' now lists three archives and resolves ubuntu-24.04-arm for the arm64 job. Closes #222. |
||
|
|
52c2debe8b |
release: publish a labelled static musl archive (#224) (#237)
* shadow-core: link libcrypt only where it exists; refuse pam on static musl musl implements crypt(3) inside libc and ships no libcrypt. Asking for -lcrypt there made a cross-build for x86_64-unknown-linux-musl fall through to the host's glibc-built libxcrypt and fail on glibc-only symbols; Alpine only worked because musl-dev ships an empty libcrypt.a. Linux-PAM loads its modules with dlopen, which static musl does not have, and no distribution ships libpam.a. Building the pam feature into a crt-static musl binary is therefore a configuration error; reject it at compile time instead of producing a binary whose authentication path can never work. The crypt tests stated the musl behaviour wrongly (musl does honour rounds=; it lacks yescrypt). They now assert the real behaviour per target instead of skipping. * release: publish a static musl archive next to the glibc one dist cannot vary cargo features per target and pam must be off for musl, so the musl archive is built by 'make dist-musl' and attached by dist as an extra artifact of the same release. The target enforces the lockfile, checks the binary has no DT_NEEDED entry and packs it with the licence, changelog, README and PLATFORM-SUPPORT.md so the trade-off travels with the binary. The archive name carries a -static suffix so it is never mistaken for the glibc build. CI runs the same make target on every pull request, smoke-tests the archive, runs shadow-core's unit tests against static musl and checks that the pam feature is rejected there, so a tag is never the first time the archive is built. * docs: describe both release archives and what the static musl one gives up The README now lists the two archives side by side with the one question a reader has to answer (is PAM, NSS or yescrypt needed on this host?), and PLATFORM-SUPPORT.md is rewritten around the archive that now exists: how it is built, which CI job guards it, and the exact effect of each gap. CONTRIBUTING listed two unsafe-permitted modules; there are three. * chore: use the canonical uutils/shadow repository URL The repository is uutils/shadow; the old shadow-rs name is a redirect. The README badges already used the new name while package metadata, the --help footer and SECURITY.md still pointed at the old one. |
||
|
|
9f197e0e50 |
chfn, chsh: authenticate the caller before applying a change (#235)
* shadow-core: move PrivDrop out of passwd The RAII guard that drops the effective UID for the PAM conversation lived inside uu_passwd, but it is process-level privilege handling that any setuid tool needs. Move it next to the seteuid wrapper it calls. Two changes while moving it: the error type becomes io::Result rather than PasswdError so other tools can use it, and the failure-to-restore message goes through uucore::show_error! instead of a hardcoded "passwd:" prefix, so it names whichever tool is running. * chfn, chsh: authenticate the caller before applying a change Both tools are installed setuid-root. They correctly restrict a non-root caller to their own account, checked against the real UID — but they never verify who is asking. Anyone with access to an unlocked session could change that user's login shell or GECOS field, and changing a login shell is a foothold. GNU prompts for the password here, and distributions already ship the service definitions we were ignoring: /etc/pam.d/chfn and /etc/pam.d/chsh both let root through with pam_rootok and fall back to common-auth for everyone else. CONTRIBUTING treats a difference from GNU as a bug, so this is one. Authenticate through PAM for non-root callers, reusing the pattern passwd already uses: drop to the real UID for the conversation so modules see the actual caller, then authenticate and check account validity. Without the pam feature there is no way to verify anyone, so the tools refuse rather than apply an unauthenticated change. That is a behaviour change for that build only, and the safe direction for a setuid binary. It also means every install path must enable the feature — 'make build' only enabled it for passwd, which would have shipped two setuid tools that reject all non-root use. Fixed, and the end-to-end assertion now covers all three tools instead of passwd alone. CI gained 'cargo test --features pam': the feature changes behaviour rather than only adding code, so both configurations need testing. |
||
|
|
cec6872425 |
docs: document platform support and what a musl build loses (#232)
The README justified the glibc-only release with a single sentence about PAM. Two further gaps turned up while evaluating musl properly, and both matter for account-management tools: - no NSS. getpwuid_r backs current_username(), used by passwd, chfn, chsh, chage and newgrp. glibc answers through its NSS modules and sees LDAP/SSSD/AD users; musl has no module system and reads /etc/passwd, so directory users are invisible. - no yescrypt. musl's crypt(3) covers DES, MD5, SHA-256, SHA-512 with rounds= and bcrypt, byte-identical to libxcrypt, but not $y$ — which is the default hash on Debian 12+ and Ubuntu 24.04. Add docs/PLATFORM-SUPPORT.md covering release targets, the test matrix, the three musl gaps, how to build for musl anyway, and why libxcrypt (LGPL-2.1+) cannot fill the crypt gap. Point README, CONTRIBUTING and the dist config at it. |