fettle 1.16.0
[1.16.0] — the audit stops looking in the wrong place, and stops hanging
Four fixes since 1.12.0, and three of them are the same bug wearing different clothes:
software that belongs to a person was being checked as though it belonged to the
machine. A maintenance run is usually root, because most of it has to be — and at that
moment the personal half of the host goes quiet.
Every one was found by reading real run logs on a real workstation, not by re-reading code.
| version | what had been happening |
|---|---|
| 1.13.0 | GNOME extensions reported NOT audited for a week straight, on a desktop with 24 of them working. gnome-extensions asks the session bus, and sudo had discarded the address. |
| 1.14.0 | Four of six audit providers printed their coverage sentence and nothing else — "examined 24 and cleared them all" looked exactly like "never ran". |
| 1.15.0 | 11 rootless podman images had never once been audited. Not an error: as root, podman answers from root's store, which was empty. No report fettle had ever written mentioned podman. --user flatpak apps were invisible the same way. |
| 1.16.0 | A stopped snapd made fettle hang forever — -c, -P and -a, --dry-run included. |
The distinction that hid three of them
Being in fettle's no-root set does not mean an action executes unprivileged. It means
it does not elevate on its own. Run it inside -a and the process has already re-exec'd
under sudo for the mutating half, so everything after that is root — including
pkg-audit, which reads three things that live in your account.
Those three are now asked as the invoking user regardless of how the run started. podman is
asked twice, once per store, because running containers as root is ordinary on a server
and picking one identity would simply move the blind spot. docker is deliberately left
alone: it is one system daemon behind a group-owned socket that root can always reach and an
ordinary user often cannot, so dropping privileges there would break it.
The measure that matters: an elevated fettle -P and an unprivileged one now return the
same findings. The audit describes the machine, not the way it was launched.
What you will notice
Every provider now states what it looked at, so a clean result stops being silent:
[gnome] 24 extensions examined — all traceable to a package; 5 enabled
enabled and running inside gnome-shell (5 of 24): …
[container] 22 container images examined — across docker, podman, podman(paulda)
[snap] nothing to examine — no snaps installed
Four outcomes stay distinct, because collapsing any two is how an audit starts lying: not
installed · installed but nothing to examine · examined N, all clean · could not
look.
Upgrading from 1.12.0 is a drop-in replacement — no configuration changes, no report
format changes. You may see findings you have not seen before, on a machine that has not
changed: those were always there.
Honest gaps
- The flatpak fix is unverified end to end. The host it was written on has no flatpak
apps to observe the difference against. The code path is identical to podman's, which
is verified — but identical-looking is not verified. - The snap probe's healthy-path latency is unmeasured; its timeouts are conservative
choices, not fitted ones.
The snapd hang in detail
Every bug fixed this week was fettle giving a wrong answer. This one made fettle
stop responding entirely, and it was live on the reporting host.
Found by accident: the test suite stopped returning while verifying an unrelated fix.
A stack dump rather than a guess showed a real snap command blocked in
subprocess.communicate:
subprocess.run → command.run → _prune_disabled_snaps → clean_caches → _clean
Reproduced outside fettle entirely, on a host where snapd had been deliberately
disabled:
/usr/bin/snap |
present, so fettle uses it |
snapd.service / snapd.socket |
inactive (dead), service disabled |
snap list, snap list --all, snap version |
never return |
Not an exotic configuration. On Arch and Manjaro snapd ships preset: disabled, so
anyone who installs it and never runs systemctl enable --now snapd.socket has a
permanently hanging snap binary — and had a permanently hanging fettle. -c, -P and
-a all wedged, --dry-run included, because the snap inventory is a read-only query
and read-only queries deliberately bypass the dry-run gate.
The obvious probe is wrong, and was measured before being built on
/run/snapd.socket still exists while snapd is disabled — a stale file left behind
from the last time the service ran. "Does the socket exist?" therefore answers yes on
exactly the host where snap does not work. A plausible-sounding fix that does nothing.
Two parts
command.run gained an opt-in timeout=. There is no default and that is
deliberate: pacman -Syu legitimately runs for twenty minutes and rpm -Va for several,
so a blanket limit would kill the very commands fettle exists to run. A timeout returns
Proc(124) — the status GNU timeout uses, so callers match a convention rather than an
invented number — keeps whatever partial output the tool managed first, and never raises.
A cached util.snap_ready() probe asks snap version under a short clock, once per
run. Every snap call site gates on it:
| action | before | after |
|---|---|---|
clean |
hung forever | skips the prune, warns why |
pkg-audit |
hung forever | UNVERIFIABLE — snaps were NOT audited |
update |
hung forever | skips the refresh, warns why |
examined deliberately stays None for the audit: "could not look" is not "examined
zero", and recording it as an empty examination would put this host in the same bucket
as one with no snaps at all — the exact false clean this model exists to prevent.
Measured after
fettle -c --dry-run completes in 5.2 seconds where it previously never returned.
fettle -P reports snaps were NOT audited — distinct from the nothing to examine — no
snaps installed it correctly printed that same morning, while snapd was still alive.
Honest gap: the healthy-path latency is unmeasured, because no working snapd was
available to time against. 5s (probe) and 30s (inventory) are conservative choices, not
fitted ones. Erring short reports "could not look", which is the safe direction; erring
long is what the old behaviour did, forever.
A test-isolation lesson came with it
The probe caches per process — right in production, where a wedged host would otherwise
pay the timeout at three separate call sites. It also leaks between tests: one test that
resolved it to False left every later snap test quietly skipping its own subject,
passing in isolation and failing in the suite. Reset by an autouse fixture in
conftest.py. Twenty-two test files also had their command.run fakes widened to accept
the new keyword — kept explicit rather than **kwargs, because a fake that silently
swallows new arguments cannot catch a call site that forgot to pass one.
What to download
| file | for |
|---|---|
fettle_<version>_all.deb |
Debian, Ubuntu, and derivatives |
fettle-<version>-1.noarch.rpm |
RHEL, Rocky, AlmaLinux, Fedora |
fettle-<version>-1-any.pkg.tar.zst |
Arch, Manjaro, EndeavourOS |
fettle-<version>-zipapp.tar.gz / .zip |
anything else — runs on any Linux with python 3.11+ |
fettle.pyz |
the zipapp on its own, if you do not want the archive |
fettle is pure standard library: it needs python 3.11 or newer and nothing else.
On RHEL/Rocky/Alma 9 and Ubuntu 22.04 the system python3 is older than that, so the
packages pull in a suitable interpreter and the launcher finds it for you.
Verifying what you downloaded
sha256sum -c SHA256SUMS --ignore-missing