Skip to content

Never Boot the Real Root

 5 min The __active/__snapshot/__rollback scheme: snapshot-and-rollback boot for Arch, with a conversion wizard living inside the initramfs.
Published

Jun 3, 2012

I posted a btrfs mkinitcpio hook to the Arch forums and put it on AUR. One user in that thread, hunterthomson, wrote:

This ability to roll-back to a older snapshot will solve 90% of the problems with using Linux.

That was the pitch, better than I ever made it: break your system, then boot a snapshot from before you broke it.

The hook started on AUR, moved to GitHub later, and was carried by community maintainers—Michael Göhler, Dave Reisner—until 2014. The Arch wiki documented the subvolume layout until its mkinitcpio-btrfs page was redirected. My discussions on linux-btrfs—subvolume semantics, atomic replacement, mount behavior, argued while the filesystem’s design was still settling—were the substrate all of this grew out of, right up to carrying the subvolume question a layer higher onto linux-fsdevel (“What to do about subvolumes?”).

The scheme

One rule: never boot the real root. The hook’s on-disk layout is three names:

: ${BTRFS_DIR_ACTIVE:="/__active"}
: ${BTRFS_DIR_SNAPSHOT:="/__snapshot"}
: ${BTRFS_DIR_ROLLBACK:="/__rollback"}

/__active is a snapshot of the installed system and it—not /—is the default boot target. Point-in-time snapshots accumulate under /__snapshot/. And when you pick a snapshot at boot, the hook does not boot it. It snapshots the snapshot to a throwaway /__rollback subvolume and boots that.

That indirection is the whole design. Rollback is always a fresh copy-on-write clone: __active survives, the chosen snapshot survives, and the rollback you’re running is disposable. Creating it does delete any stale __rollback left by an earlier selection. If the rolled-back state turns out to be the one you want, promoting it is a userspace snapshot back onto __active. The protected sources remain intact; the throwaway target is replaced.

Göhler’s rework switched boot selection from rootflags=subvol= to btrfs subvolume set-default, so a choice made at the boot prompt persists without editing the kernel command line—at the cost of an install-message warning once GRUB 2.02 stopped supporting relative paths from set-default subvolumes. Even the packaging was precise about transitions: the install script used vercmp to show the rollback-migration explainer exactly once, to exactly the users crossing the 0.3 boundary whose on-disk layout was about to change.

A wizard in the initramfs

The hook had to get systems into this layout, and the initramfs is the cleanest conversion point: normal userspace and its stateful services have not started, so the root is quiescent enough to explain and move as one system. Btrfs can take a metadata-atomic snapshot of a mounted subvolume too; early boot is not the only point where snapshot creation is atomic. What it buys is a simple application-consistency boundary. The migration wizard therefore lives in busybox ash, gated on a three-second keypress:

read -s -p "Press any key to setup BTRFS rollback support..." -n1 -t3

Pressing a key opened an explanation of the conversion: snapshot / to /__active, create /__snapshot, and leave the original root intact. The wizard asked for confirmation before proceeding. The same three-second keypress window opened rollback selection on subsequent boots.

The kernel follows the root

Rolling back userspace is half a rollback: the kernel on disk inside the chosen subvolume may not be the kernel you’re running. So after a selection, the hook reads the version string embedded in the bzImage itself—the x86 boot protocol keeps a pointer to it at offset 0x20E—compares against uname -rv, and if they differ, copies that subvolume’s kernel and initrd to /tmp, unmounts, and kexecs into them with --append="kexeced" so the next initramfs knows not to loop. Kernel and userspace roll back together. The two-line version peek is its own note: bzimage-peek. Credit where due: the kexec path and version check came from Göhler, the tightened hexdump form from Reisner; the rollback frame they slotted into was mine.

A bootloader from the initramfs generator

An earlier experiment, mkinitcpio-btrfsadm, stopped mid-refactor—the final commit message reads “lots of thought… more enroute.” It exists because of a problem that has since evaporated: at the time, no mainstream bootloader could read btrfs. If your kernel lived inside the filesystem, nothing could load it.

The answer was to repurpose mkinitcpio as an appliance-image compiler. A standalone hook builds an image from scratch—add_dir for a skeleton, add_device for consoles—and then replaces init entirely:

add_file "/lib/initcpio/btrfsadm/init" "/init"

The installed file is a statically-linked i386 ELF: kexec-loader v2.2.2 with kexec-tools 2.0.1 inside. The generated “bootramfs” is a stage-1 bootloader—a kernel plus this image boots from anywhere, mounts the btrfs pool, shows a menu, and kexecs the real kernel living inside btrfs. The install doc explained why it had to be regenerated on every kernel update:

to prevent btrfs lockouts, ie. upstream deciding to use an incompat flag

A filesystem feature flag could leave the bootloader unable to read the kernel.

The unfinished redesign models the btrfs pool with Git structure: a HEAD link naming the boot subvolume, refs/{ro,rw}/... as named pointers, vols/$(date +%s) as the object store of snapshots. Boot operations are specified as ref transitions, including a detached-HEAD boot:

# REF_STATE == detach
#
# Snapshot subvol REF, set HEAD link to SNAPSHOT, bypass REFS.
#
# Creates a temporary/persistent rollback environment. SNAPSHOT will be
# automatically garbage collected should ANY other REF be selected at a
# subsequent boot unless a reference to SNAPSHOT is created under REFS
# while the system is running.

The design applied refs, detached HEAD, and reachability-based garbage collection to a root filesystem. Its executable body was never written; the file retained the working hook alongside the proposed transitions.

What became of it

The hook itself coasted. After the final cleanups the commits stop, and two of the bugs that era left behind are a story of their own: where shell hides bugs. The __active and __snapshot names remained on the Arch wiki until that page was redirected, two years after the code stopped.