Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
93 changes: 93 additions & 0 deletions doc/boot-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,99 @@ 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:

1. **Signature verification**: Check for `.sig` or `.asc` detached signature
2. **Hybrid detection**: Check MBR signature at offset 510 (0x55AA = hybrid)
3. **Mount ISO**: Mount the ISO file as loopback device
4. **Initrd scanning**: Unpack ISO initrd and scan for filesystem support
(ext4, vfat, exfat modules) and boot method support (iso-scan, findiso,
live-media, boot=live, boot=casper, nixos, anaconda)
5. **Config scanning**: Grep all `*.cfg` files in the mounted ISO for boot
params as a fallback when initrd detection fails (covers GRUB, syslinux,
ISOLINUX configs)
6. **Warning dialog**: If no supported boot method is detected, warn the user
and suggest alternative USB creation methods

### Boot methods

ISOs use different initramfs boot systems. Detection checks for known patterns:

| Boot system | Detection patterns | Notes |
|------------|---------------------|-------|
| Dracut (iso-scan) | `iso-scan/filename=`, `findiso=` | Ubuntu, Debian Live, Tails, PureOS |
| Dracut (live-media) | `live-media=` | Tails |
| Dracut (boot=live) | `boot=live`, `rd.live.image`, `rd.live.squashimg=` | Debian Live, Fedora Workstation, Kicksecure |
| Dracut (casper) | `boot=casper` | Ubuntu, PureOS |
| NixOS | `nixos` | NixOS |
| Anaconda | `inst.stage2=`, `inst.repo=` | Fedora, Qubes OS — requires block device (CD-ROM or dd'd USB) |
| Unknown | (no pattern matched) | May still work — try anyway |

### ISO filesystem support

The ISO initrd must support the USB stick filesystem. Detection unpacks the ISO
initrd and looks for kernel module files (ext4.ko, vfat.ko, exfat.ko) to
determine if the USB fs is supported.

Known supported filesystems: **ext4**, **vfat**, **exfat** (detected in kernel module paths).

### Boot parameter flow

1. `kexec-iso-init.sh` passes standard boot params via kexec:
- `iso-scan/filename=/${ISO_PATH}` — Dracut standard
- `fromiso=`, `img_loop=`, `img_dev=` — additional Dracut variants
2. `kexec-select-boot.sh` parses the ISO's GRUB/syslinux config to build the
boot menu
3. `kexec-parse-boot.sh` strips unresolved `${iso_path}` variables from parsed
entries (prevents malformed params like `iso-scan/filename=` with orphaned paths)
4. `kexec-boot.sh` adds parsed entries and executes kexec

### Known compatible ISOs (tested 2026-04)

| Distribution | MBR | Boot method | Config source | USB FS | Status |
|---|---|---|---|---|---|
| Ubuntu Desktop | hybrid | iso-scan/filename | grub.cfg, isolinux/*.cfg | ext4/vfat/exfat | works |
| Debian Live kde/xfce | hybrid | boot=live, rd.live.image | grub.cfg, isolinux/*.cfg | ext4/vfat/exfat | works |
| Tails 7.6 | hybrid | boot=live | grub.cfg, isolinux/*.cfg | ext4/vfat | works |
| Tails (exfat-support) | hybrid | boot=live | grub.cfg, isolinux/*.cfg | exfat | works |
| Fedora Workstation Live | hybrid | boot=live, rd.live.image | grub.cfg, isolinux/*.cfg | ext4/vfat | works |
| NixOS | hybrid | findiso, nixos | grub.cfg, isolinux/*.cfg | ext4/vfat/exfat | works |
| PureOS | hybrid | boot=casper | grub.cfg, isolinux/*.cfg | ext4/vfat/exfat | works |
| Kicksecure | hybrid | boot=live, rd.live.image | grub.cfg | ext4/vfat/exfat | works |

### Known limited ISOs

| Distribution | Boot method | Limitation |
|---|---|---|
| Fedora Silverblue | anaconda (inst.stage2=) | Requires block device or matching LABEL. Not USB file boot without extra config. |
| Qubes OS R4.3 | anaconda (inst.repo=hd:LABEL=) | Requires block device or matching LABEL. Installer only. |
| Debian DVD | none (installer) | No live boot params — installer ISO only. Use netinst or dd. |
| TinyCore/CorePlus | unknown (cde, iso=) | Boot method not detected. May work but unverified. |

### On unknown boot methods

If no known boot method is detected, the boot still proceeds with a warning.
Some ISOs use custom boot mechanisms not covered by detection patterns. Examples:

- **TinyCore/CorePlus**: Uses `cde` (from CD) and `iso=` kernel parameter.
The `fromISOfile` script mounts ISO as `/mnt/cdrom`. May work despite
no detection pattern match.

The detection approach is best-effort. Users with unsupported ISOs should:
- Try Ventoy, Rufus, or distribution USB creation tools
- Report to upstream that the ISO should support USB file boot
- Use `dd` to write ISO directly to USB if all else fails

### References

- [GRUB2 loopback ISO boot](https://a1ive.github.io/grub2_loopback.html)
- [Arch Linux ISO Boot](https://wiki.archlinux.org/title/ISO_Spring_(%27Loop%27_device))
- [Debian USB creation](https://wiki.debian.org/DebianInstaller/CreateUSBMedia)

---

## Stage 3: kexec-select-boot

Called from the boot menu. Responsible for final verification and OS handoff.
Expand Down
104 changes: 104 additions & 0 deletions doc/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,107 @@ When touching the Makefile or build system:

See [ux-patterns.md](ux-patterns.md) for `INPUT`, `STATUS`/`STATUS_OK`,
`DO_WITH_DEBUG`, `HEADS_TTY` routing, and PIN caching conventions.

## Testing ISO Boot Logic from Host

ISO boot scripts (`kexec-iso-init.sh`, `kexec-parse-boot.sh`, `kexec-select-boot.sh`)
can be tested directly against mounted ISOs without building or running QEMU.

### Heads Runtime Environment

Heads runtime uses:

- **Busybox** (unconditional) — coreutils (ls, cp, mv, dd, find, grep, sed, awk, etc.)
- **Bash** (`CONFIG_BASH=y` by default) — full bash for scripting
- **Shell shebang** — `#!/bin/bash` in scripts (bash is always available)
- **Tools** — kexec, blkid, cpio, xz, zstd, gzip for ISO boot handling

See `config/busybox.config` for busybox features and `boards/*/` for module selection.

### Mount ISO and Test

```bash
# Mount an ISO (fuseiso works without root)
mkdir -p /tmp/iso-test/kicksecure
fuseiso -p /path/to/Kicksecure-LXQt-18.1.4.2.Intel_AMD64.iso /tmp/iso-test/kicksecure

# Test initrd path extraction from GRUB configs
bootdir="/tmp/iso-test/kicksecure"
for cfg in $(find "$bootdir" -name '*.cfg' -type f 2>/dev/null); do
grep -E "^[ ]*initrd[ ]" "$cfg" | while read line; do
echo "$line" | awk '{for(i=1;i<=NF;i++) if($i=="initrd") print $(i+1)}'
done
done

# Test initramfs unpacking
bash initrd/bin/unpack_initramfs.sh \
/tmp/iso-test/kicksecure/live/initrd.img-6.12.69+deb13-amd64 \
/tmp/initrd-unpacked

# Test GRUB config parsing (kexec-parse-boot.sh logic)
bootdir="/tmp/iso-test/kicksecure"
for cfg in $(find "$bootdir" -name '*.cfg' -type f 2>/dev/null); do
bash initrd/bin/kexec-parse-boot.sh "$bootdir" "$cfg"
done

# Cleanup
fusermount -u /tmp/iso-test/kicksecure
```

### Key Differences from Heads Runtime

| Aspect | Heads Runtime | Host Testing |
|--------|-------------|--------------|
| Root filesystem | Read-only initramfs | Full system |
| `/boot` mount | FUSE/loopback of ISO | Direct ISO mount |
| `blkid` output | ISO9660 with UUID | Same |
| Device paths | `/dev/sda` etc | Same |
| `unpack_initramfs.sh` | Works the same | Works the same |
| Bash | Full bash available | Same |
| Busybox awk | Limited regex (no `[[:space:]]`) | Use `[ \t]` instead |
| TPM/PCR | N/A | N/A |
| GPG keys | Different | Different |

### What Can Be Tested

- ✅ GRUB/ISOLINUX config parsing (`kexec-parse-boot.sh`)
- ✅ Initrd path extraction from configs
- ✅ Initramfs unpacking and module scanning
- ✅ Boot method detection (boot=live, casper, etc.)
- ✅ Path handling (`/boot` prefix stripping)
- ⚠️ Combined boot params (injected params tested conceptually, not end-to-end)
- ❌ Actual `kexec` kernel loading
- ❌ TPM PCR extending
- ❌ Whiptail/GUI dialogs
- ❌ FUSE mount behavior inside initrd

### Test Suite: `tests/iso-parser/run.sh`

The test suite validates ISO boot compatibility:

```bash
cd tests/iso-parser
./run.sh # test all ISOs
./run.sh /path/to/iso.iso # test single ISO
```

Output shows:
- **First section**: ISO metadata (entries count, hybrid MBR, sample boot params)
- **Second section**: Initramfs boot support detection (mechanisms found, compatibility)

Compatibility status:
- **OK**: Known boot mechanism detected → should work via kexec-ISO-boot
- **WARN**: No known mechanism detected → may work but unverified
- **SKIP**: Installer ISO → use dd/Ventoy instead

The test scans both:
1. **Initrd content** (primary): Unpacks initrd and searches for boot mechanisms
2. **Config files** (fallback): Greps *.cfg for known boot params

Runtime injection (not tested):
- `findiso=`, `fromiso=`, `iso-scan/filename=`, `img_dev=`, `img_loop=`
- `live-media=`, `boot=live`, `boot=casper`

These are injected unconditionally by `kexec-iso-init.sh` and combined with
parsed params in `kexec-boot.sh`. Duplicates resolve naturally (kernel uses
last value).
81 changes: 58 additions & 23 deletions initrd/bin/kexec-boot.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
#!/bin/bash
# Launches kexec from saved configuration entries
# Execute kexec to boot an OS kernel from parsed boot configuration
#
# This script takes a boot entry (from kexec-parse-boot.sh) and executes
# kexec to load and boot the OS kernel. It handles:
# - ELF kernels (standard Linux)
# - Multiboot kernels (Xen)
# - Initial ramdisks (initrd)
# - Kernel command line modification (add/remove parameters)
#
# Options:
# -b Boot directory (e.g., /boot)
# -e Entry string (name|kexectype|kernel path[|initrd][|append])
# -r Parameters to remove from cmdline
# -a Parameters to add to cmdline
# -o Override initrd path
# -f Dry run: print files only
# -i Dry run: print initrd only
#
set -e -o pipefail
. /tmp/config
. /etc/functions.sh
Expand All @@ -11,13 +28,19 @@ printfiles="n"
printinitrd="n"
while getopts "b:e:r:a:o:fi" arg; do
case $arg in
b) bootdir="$OPTARG" ;;
e) entry="$OPTARG" ;;
r) cmdremove="$OPTARG" ;;
a) cmdadd="$OPTARG" ;;
o) override_initrd="$OPTARG" ;;
f) dryrun="y"; printfiles="y" ;;
i) dryrun="y"; printinitrd="y" ;;
b) bootdir="$OPTARG" ;;
e) entry="$OPTARG" ;;
r) cmdremove="$OPTARG" ;;
a) cmdadd="$OPTARG" ;;
o) override_initrd="$OPTARG" ;;
f)
dryrun="y"
printfiles="y"
;;
i)
dryrun="y"
printinitrd="y"
;;
esac
done

Expand All @@ -27,10 +50,15 @@ fi

bootdir="${bootdir%%/}"

kexectype=`echo $entry | cut -d\| -f2`
kexecparams=`echo $entry | cut -d\| -f3- | tr '|' '\n'`
kexectype=$(echo $entry | cut -d\| -f2)
kexecparams=$(echo $entry | cut -d\| -f3- | tr '|' '\n')
kexeccmd="kexec"

DEBUG "kexec-boot.sh: entry='$entry'"
DEBUG "kexec-boot.sh: kexectype='$kexectype'"
DEBUG "kexec-boot.sh: kexecparams='$kexecparams'"
DEBUG "kexec-boot.sh: cmdadd='$cmdadd'"

cmdadd="$CONFIG_BOOT_KERNEL_ADD $cmdadd"
cmdremove="$CONFIG_BOOT_KERNEL_REMOVE $cmdremove"

Expand All @@ -53,29 +81,33 @@ fix_file_path() {

adjusted_cmd_line="n"
adjust_cmd_line() {
DEBUG "adjust_cmd_line: original cmdline='$cmdline'"
cmdline=$(echo "$cmdline" | sed 's/---.*$//' | xargs)
DEBUG "adjust_cmd_line: after stripping --- separator='$cmdline'"
if [ -n "$cmdremove" ]; then
for i in $cmdremove; do
cmdline=$(echo $cmdline | sed "s/\b$i\b//g")
done
fi

if [ -n "$cmdadd" ]; then
DEBUG "adjust_cmd_line: cmdadd='$cmdadd'"
cmdline="$cmdline $cmdadd"
DEBUG "adjust_cmd_line: final cmdline='$cmdline'"
fi
adjusted_cmd_line="y"
}

if [ "$CONFIG_DEBUG_OUTPUT" = "y" ];then
if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then
#If expecting debug output, have kexec load (-l) output debug info
kexeccmd="$kexeccmd -d"
fi

module_number="1"
while read line
do
key=`echo $line | cut -d\ -f1`
firstval=`echo $line | cut -d\ -f2`
restval=`echo $line | cut -d\ -f3-`
while read line; do
key=$(echo $line | cut -d\ -f1)
firstval=$(echo $line | cut -d\ -f2)
restval=$(echo $line | cut -d\ -f3-)
if [ "$key" = "kernel" ]; then
fix_file_path
if [ "$kexectype" = "xen" ]; then
Expand Down Expand Up @@ -112,7 +144,7 @@ do
fi
fi
fi
module_number=`expr $module_number + 1`
module_number=$(expr $module_number + 1)
kexeccmd="$kexeccmd --module \"$filepath $cmdline\""
fi
if [ "$key" = "initrd" ]; then
Expand All @@ -135,7 +167,7 @@ do
adjust_cmd_line
kexeccmd="$kexeccmd --append=\"$cmdline\""
fi
done << EOF
done <<EOF
$kexecparams
EOF

Expand All @@ -149,14 +181,17 @@ fi

if [ "$dryrun" = "y" ]; then exit 0; fi

DEBUG "kexec-boot.sh: cmdadd='$cmdadd'"
DEBUG "kexec-boot.sh: cmdremove='$cmdremove'"
DEBUG "kexec-boot.sh: final cmdline='$cmdline'"
STATUS "Loading the new kernel"
DEBUG "kexec command: $kexeccmd"
# DO_WITH_DEBUG captures the debug output from stderr to the log, we don't need
# it on the console as well
DO_WITH_DEBUG eval "$kexeccmd" 2>/dev/null \
|| DIE "Failed to load the new kernel"
DEBUG "kexec-boot: executing kexec with adjusted_cmd_line=$adjusted_cmd_line kexectype=$kexectype"
echo "Loading kernel with: $kexeccmd" >/dev/console
DO_WITH_DEBUG eval "$kexeccmd" ||
DIE "Failed to load the new kernel"

if [ "$CONFIG_DEBUG_OUTPUT" = "y" ];then
if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then
#Ask user if they want to continue booting without echoing back the input (-s)
INPUT "[DEBUG] Continue booting? [Y/n]:" -s -n 1 debug_boot_confirm
if [ "${debug_boot_confirm^^}" = N ]; then
Expand Down
Loading