From 4ce76d3e5e9a6406b4717aae1109b4aaaa45e864 Mon Sep 17 00:00:00 2001 From: Brian Ketelsen Date: Wed, 12 Aug 2026 20:41:18 -0400 Subject: [PATCH] =?UTF-8?q?docs(adr):=20ADR-0030=20=E2=80=94=20shipped=20s?= =?UTF-8?q?ystemd=20units=20never=20use=20RequiredBy=3D=20enablement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First-boot presets persist [Install] enablement into each machine's /etc, where it outlives the image that created it. A persisted .wants link dangles harmlessly when its unit is later retired; a persisted .requires link (from RequiredBy=) becomes a Requires= on a unit that fails to load and invalidates PID 1's very first transaction — the machine dies at "Failed to isolate default target" before any service runs, so no runtime migration can ever fire. This bricked updates on real hardware 2026-08-12 (snosi e08311f retired snow-linux-live-setup.service; image 20260812205454 failed all counted boots on every install first-booted before the retirement, while fresh installs and therefore all CI passed). Decision: WantedBy= is the only [Install] enablement in Frostyard-shipped units; hard dependencies are declared from the dependent unit's [Unit] section or via static wants in /usr. Repos shipping unit payloads enforce this in CI (snosi ADR-0013: check-required-by-guard.sh), and image boot paths that can do so prune stale .requires links defensively (snosi's native A/B initrd), since distro packages may still ship RequiredBy=. Co-Authored-By: Claude Fable 5 --- docs/README.md | 1 + ...-requiredby-enablement-in-shipped-units.md | 73 +++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 docs/adr/0030-no-requiredby-enablement-in-shipped-units.md diff --git a/docs/README.md b/docs/README.md index 3dce2fe..64c8622 100644 --- a/docs/README.md +++ b/docs/README.md @@ -42,6 +42,7 @@ Docs are split by the question they answer: - [0027 — Retire fisherman; firn is the frostyard bootc installer](adr/0027-retire-fisherman-superseded-by-firn.md) - [0028 — Retire snosi-install; firn is the frostyard A/B installer](adr/0028-retire-snosi-install-superseded-by-firn.md) - [0029 — ACMM conformance via canonical aliases](adr/0029-acmm-conformance-via-canonical-aliases.md) +- [0030 — Shipped systemd units never use RequiredBy= enablement](adr/0030-no-requiredby-enablement-in-shipped-units.md) ### Design diff --git a/docs/adr/0030-no-requiredby-enablement-in-shipped-units.md b/docs/adr/0030-no-requiredby-enablement-in-shipped-units.md new file mode 100644 index 0000000..4396a5a --- /dev/null +++ b/docs/adr/0030-no-requiredby-enablement-in-shipped-units.md @@ -0,0 +1,73 @@ +# 0030 — Shipped systemd units never use RequiredBy= enablement + +- **Status:** Accepted +- **Date:** 2026-08-12 + +## Context + +Frostyard OS images (snosi and successors) apply preset policy at first +boot, materializing shipped units' `[Install]` sections as enablement +symlinks in the machine's persistent `/etc` — state that outlives the +image that created it. When a later image retires a unit, a persisted +`.wants` link dangles harmlessly, but a persisted `.requires` link (from +`RequiredBy=`) becomes a `Requires=` on a unit that fails to load. That +invalidates PID 1's very first transaction — starting `default.target` — +and the machine dies at "Failed to isolate default target" before any +service runs, before the journal persists, and before any runtime +migration could possibly fire. + +This bricked real hardware on 2026-08-12: snosi's `e08311f` retired +`snow-linux-live-setup.service` (`RequiredBy=multi-user.target`, +`RequiredBy=display-manager.service`); image `20260812205454` failed all +three counted boots on every install whose first boot predated the +retirement, while fresh installs — and therefore every CI boot test — +passed. A/B fallback saved the machines; nothing saved the update. + +`WantedBy=` has no such failure mode, and hard startup ordering can +always be expressed from the dependent unit's side. + +## Decision + +Units shipped in Frostyard-built images, sysexts, and packages do not use +`RequiredBy=` (or hand-shipped `*.requires/` links) for enablement. Where +a hard dependency is genuinely needed, the *dependent* unit declares +`Requires=`/`BindsTo=` in its `[Unit]` section, or the repository's +static-wants-in-/usr pattern is used — mechanisms that live in `/usr` and +update atomically with the image, leaving no persisted `/etc` state to go +stale. `WantedBy=` remains the normal enablement mechanism. + +Repositories that ship unit payloads enforce this with a CI guard (snosi: +`check-required-by-guard.sh`, ADR-0013 there), with a per-line +escape-hatch comment for the rare provably-safe exception. OS images +whose boot path can do so additionally prune stale `.requires` links +defensively before PID 1 sees them (snosi: the native A/B initrd's +etc-overlay module), because third-party packages can still ship +`RequiredBy=` units and packages get dropped from images. + +## Consequences + +- Retiring or renaming a shipped unit is always safe with respect to + persisted enablement; unit removal no longer requires a migration + story for `/etc`. +- Authors must express hard dependencies in the dependent direction, + which matches systemd upstream practice. +- Third-party (distro-packaged) `RequiredBy=` units remain possible; + image repos are expected to carry the defensive prune where their boot + architecture allows it. + +## Alternatives considered + +- **Per-retirement no-op stub units:** permanent name cruft, easy to + forget, and a mask variant is itself boot-fatal for required units. +- **Runtime migration services:** structurally impossible — the failure + precedes all services. +- **Policy without enforcement:** this exact hazard shipped once already; + a convention that CI does not check will regress. + +## References + +- Shapes: snosi + [ADR-0013](https://github.com/frostyard/snosi/blob/main/docs/adr/0013-no-requiredby-enablement-prune-stale-requires.md) + (guard + initrd prune implementation) +- Builds on: [ADR-0004](0004-product-namespaced-filesystem-tiers.md) + (/run/snosi runtime-state reporting path used by the prune)