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.
Merges board defaults, user overrides, and CBFS config into /tmp/config.
See architecture.md#configuration-system.
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. Invoked from Options → Boot Options → "USB boot".
The ISO boot flow consists of 7 steps, with branching after step 3:
Step 1: Signature verification ──FAIL→ abort
Step 2: Mount ISO
Step 3: loopback.cfg?
├── FOUND → Step 4: Fast-path gate
│ ├── Verify ISO compatibility → Step 5 → Step 6 → Step 7 (menu)
│ └── Boot ISO now → Step 6 → Step 7 (menu)
└── NOT FOUND → Step 4: Probing gate
├── Verify ISO compatibility → Step 5 → Step 6 → Step 7 (menu)
├── Boot ISO now → Step 6 → Step 7 (menu)
└── Cancel → back to ISO selection
-
Signature verification: Check for
.sigor.ascdetached signature. Unsigned ISOs prompt the user before proceeding. -
Mount ISO: Mount the ISO file as a loopback device at
/boot. -
loopback.cfg: Read
boot/grub/loopback.cfgorboot/grub2/loopback.cfg— a ~2 KB file check vs. unpacking the entire initramfs (200+ MB). Three outcomes:-
Found + GRUB vars present (
${iso_path}or${isofile}inloopback.cfg): Variables are stripped — unresolved references would be passed literally to the kernel (security risk). Universal ADD params provide absolute ISO paths. Fast-path gate offers "Verify ISO compatibility" or "Boot ISO now." Only Ubuntu 26.04 uses GRUB vars in its loopback.cfg. -
Found + no GRUB vars (most ISOs — Debian, Fedora, Tails, Kicksecure, NixOS, openSUSE KDE Live): Fast-path gate with two options:
- Verify ISO compatibility (recommended) — run step 5 (initramfs + kernel scan, ~30-60s), then boot menu with markers.
- Boot ISO now — skip step 5, go directly to boot menu. Entries appear without compatibility markers (unverified). Universal fallback ADD params handle ISO-finding kernel parameters.
-
Not found: Skip to the probing gate below. No
loopback.cfgcould be detected on this ISO.
-
-
Probing gate (only when step 3 found nothing): When the ISO has no
loopback.cfg, a three-option whiptail dialog asks how to proceed before the expensive step 5 scan:- Verify ISO compatibility — run step 5 (unpack + kernel scan, ~30-60s).
- Boot ISO now — skip step 5, boot menu with no markers.
- Cancel — return to ISO file browser.
-
Initramfs compatibility check (
check_initramfs_compat, runs when user chose "Verify ISO compatibility" at either gate): Verify the ISO's initramfs contains kernel modules for the USB partition's filesystem (ext4/vfat/exfat). If the initramfs cannot read the USB filesystem, the kernel won't find the ISO after kexec. Display driver detection runs against the decompressed kernel — checks for built-in vesafb, vesadrm, or simpledrm symbols via _check_kernel_probe_driver().- Parsing boot configs for initramfs paths (instead of searching the whole ISO)
- Unpacking each initramfs and checking for required
.kofiles andmodules.builtin - Each initramfs gets its own independent
[OK]/[!]/ (blank) fs-compat marker and[OK]:graphics (<driver>)/[~]:drm/[!]display marker. - Per-pair kernel check: Each (kernel, initramfs) pair is checked
independently. A kernel with built-in vesafb/vesadrm/simpledrm has its
display marker upgraded from
[~]:drmto[OK]:graphics (<driver>). This correctly handles ISOs where one kernel variant has a display driver and another doesn't. - Read-only filesystems (iso9660/squashfs/udf) and unmapped fstypes skip.
- All initramfs archives are checked (no early break) so the compat file is complete.
- A filesystem warning is shown if no initramfs has the USB fs module.
- A display warning is shown if no kernel has a display driver.
-
Boot param injection: All common ISO boot methods are unconditionally injected as kernel ADD params — the kernel passes all of them to userspace via /proc/cmdline. The ISO's initramfs scripts (casper, live-boot, dracut) recognize only the parameters they need and ignore the rest. When loopback.cfg contains GRUB variable references, those are stripped (removed via
_strip_grub_vars()) — the universal combined with the universal fallback.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.
-
kexec-select-boot: Launch the boot menu. Entries show markers when step 5 ran, or no markers when step 5 was bypassed.
An EXIT trap is installed early in the script. On any exit (success, error,
or user cancel) the ISO loopback mount is unmounted from /boot and temporary
compatibility files are cleaned up. This prevents stale mounts if the user
returns to the ISO browser and selects a different ISO.
During step 5 (initramfs compat), check_initramfs_compat writes per-initramfs
results to /tmp/kexec_initrd_compat.txt (filesystem module check) and
/tmp/kexec_display_driver.txt (kernel display driver check via symbol
detection — the kernel's built-in driver binds before initramfs runs).
kexec-select-boot combines these two signals into three-state menu
markers shown before each entry name:
| Marker | Meaning | User experience |
|---|---|---|
[OK] |
USB filesystem module found and kernel display driver confirmed | Boots with continuous or quickly-restored display |
[~] |
Display driver not found in kernel ([~]:drm), or USB fs missing |
Boots with caveat — brief blank or degraded |
[X] |
No display driver, initramfs module missing, or unknown marker | Screen stays blank until native driver loads |
| (blank) | Initramfs has no .ko files: can't verify | Assume OK (minimal initramfs) |
When the user chose "Boot ISO now" at either gate (fast-path or probing), step 5 was bypassed and no compat files exist: all entries appear without a marker prefix. A STATUS line explains why.
A STATUS line is displayed before the menu.
When step 5 ran, it shows the [OK]/[~] legend describing the markers.
When step 5 was skipped, it shows
"Compatibility not checked -- entries may still work" instead.
Markers follow doc/logging.md accessibility rules: text-based,
serial-safe, not color-dependent.
Initramfs archives with no .ko files at all get no marker at all (blank): we can't
verify either way, so nothing is displayed.
Universal ADD params are always injected via _build_universal_add().
For the full parameter reference, fromiso= conflict rationale, and
per-distro requirements, see iso_boot.md.
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. On mismatch,
an interactive whiptail menu offers options: investigate discrepancies,
update checksums, or return to the main menu.
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 initramfs prepended to the OS initramfs.
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.
For details on how the kernel command line is assembled from boot entry params,
ADD params, and Board ADD, see kexec_handoff.md.
Just before kexec, if CONFIG_FINALIZE_PLATFORM_LOCKING=y, lock_chip.sh triggers
a chipset-level PR0 lockdown via SMI (io386 writes to port 0xb2), setting
FLOCKDN in the SPI controller. Once locked, the protected flash region becomes
read-only until the next system reset — the OS cannot modify the firmware. See
wp-notes.md.
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.