Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
edd1868
initrd/bin/unpack_initramfs.sh: fix multi-segment cpio and BusyBox co…
tlaurion May 13, 2026
a41ad75
initrd/bin/kexec-parse-boot.sh, kexec-parse-bls.sh: clean up boot con…
tlaurion May 13, 2026
b2e1685
initrd/bin/kexec-boot.sh: add cmdline length warning and --- separato…
tlaurion May 13, 2026
141ad85
initrd/etc/functions.sh: skip EFI configs in scan_boot_options, add I…
tlaurion May 13, 2026
8929b22
initrd/bin/kexec-iso-init.sh: implement layered ISO boot with initrd …
tlaurion May 13, 2026
cf00ddc
initrd/bin/kexec-select-boot.sh: add boot menu compat markers, displa…
tlaurion May 13, 2026
33d5588
boards/qemu-*-prod_quiet: remove quiet rhgb splash from KERNEL_REMOVE
tlaurion May 13, 2026
062a018
tests/iso-parser/run.sh: add ISO boot tool test harness
tlaurion May 13, 2026
405ddf4
doc: add ISO boot documentation, efifb display chain, BusyBox referen…
tlaurion May 13, 2026
6edae72
initrd: return to ISO selection menu on user cancellation, not recove…
tlaurion May 13, 2026
4dab271
initrd/bin/kexec-iso-init.sh: check for efifb in modules.builtin, fix…
tlaurion May 13, 2026
4d72fb0
initrd/bin/kexec-iso-init.sh: informative display warning for users a…
tlaurion May 13, 2026
6f985f9
initrd/bin/kexec-iso-init.sh: fix grep -E bug causing all display dri…
tlaurion May 13, 2026
e238e23
initrd/bin/kexec-iso-init.sh: fix display driver detection and Xen mo…
tlaurion May 13, 2026
077ab18
doc/busybox_perks.md: add strings, tail, tr sections; fix grep altern…
tlaurion May 13, 2026
58e4b91
doc/busybox_perks.md: correct grep -b not available, add QEMU-verifie…
tlaurion May 13, 2026
d7ce34f
doc/busybox_perks.md: remove line number references from summary table
tlaurion May 13, 2026
e57fa86
initrd/bin/extract-ikconfig: BusyBox-adapted kernel config extractor
tlaurion May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export CONFIG_BOOT_REQ_HASH=n
export CONFIG_BOOT_REQ_ROLLBACK=n
export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0"
export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0"
export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash"
export CONFIG_BOOT_KERNEL_REMOVE=""
export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet"
#export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export CONFIG_BOOT_REQ_HASH=n
export CONFIG_BOOT_REQ_ROLLBACK=n
export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0"
export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0"
export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash"
export CONFIG_BOOT_KERNEL_REMOVE=""
export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm1-prod_quiet"
#export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export CONFIG_BOOT_REQ_HASH=n
export CONFIG_BOOT_REQ_ROLLBACK=n
export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0"
export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0"
export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash"
export CONFIG_BOOT_KERNEL_REMOVE=""
export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet"
#export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export CONFIG_BOOT_REQ_HASH=n
export CONFIG_BOOT_REQ_ROLLBACK=n
export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0"
export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0"
export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash"
export CONFIG_BOOT_KERNEL_REMOVE=""
export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm2-prod_quiet"
#export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal"

Expand Down
128 changes: 108 additions & 20 deletions doc/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ Linux kernel, and a security-focused initrd. It establishes a hardware root of t
measured boot via TPM, and verifies the OS boot environment before handing off control.

See also: [security-model.md](security-model.md), [boot-process.md](boot-process.md),
[tpm.md](tpm.md) detailed subsystem documentation.
[tpm.md](tpm.md): detailed subsystem documentation.

External reference: [deepwiki.com/linuxboot/heads](https://deepwiki.com/linuxboot/heads)
External reference: [deepwiki.com/linuxboot/heads](https://deepwiki.com/linuxboot/heads)
validated against code in this repository.

---
Expand All @@ -19,7 +19,7 @@ validated against code in this repository.
│ SPI Flash ROM │
│ ┌──────────────┐ ┌───────────────┐ ┌──────────┐ │
│ │ coreboot │ │ Linux kernel │ │ initrd │ │
│ │ (HW init + │ │ (minimal, │ │ (boot │ │
│ │ (HW init + │-> │ (minimal, │-> │ (boot │ │
│ │ PCR 2 SRTM) │ │ no initramfs)│ │ scripts)│ │
│ └──────────────┘ └───────────────┘ └──────────┘ │
└─────────────────────────────────────────────────────┘
Expand All @@ -31,13 +31,13 @@ validated against code in this repository.
### coreboot

Replaces vendor firmware. Performs hardware initialization (memory training, PCIe, USB),
extends PCR 2 in the TPM with each firmware stage (bootblock romstage ramstage
extends PCR 2 in the TPM with each firmware stage (bootblock -> romstage -> ramstage ->
payload) as the Static Root of Trust for Measurement (SRTM), and launches the kernel
directly without a second-stage bootloader.

### Linux kernel (payload)

A minimal, stripped kernel compiled specifically for Heads. No initramfs it boots directly
A minimal, stripped kernel compiled specifically for Heads. No initramfs: it boots directly
into the Heads initrd. Provides device drivers (TPM, USB, storage, network), filesystem
support, and the platform for the boot scripts.

Expand Down Expand Up @@ -70,9 +70,9 @@ execution. Source lives in `initrd/`.

Three-layer hierarchy:

1. **`/etc/config`** Board defaults compiled into the ROM at build time
2. **`/etc/config.user`** User overrides extracted from CBFS at runtime
3. **`/tmp/config`** Combined result, sourced during boot
1. **`/etc/config`**: Board defaults compiled into the ROM at build time
2. **`/etc/config.user`**: User overrides extracted from CBFS at runtime
3. **`/tmp/config`**: Combined result, sourced during boot

`combine_configs()` in `initrd/etc/functions.sh` merges these by concatenating
`/etc/config*` into `/tmp/config`. User settings in CBFS take precedence
Expand All @@ -87,14 +87,14 @@ Changes to user configuration are persisted by reflashing the ROM (CBFS operatio
The top-level `Makefile` orchestrates:

- Cross-compiler (`musl-cross-make`, target: `x86_64-linux-musl` or `powerpc64le-linux-musl`)
- Modules (coreboot, Linux, busybox, GPG, cryptsetup, kexec, LVM2, )
- Modules (coreboot, Linux, busybox, GPG, cryptsetup, kexec, LVM2, zstd, ...: see [modules.md](modules.md) for the full list)
- Six CPIO archives assembled into the initrd:
1. `dev.cpio` device nodes
2. `modules.cpio` kernel modules
3. `tools.cpio` userspace tools + configuration
4. `board.cpio` board-specific scripts
5. `heads.cpio` security scripts (`CONFIG_HEADS=y`)
6. `data.cpio` data files
1. `dev.cpio`: device nodes
2. `modules.cpio`: kernel modules
3. `tools.cpio`: userspace tools + configuration
4. `board.cpio`: board-specific scripts
5. `heads.cpio`: security scripts (`CONFIG_HEADS=y`)
6. `data.cpio`: data files
- Final ROM image: coreboot ROM with Linux + initrd payload embedded

Reproducible builds are achieved via Nix-pinned Docker images. See [docker.md](docker.md).
Expand All @@ -114,8 +114,96 @@ The CI pipeline's workspace and cache behavior is documented in

## Key design principles

- **No network at boot** — all verification is local; no certificate authorities
- **Hardware root of trust** — coreboot in SPI flash is the trust anchor; coreboot extends measurements into the TPM
- **Fail-closed** — failed verification drops to authenticated recovery shell, not an unverified OS boot
- **Separation of duties** — the key that signs `/boot` lives on a hardware security dongle, never in the ROM
- **Auditability** — all source is open, builds are reproducible, ROM images are verifiable
- **No network at boot**: all verification is local; no certificate authorities
- **Hardware root of trust**: coreboot in SPI flash is the trust anchor; coreboot extends measurements into the TPM
- **Fail-closed**: failed verification drops to authenticated recovery shell, not an unverified OS boot
- **Separation of duties**: the key that signs `/boot` lives on a hardware security dongle, never in the ROM
- **Auditability**: all source is open, builds are reproducible, ROM images are verifiable

---

## Display output after kexec

```
kexec boundary
┌──────────────────────────────────────────────────┐
│ DISPLAY CONTROLLER │
│ (initialised by coreboot, continuously scans │
│ out from framebuffer at physical address X) │
└──────────────────────┬───────────────────────────┘
│ reads pixels from
┌────────────────┐
│ scanout buf │
│ @ phys addr X │
└────────────────┘
───────────────────────┼──────────────────────────────
Heads side │ OS side
┌──────────────────────────┐ ┌───────────────────────────────┐
│ coreboot/libgfxinit │ │ target kernel (after kexec) │
│ - initialises display │ │ sysfb_init() reads boot │
│ - sets VIDEO_TYPE_EFI │ │ params -> VIDEO_TYPE_EFI │
│ in screen_info │ │ -> "efi-framebuffer" device │
└────────────┬─────────────┘ │ -> efifb binds │
│ │ BUT: lfb_base == 0 │
▼ │ -> efifb_probe fails │
┌──────────────────────────┐ │ -> no /dev/fb0 │
│ Heads kernel │ │ -> initramfs display blank │
│ sysfb_init() -> sees │ └───────────────────────────────┘
│ VIDEO_TYPE_EFI │
│ -> "efi-framebuffer" │ ┌───────────────────────────────┐
│ -> efifb binds │ │ DRM driver (post-switchroot) │
│ -> Heads GUI visible │ │ i915 reinitialises display │
└────────────┬─────────────┘ │ -> framebuffer works again │
│ └───────────────────────────────┘
┌──────────────────────────┐
│ kexec-tools │
│ opens /dev/fb0 -> reads │
│ fb parameters │
│ fix.id = "EFI VGA" ✓ │
│ smem_start = 0 ✗ │
│ (kernel hid it) │
│ writes to new kernel │
│ boot params: │
│ orig_video_isVGA = EFI │
│ lfb_base = 0 │
└──────────────────────────┘
```

Heads' initrd runs under a kernel compiled with `CONFIG_FB_EFI=y`.
coreboot/libgfxinit initialises the display hardware (PLL, timings,
scanout buffer) and presents it to Linux as an efifb-compatible
framebuffer (`screen_info` set to `VIDEO_TYPE_EFI`). Heads' kernel
binds efifb, maps the scanout buffer, and the Heads console and
whiptail GUI are visible. Without this, Heads would have no
framebuffer at all.

After kexec, the display hardware remains in its initialised state
(kexec does not reset devices: the controller keeps scanning out
from the same physical address). The target kernel must also have
`CONFIG_FB_EFI=y` to adopt the same framebuffer: simplefb and
simpledrm are not compatible because the scanout buffer was set up
as an efifb-compatible buffer by coreboot, not as a simplefb buffer.

**Known limitation: lost framebuffer address:**

Linux no longer exposes the physical framebuffer address to userspace.
kexec-tools obtains `smem_start = 0` from `FBIOGET_FSCREENINFO` and
writes `lfb_base = 0` into the new kernel's boot params. The target
kernel's efifb sees a zero address and cannot map the framebuffer
the display stays blank even though the controller is still scanning
out the correct memory (set up by coreboot, preserved through kexec).

Historically, Heads worked around this by using i915 with
`CONFIG_DRM_FBDEV_LEAK_PHYS_SMEM=y` (leaking the physical address),
but that was suboptimal. The current efifb approach is also
suboptimal: we are waiting for improvements in coreboot, Linux, and
kexec-tools to properly convey the framebuffer state across kexec.

**TPM Disk Unlock Key** works around this for encrypted disks by
injecting the LUKS key before kexec: the initramfs never prompts for
a passphrase on a blank display. Serial console is unaffected.
97 changes: 96 additions & 1 deletion doc/boot-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,101 @@ menu, system info, power off.

---

## Stage 2b: USB ISO Boot (`kexec-iso-init.sh`)

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.

### Flow

1. **Signature verification**: Check for `.sig` or `.asc` detached signature
2. **Mount ISO**: Mount the ISO file as loopback device at `/boot`
3. **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 `.ko` files and
`modules.builtin`
- Each initrd gets its own independent `[OK]` / `[!]` / (blank) marker in
`/tmp/kexec_initrd_compat.txt` (the per-initrd flag `initrd_supports_fs` is tracked
separately from the global `any_supported` flag, so no initrd is silently
skipped)
- `[OK]` = initrd has the needed module as `.ko`, has it in
`modules.builtin`, or has no `.ko` files 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.
4. **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.
5. **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 dracut
- `findiso=/$ISO_PATH`: Debian live-boot, NixOS stage-1
- `img_dev=/dev/disk/by-uuid/$DEV_UUID`: block device containing the ISO
- `img_loop=$ISO_PATH`: loopback file path (relative)
- `iso=$DEV_UUID/$ISO_PATH`: UUID/path alternative
- `live-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 with `findiso=`
in Debian live-boot's `check_dev()`: `fromiso` mounts the ISO, then `findiso`
looks 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 (`/live` for Debian, `/casper` for Ubuntu/PureOS, `/LiveOS` for
Fedora); leaving it unset lets each distro use its own default.
6. **Layer 3: kexec-select-boot**: Launch the standard boot menu with `-u`
(unique entries, dedup sorted by name).

### Initrd compatibility markers in the boot menu

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.

### Compatibility note for ext4 and vfat

Initrds with no `.ko` files at all get no marker at all (blank): we can't
verify either way, so nothing is displayed.

### Boot param injection

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) |

---

## Stage 3: kexec-select-boot

Called from the boot menu. Responsible for final verification and OS handoff.
Expand All @@ -136,7 +231,7 @@ 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.
invalid signature causes `die`: there is no "boot anyway" path.

Optionally, root partition hashes are also checked if `CONFIG_ROOT_CHECK_AT_BOOT=y`.

Expand Down
Loading