0004 — OpenZFS version & pool-feature lifecycle (keyboot is the ZFS floor)
Status: accepted (2026-06-22) — ratified by maintainer; proposed 2026-06-04
Context
A pool that has enabled OpenZFS feature flags can only be imported by a ZFS implementation that understands them. If the recovery environment’s ZFS is older than the pool’s enabled features, the pool won’t import — the classic “recovery USB’s zfs is too old to import my pool” lockout, and a continent-away brick.
keyboot is the recovery environment. It carries its own zfs.ko + userland
(SPEC §3, build via zfs/build.sh, bundled by ci/build-image.sh INCLUDE_ZFS=yes), independent of whatever the booted BE ships. Source/rolling
distros make this sharp: Gentoo can keyword-unmask and emerge a newer
sys-fs/zfs, and an operator can run zpool upgrade and silently push the pool
past what keyboot can import. SPEC §3 already locks the doctrine — “ZFS coupling
tracks upstream OpenZFS in lockstep; hosts upgrade keyboot before bumping pool
features.” This ADR pins the concrete guards that make the doctrine hold.
Key fact that bounds the risk: upgrading the ZFS software is safe; only
enabling pool features is dangerous. OpenZFS never auto-enables features, and
no distro’s package upgrade runs zpool upgrade. Newer ZFS imports an
older-feature pool fine. Lockout happens only when features get enabled
beyond the recovery env’s capability.
This ADR protects keyboot’s recovery floor on two fronts: the ZFS feature lockout above, and the integrity of the ESP — the mdraid1 vfat that holds keyboot’s own kernel/initramfs + GRUB. The ESP is the only unencrypted boot artifact; if a stray write corrupts it, the recovery floor itself is gone, so it gets the same “protect by default, change only deliberately” treatment.
Decision
- keyboot’s embedded OpenZFS is the floor. The pool’s enabled feature set must never exceed what keyboot can import. Release ordering: upgrade keyboot (its embedded zfs.ko/userland) before bumping pool features.
- Pin
compatibility=on every pool (defaultopenzfs-2.1-linuxor a keyboot-blessed set; the compat files are bundled into the rescue image and shipped in the BE). With the property set,zpool upgradecan only enable features within that set — so the pool structurally cannot outrun keyboot as long as keyboot ≥ the pinned baseline. This is the real safety net. zpool upgradeis gated, never automatic. Ship a wrapper shim (/usr/local/sbin/zpool, earlier in PATH than the real binary) in the BE that intercepts only theupgradesubcommand: it checks the resulting feature set against keyboot’s capability / the pinned compatibility and refuses or warns, thenexecs the realzpool(located by absolute path, not PATH, to avoid recursion) forupgradeonce cleared and for all other subcommands untouched. This is an accident guardrail, not a security control — explicit/sbin/zpoolcalls and automation bypass it; guard (2) is the structural guarantee.- Boot-time feature-vs-capability check. At import, keyboot compares the
pool’s required features against its own
zfs.koand warns loudly (and declines a silent rw import that would worsen the gap) if the pool has outrun it — surfacing the problem at the next boot, while the pool is still importable, instead of at the moment it bricks. - Mount the ESP read-only by default. The ESP holds keyboot + GRUB — the
recovery floor itself — and is not needed at runtime (verified: it isn’t
even mounted in a normally-booted BE). So the BE mounts it
roby default; a gated keyboot/GRUB update is the only thing that remounts itrw, does its write, and drops back toro. This blocks a straydd, a distrogrub-install/os-prober, or a fat-fingered/boot/efiwrite from corrupting the one artifact that must survive for recovery to work. (Pairs with the keyboot self-upgrade boundary — A/B keyboot images on the ESP so even a botched keyboot update is recoverable — now decided in ADR 0009, which carves out one bounded ESP-rw exception: keyboot’s trial-commit grubenv write at unlock+import.)
Alternatives considered
- Discipline only (no pin, no shim): relies on the operator remembering the
ordering; one
zpool upgradebricks recovery. Rejected. - Hard-block
zpool upgradeentirely: too rigid — feature upgrades are legitimate after keyboot is upgraded. Rejected in favour of gate-with-check. - Shim
zfsto policezfs snapshot/other subcommands: breaks replication, sanoid,zfs send, and our own snapshot tool. Theupgradegate lives onzpool; thezfsside is warn-only at most (see ADR 0006). - TPM/attested version negotiation: overkill for v1. Deferred.
Consequences
- A keyboot release that bumps OpenZFS must ship before fleet pools enable the new features — a documented release-ordering rule.
- The shim is best-effort (PATH order); the
compatibility=pin is the binding guarantee. Both, plus the boot-time check, are defense-in-depth. - keyboot’s import path (stage-6/7) gains the feature-vs-capability comparison + a loud warning channel.
- Gentoo unmask + emerge of newer ZFS is safe; the discipline is “bump keyboot’s
embedded ZFS in the same step, and treat
zpool upgradeas gated.” - Ties into the upgrade story (SPEC §14); the gate belongs with
keyboot-install upgrade.