279 Commits
Author SHA1 Message Date
Pierre Warnier 67899fd7d6 release: publish the workspace to crates.io from the release pipeline (#333)
Until now a tag produced the archives and the GitHub release, and the
crates were published by hand from the tag's tree with an owner's
token; crates.io fell three minor versions behind that way.

dist's publish-jobs now calls a reusable workflow once the archives are
built and the release exists, on a real tag only. It authenticates with
crates.io Trusted Publishing -- GitHub's OIDC token exchanged for a
short-lived crates.io one, nothing stored -- and runs
.github/scripts/publish-crates.sh, which asks the crates.io index which
workspace crates are already at this version, excludes them, and hands
the rest to `cargo publish --workspace --locked`, which orders them by
dependency. A run that stopped halfway (a crate not yet trusting the
repository, the new-crate rate limit) is rerun and resumes.

It could not be a workflow triggered by the release event: dist creates
the release with GITHUB_TOKEN, and GitHub runs no workflow for events
that token causes. crates.io matches the calling workflow's filename,
release.yml, so that is what each crate's Trusted Publishing settings
name. The same script publishes by hand.
2026-10-13 11:25:23 +02:00
Pierre Warnier daf675965b release: 0.6.0 (#332)
A minor bump, not a patch: every tool now starts from its own
environment rather than the caller's, which is a change in behaviour
for all of them, deliberate and documented under Changed. With it, the
fix for the Alpine crash that change first caused, the library crate's
manifest name aligned with crates.io, and the documentation's account
of upstream's licence corrected.

This is also the first release the crates.io copies can be published
from: the manifest name uu_shadow_core is what makes `cargo publish`
work from a checkout.
0.6.0
2026-10-13 09:22:19 +02:00
renovate[bot] 52f51297ce chore(deps): update rust crate proptest to v1.12.0 2026-10-11 10:35:08 +02:00
renovate[bot] 774a5bc9e5 chore(deps): update rust crate zeroize to v1.9.1 2026-10-07 06:56:39 +02:00
Pierre Warnier 806b2f279d ci: run Format and Clippy in the pinned debian image (#329)
Both jobs ran on the runner's own toolchain, which lags the images by a
release or two, and rustfmt and clippy change between releases. The
move to Rust 1.99 passed every CI job while `make check` in the rebuilt
image failed on a lint new in 1.99; only the local gate caught it. Both
jobs now build the debian image, as the Docker test jobs do, and run
there, so the lint CI applies is the one a contributor runs. Each job
prints the compiler version it used.
2026-10-05 11:15:37 +02:00
renovate[bot]andPierre Warnier 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>
2026-10-05 11:04:09 +02:00
renovate[bot] dc8498d1f5 chore(deps): update rust crate libc to v0.2.190 2026-10-03 07:03:49 +02:00
Pierre Warnier 55a881aa2b shadow-core: replace the environment by swapping the pointer, not clearenv (#325)
Two Alpine nightlies crashed with SIGSEGV inside unit tests: the
integration test binary once, uu_passwd's lib tests once. Both call a
tool's entry point in-process from some tests, and since the tool
replaces its environment first thing, that meant clearenv(3) followed
by one setenv(3) per kept variable in a process whose other threads
were reading the environment (tempfile asks for TMPDIR). The C library
takes no lock around environ, and musl's setenv reallocates the array
on each call, so a reader walking the previous array reads freed
memory.

replace_environment now builds a fresh NUL-terminated array of fresh
KEY=value strings and points environ at it: one aligned pointer store,
nothing freed. The exec-time array and its strings stay where they
are, unfreed -- a few kilobytes, once per process. A concurrent reader
sees the old array or the new one, both intact. POSIX allows a program
to assign environ, and every libc getenv/setenv copes with an array it
did not allocate; the libc crate does not declare environ for Linux,
so the module does. What the tool ends up with is unchanged.

Reproduced with a small program on Alpine: 16 threads calling getenv
and walking environ while the main thread replaces the environment
2000 times. clearenv+setenv crashed 19 runs of 20; the swap, 0 of 20.
The real lib tests do not reproduce it on demand (32 test threads, 120
runs, 0 crashes on either tree); the window is narrow there.

make check in the container: 2842 tests, 0 failed. e2e: 416
assertions, the PAM probe still sees the replaced environment and the
fixed PATH.
2026-09-28 15:15:58 +02:00
renovate[bot] 3b0c0012c5 chore(deps): update rust crate thiserror to v2.0.21 2026-09-24 07:09:08 +02:00
Pierre Warnier 9bd1cec3d3 shadow-core: give the tool its own environment, not the caller's (#323)
harden_process() left the process environment as the caller exported
it. The PAM stack, the NSS modules and the crypt library run inside the
tool and read the environment themselves, so they read the caller's;
only children were given a clean one. Every tool now replaces its
environment first thing, while still single-threaded.

What stays is what has a known reader here: TERM and NO_COLOR for how
prompts are drawn; LANG, LANGUAGE and LC_* for the language of messages,
PAM's included; VISUAL and EDITOR for vipw; and TZ, so chage -l prints
dates in the caller's zone, but only when it names a zone -- glibc also
takes a path there and reads that file with the process's privilege, so
such a value is dropped. PATH is fixed. The rest is gone before the
first line of the tool runs. The allow-list is a pure function with
unit tests, and a further test re-runs the test binary as a child,
calls harden_process there and reads the environment back.

The deployment suite proves the claim where it matters. A small PAM
module, tests/e2e/pam_envprobe.c, built into the e2e image, runs inside
a real password change and writes out the environment of the process
it runs in: a variable the caller exported is not there, and PATH is
the fixed one. pam_exec was tried first and cannot do this job: it
gives its child the PAM environment and the PAM items, never the
calling process's, so a probe run through it saw nothing whether or not
the tool had cleaned up. Checked against main's binary with the module:
there the caller's variable and PATH both reach the PAM stack.

Closes #314.
2026-09-21 16:32:53 +02:00
renovate[bot] bcd9bf5dbb chore(deps): update rust crate uucore to 0.12 (#319)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-21 15:52:59 +02:00
Pierre Warnier b516b87be8 docs: the upstream suite is not GNU, and is mostly BSD-licensed (#322)
* docs: the upstream suite is not GNU, and is mostly BSD-licensed

The README called the original shadow-utils a GNU project and GPL-2.0+,
and CONTRIBUTING grounded the clean-room policy on that. Neither holds,
as #320 points out: shadow-maint/shadow is not a GNU project, and its
tree is mostly BSD-3-Clause with GPL-2.0-or-later files among it --
su.c, vipw.c, contributed scripts, some translated manuals. Checked
against Debian's copyright file for the package and Fedora's licence
tag, BSD-3-Clause AND GPL-2.0-or-later, not against the source.

The policy stands, on the accurate ground: this is an independent MIT
implementation that copies from neither part. Copying from the BSD part
would carry attribution terms this project does not take on; from the
GPL part, a derivative work; sorting files by licence would mean reading
them. So the rule is unchanged: do not read that tree.

Every "GNU" that meant upstream is now "upstream" or "the distribution's
package": README, CONTRIBUTING, Makefile, the man pages' section heading,
the e2e image and suite, test comments. make test-gnu-compat is make
test-upstream-compat and its script is renamed with it. The 2026-05-04
audit record gets an erratum at the top and is otherwise left as it was
run.

* docs: the last upstream-as-GNU mentions in the compat suite and the tool table

* tests: say upstream, not GNU, in the test comments and skip messages too

* tests: one more

* docs: keep the README's clean-room line as it was, reworded around it
2026-09-21 15:52:51 +02:00
Pierre Warnier b29236088a ci: do not read a bot's pull-request text for AI tooling names (#321)
Renovate's #319 failed the trace check on its body, which is the pasted
release notes of uucore 0.12 and names a code-review tool in the title
of an upstream coreutils change. Nobody here wrote that text, and a
dependency update should not fail over it. A bot's title and body are
now passed to the check as empty; its commits and its diff are checked
as before, and a human's title and body still are.
2026-09-21 15:06:35 +02:00
renovate[bot] a116e9fa0f chore(deps): update rust crate rustix to v1.1.5 2026-09-17 07:12:22 +02:00
renovate[bot] 53a68c0328 chore(deps): update rust crate clap_complete to v4.6.11 2026-09-16 07:05:23 +02:00
Pierre Warnier bf0550374d shadow-core: name the package uu_shadow_core, as crates.io knows it (#316)
The library was published as uu_shadow_core (0.2.1) because shadow-core
on crates.io belongs to an unrelated project, but the manifest still
said shadow-core, so a publish from a checkout could not work and the
crates.io copies fell three minor versions behind. The package is now
uu_shadow_core; its Rust name stays shadow_core through [lib] name, and
every dependant reaches it through the same `shadow-core` dependency
key with `package = "uu_shadow_core"`, so no code changes. The fuzz
workspace, the lockfile and the two `-p` invocations in ci.yml follow.

CONTRIBUTING gains the publish order: the library, the tool crates, the
five thin crates that wrap another tool's crate, then the multicall.

`cargo publish --dry-run -p uu_shadow_core` packages 32 files;
`make check` in the container is green.
2026-09-15 12:44:02 +02:00
Pierre Warnier 6f2621c938 docs: point the environment-hardening item at an issue that is open (#315)
#249 was closed on 2026-09-04 with the record I/O work and never covered
this; the item now has its own issue, #314, with what it would take.
2026-09-15 12:08:18 +02:00
Pierre Warnier 3a4f5c543d release: keep the dist build-setup steps out of GitHub's workflow directory (#313)
GitHub reads every YAML file directly under .github/workflows as a
workflow. The build-setup file added for dist is a bare list of steps,
not a workflow, and GitHub reported it as a failing one on every push
to main since #312. dist resolves github-build-setup relative to
.github/workflows, so the file moves one directory down, where GitHub
does not look. release.yml regenerated: identical, the steps it inlines
are the same.
2026-09-15 11:56:21 +02:00
Pierre Warnier c6c2d8fdeb release: make apt non-interactive in the build container (#312)
The 0.5.1 pipeline failed on both architectures before building
anything: dist installs the apt dependencies from dist-workspace.toml
with a plain `apt-get install`, no `-y`, and inside the buildpack-deps
container apt asked "Do you want to continue?" and answered itself
"Abort". The step is generated by dist and takes no flags (axodotdev/
cargo-dist#2353); on GitHub's own runners it works only because their
apt is configured to assume yes, which the container's is not. The
same workflow built 0.5.0 eight days earlier on an earlier build of the
same image.

dist's github-build-setup inserts steps into every build job before it
installs dist and its dependencies. The one added here configures apt
the way the runners do and sets DEBIAN_FRONTEND, in the container, so
dist's own line succeeds whatever the image does. Verified by running
the step and then dist's exact apt line, stdin closed, in
buildpack-deps:22.04: libpam0g-dev installs, exit 0.

release.yml regenerated with dist 0.33.0; the diff is the inserted
step.
0.5.1
2026-09-15 11:33:58 +02:00
Pierre Warnier 3b0885e91f release: 0.5.1 (#311)
Two install fixes and no new tool, so a patch. `make install` no longer
fails on a system without a shadow group -- Fedora, Arch, a packager's
chroot -- and chage can now read a user's own aging line, the one thing
it lets a user do. Both matter to the Arch package, which builds from
these tarballs with `make install`. Also ships `make verify-release`.
2026-09-15 11:21:20 +02:00
Pierre Warnier ab6b3df367 chage: let a user read their own aging line (#310)
`chage -l` is the one mode a user may run, on their own account, and it
did not work for them: `make install` put chage at 0755, which cannot
read /etc/shadow, and the multicall binary dropped its privilege before
running it. The Makefile's own comment placed chage in bin "for chage
-l, the one mode a user may run on their own account".

The packaged binaries settle the contract: Debian ships chage 2755
root:shadow, the same as expiry; Fedora and Arch have no shadow group
and ship it 4755 root. chage now follows expiry -- setgid shadow, or
setuid root where the group is absent, and an applet that keeps euid 0
in the multicall binary. chage decides what the caller may do from the
real uid, and --root is refused before the chroot on the same basis, so
the privilege buys a user their own line and nothing else.

Verified with the per-tool binaries installed in the debian image under
both layouts; as an unprivileged user under each, chage -l on the own
account answers, chage -l root, chage -M 5 and chage --root are refused,
and the shadow field is unchanged afterwards. The deployment suite
gains those four assertions for the multicall layout.
2026-09-15 11:11:56 +02:00
renovate[bot]andPierre Warnier 2ce67f9b3b chore(deps): update dependency axodotdev/cargo-dist to v0.33.0 (#305)
* chore(deps): update dependency axodotdev/cargo-dist to v0.33.0

* release: regenerate release.yml for dist 0.33.0

Renovate bumps cargo-dist-version alone; the workflow it generates pins
the same version in its installer line and has to be regenerated with
the new dist, or the release job installs 0.32.0 to build a manifest
that says 0.33.0. `dist generate --mode ci` with 0.33.0 changes that one
line, and `dist plan` with 0.33.0 still yields the nine artifacts.

0.33.0 adds Windows code signing and moves the shell installer's env
helper; this project ships neither.

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Pierre Warnier <pierre@warnier.net>
2026-09-15 10:52:54 +02:00
Pierre Warnier 91c9fc12a5 install: cope with a system that has no shadow group (#309)
`make install` put expiry setgid `shadow`, which is how Debian and Ubuntu
ship it, and failed outright wherever that group does not exist: Fedora
and Arch have none -- /etc/shadow there is readable by root alone -- and
a packager's chroot may lack it whatever the target. The Arch packager
had to sed the Makefile (#306), and the result, setgid to a group with
no rights on the file, left expiry unable to read it for anyone but
root.

On those systems the GNU tool that reads /etc/shadow on a user's behalf,
chage, is setuid root; that is the convention expiry now follows where
the group is absent. It buys a user nothing more: expiry checks the real
uid and reads that one line. SHADOW_GROUP names a group that goes by
another name. The choice is made once, at make time, from getent, and
the install summary says which layout was used.

Verified in the debian image with the group (2755 root:shadow) and with
SHADOW_GROUP pointing at a group that does not exist (4755 root), and in
the fedora image, where `make install` now completes; under each layout
`expiry -c` answers an unprivileged user.
2026-09-15 10:52:48 +02:00
renovate[bot] 663e58d8ab chore(deps): update rust crate clap_complete to v4.6.10 2026-09-15 07:14:11 +02:00
renovate[bot] ff7028f9b9 chore(deps): update rust crate clap to v4.6.7 2026-09-15 07:13:47 +02:00