This document describes the complete boot flow from power-on to OS handoff.
See also: architecture.md for component overview, tpm.md for TPM PCR details, security-model.md for the trust model.
Power-on
│
▼
coreboot (SPI flash)
│ hardware init, SRTM measurement into PCR 2
▼
Linux kernel (coreboot payload, no initramfs)
│
▼
/init ← first userspace process
│ mount filesystems, load config, combine user overrides
▼
/bin/gui-init ← main interactive boot loop
│ TPM preflight, GPG key check, TOTP/HOTP attestation
▼
kexec-select-boot
│ verify /boot hashes + GPG signature, rollback counter
▼
kexec ← hands off to OS kernel
/init is the first userspace process. It:
- Mounts virtual filesystems (
/dev,/proc,/sys). - Loads board defaults from
/etc/configand the functions library. - Runs
cbfs-initto extract user configuration from CBFS into/etc/config.user. - Calls
combine_configs()to merge all/etc/config*files into/tmp/config, then sources/tmp/configso all subsequent scripts see the merged settings. - If
CONFIG_BOOT_RECOVERY_SERIALis set, starts a backgroundpause_recoverypath on that serial TTY (/dev/ttyS*) that waits for Enter and then launches the recovery shell there. - Checks for a quick
rkeypress (100 ms timeout) to drop to a recovery shell before any GUI starts. - Starts
cttyhack $CONFIG_BOOTSCRIPT(default:/bin/gui-init) under a PID 1 respawn loop, so the boot script is relaunched if it exits unexpectedly while init stays alive.
/etc/config (ROM, board defaults)
/etc/config.user (CBFS, user overrides)
│
└─► combine_configs() ─► /tmp/config (runtime, sourced by all scripts)
User settings appear last in the concatenation and therefore override board defaults. Changes are persisted by reflashing CBFS.
gui-init is the main interactive boot agent. It runs as an infinite loop and
handles all user interaction until the OS is handed off via kexec.
On startup, gui-init detects the controlling TTY (set by cttyhack in /init)
and exports it as HEADS_TTY and GPG_TTY. This ensures that all interactive
prompts and GPG operations reach the correct terminal regardless of stdout/stderr
redirections.
Before showing any menu, gui-init verifies that the TPM rollback counter is
consistent with /boot/kexec_rollback.txt. An inconsistency indicates either a
TPM reset (expected: user must re-seal secrets) or an unexpected state (possible
tampering). On failure, the main menu background is set to error color and the
user is offered recovery options.
gui-init counts the keys in the GPG keyring. An empty keyring means no /boot
signature can be verified. The user must add a key or perform OEM Factory Reset
before booting.
unseal-totp retrieves the TOTP secret from TPM NVRAM and generates the current
30-second code. If the unseal fails (PCR mismatch, TPM reset, tampered firmware),
INTEGRITY_GATE_REQUIRED is set to y, which blocks all subsequent TPM secret
sealing until an integrity check passes. See security-model.md.
If a hardware HOTP token is present (/bin/hotp_verification), gui-init obtains
the HOTP secret (unsealed from TPM on boards with a TPM; derived from a ROM hash on
boards without one) and asks the token to verify the current code. Result codes:
0 = success, 4 = wrong code, 7 = not a valid HOTP value.
See security-model.md
for the no-TPM path.
If HOTP succeeded and CONFIG_AUTO_BOOT_TIMEOUT is set, a countdown starts and
the default boot entry is selected automatically if the user does not intervene.
show_main_menu displays the current date, TOTP code, and HOTP status in the
menu title bar. The background color reflects the current integrity state
(normal / warning / error). Options: default boot, refresh TOTP/HOTP, options
menu, system info, power off.
When booting from an ISO file on USB media, kexec-iso-init.sh handles the ISO
boot flow. It is invoked from the "USB ISO Boot" option in the main menu.
- Signature verification: Check for
.sigor.ascdetached signature - Mount ISO: Mount the ISO file as loopback device at
/boot - Layer 1: initramfs fs compatibility check (
check_initrd_compat): Before presenting boot options, verify the ISO's initramfs contains kernel modules for the USB partition's filesystem (ext4/vfat/exfat). If the initrd can't read the USB filesystem, the kernel won't find the ISO after kexec. Also checks for a framebuffer driver (efifb, bochs) needed for display after kexec.- Parsing boot configs for initrd paths (instead of searching the whole ISO)
- Unpacking each initrd and checking for required
.kofiles andmodules.builtin - Each initrd gets its own independent
[OK]/[!]/ (blank) marker in/tmp/kexec_initrd_compat.txt(the per-initrd flaginitrd_supports_fsis tracked separately from the globalany_supportedflag, so no initrd is silently skipped) [OK]= initrd has the needed module as.ko, has it inmodules.builtin, or has no.kofiles at all (minimal initrd with everything built into the kernel: nothing to check against).[!]= initrd has loadable kernel modules but none for the USB filesystem type. No built-in assumption: we report what we find.- Read-only filesystems (iso9660/squashfs/udf) and unmapped fstypes skip
- All initrds are checked (no early break) so the compat file is complete.
- Framebuffer results are written to
/tmp/kexec_fb_compat.txt. A separate warning is shown if no initrd has a known fb driver.
- Layer 2: loopback.cfg fast path: If the ISO has a
loopback.cfg, parse it and resolve GRUB variables (${iso_path},${isofile}) to extract the ISO kernel params from loopback entries. - Boot param injection: When Layer 2 resolves nothing (no GRUB vars found
in loopback.cfg), all common ISO boot methods are injected unconditionally
as kernel ADD params so the ISO initrd can pick whichever it supports:
iso-scan/filename=/$ISO_PATH: Ubuntu casper, Fedora dracutfindiso=/$ISO_PATH: Debian live-boot, NixOS stage-1img_dev=/dev/disk/by-uuid/$DEV_UUID: block device containing the ISOimg_loop=$ISO_PATH: loopback file path (relative)iso=$DEV_UUID/$ISO_PATH: UUID/path alternativelive-media=/dev/disk/by-uuid/$DEV_UUID: device filter (casper, live-boot) The kernel ignores parameters it doesn't understand.fromiso=is intentionally not injected because it conflicts withfindiso=in Debian live-boot'scheck_dev():fromisomounts the ISO, thenfindisolooks for the ISO file inside the mounted ISO (not found), unmounts it, leaving orphaned loop devices that get re-scanned -> infinite loop.findiso=alone covers Debian and NixOS.live-media-path=is intentionally not injected because the default differs per distro (/livefor Debian,/casperfor Ubuntu/PureOS,/LiveOSfor Fedora); leaving it unset lets each distro use its own default.
- Layer 3: kexec-select-boot: Launch the standard boot menu with
-u(unique entries, dedup sorted by name).
During Layer 1, check_initrd_compat writes per-initrd results to
/tmp/kexec_initrd_compat.txt. kexec-select-boot reads this file and shows
[OK] or [!] at the start of each menu line (before the entry name):
| Marker | Meaning | Behavior |
|---|---|---|
[OK] |
Initrd has the USB fs module (as .ko or modules.builtin) | Boot should work |
[!] |
Initrd has loadable modules but none for the USB fs type | May fail after kexec |
| (blank) | Initrd has zero .ko files: can't verify either way | Assume OK (minimal initrd) |
| (none) | Entry has no initrd (memtest, etc.) | No filesystem dependency |
A NOTE (3-second sleep, cannot scroll past) is displayed before the menu
explaining the legend. Markers follow doc/logging.md accessibility rules:
text-based, serial-safe, not color-dependent.
Initrds with no .ko files at all get no marker at all (blank): we can't
verify either way, so nothing is displayed.
When Layer 2 (loopback.cfg) resolves no GRUB variables, the following parameters are injected unconditionally so the ISO initrd can find the USB partition and the ISO file after kexec, regardless of which distribution's init system it uses:
| Parameter | Example | Used by |
|---|---|---|
iso-scan/filename= |
/ISOs/foo.iso |
Ubuntu casper, Fedora dracut |
findiso= |
/ISOs/foo.iso |
Debian live-boot, NixOS stage-1 |
img_dev= |
/dev/disk/by-uuid/UUID |
Block device hint |
img_loop= |
ISOs/foo.iso |
Loopback path |
iso= |
UUID/ISOs/foo.iso |
Alternative path |
live-media= |
/dev/disk/by-uuid/UUID |
Device filter (casper, live-boot) |
Called from the boot menu. Responsible for final verification and OS handoff.
For TPM2 systems, verifies the SHA-256 hash of the TPM2 primary key handle
against /boot/kexec_primhdl_hash.txt (if the file exists). A mismatch means
the TPM2 primary key was regenerated without updating the stored hash.
verify_checksums checks the SHA-256 of every /boot file against
kexec_hashes.txt, then verifies kexec.sig with gpgv. A hash mismatch or
invalid signature causes die: there is no "boot anyway" path.
Optionally, root partition hashes are also checked if CONFIG_ROOT_CHECK_AT_BOOT=y.
The TPM monotonic counter index is read from /boot/kexec_rollback.txt and the
counter is read from the TPM. The SHA-256 of the counter file is then checked
against the hash stored in kexec_rollback.txt. Any discrepancy aborts the boot.
If a TPM-sealed LUKS Disk Unlock Key (DUK) is configured, kexec-insert-key
unseals the DUK and injects it into a minimal initrd prepended to the OS initrd.
The OS kernel then finds the key and unlocks LUKS without prompting the user.
kexec-boot performs the final kexec system call to hand off to the OS kernel.
The recovery shell is an authenticated environment. Entering it extends TPM
PCR 4 with "recovery", permanently invalidating TOTP/HOTP/LUKS unseal for
the rest of the boot session. See tpm.md.