Decision 0036
The root is a snapshot restored at boot
Architecture decision record 0036: The root is a snapshot restored at boot
Context
ADR-0012 chose “impermanence by default: an ephemeral root, a declared /persist; the machine is
a function of (genome, persist); no hidden state.” What the tree had, until now, was the second
half of that: /persist and the bind mounts that carry the genome, the state, the log and the
uid map across a boot. The first half — a root that is actually thrown away — was a sentence.
The root subvolume kept everything written to it, which is the state of every machine that is
not Tacid, and the manifesto’s claim that a machine has no hidden state was a claim about
intent.
Decision
The installer keeps a read-only snapshot of the root subvolume while it is still empty, and the initrd makes the root again from it at every boot.
- The stem’s disk sequence takes
@root-blank, a read-only snapshot of@root, right after the subvolumes are created and before anything is written into any of them. It is one more step in the same typed sequence, in the order the test holds it to. body/disks.nixdeclarestacid.core.root = { device; ephemeral = true; }: the opened LUKS volume, or the partition when the root is not encrypted. The declaration is the body’s; the mechanism is the module’s.- The module gives the initrd a service that runs after the root device appears and before
sysrootis mounted: it mounts the top of the filesystem on its own, deletes@rootand whatever is nested in it, makes a new@rootfrom the blank, and unmounts. On a machine without the declaration nothing is added. - What must survive a boot is declared, so it is: the genome and Tacid’s state, the log, the
uid map and the machine id as before, bound in from
/persist; and people’s homes, which are now a subvolume of their own,@home, mounted from the initrd. The first boot of the change had bound/homein from/persistinstead, and found the home that activation had just made hidden underneath the mount: a bind mount that arrives after activation arrives too late for anything activation creates. What a service keeps is the service’s module’s to declare, as ADR-0008 asks — the openssh module already keeps the host keys, and the first evaluation of this change found that out by declaring them twice.
Consequences
- The installation test writes a file to the root and one to
/persist, and one in the owner’s home, reboots the installed machine through its passphrase prompt, and finds the first gone and the others where they were, with the genome’s history and the hostname intact. That is the sentence, tested. /rootis not persisted. The root user is not a person on a Tacid machine; the owner has a home, and it is kept. Anything a service keeps under/var/libthat is not Tacid’s own is ephemeral until the service’s module declares otherwise, which is what ADR-0008 asks of everytacid.services.*module and what the openssh module should be held to next.- A root that is fresh at every boot is a root on which nothing can be quietly installed by hand and survive. That is the point, and it is also a surprise the first time; the agent detecting writes to ephemeral paths and offering to declare them, from ADR-0012, is the companion piece and is still to be written.
- The unit runs
btrfs subvolume delete --recursive, which needs btrfs-progs 6.5 or later; the release this tree pins has 7.1.
Alternatives rejected
- A tmpfs root. Simple, and it makes the root’s size a function of memory, which on a machine that is also running a model is the wrong thing to spend memory on.
- Deleting the root’s contents from stage 2, after mounting. Racing every unit that starts at boot for the files they are about to write. The initrd is the one place where nothing else is running yet.
- Rolling back to the snapshot only when asked. A root that is ephemeral on request is a root that keeps state, and the promise is that it does not.