Skip to content

Commit 594c17f

Browse files
committed
kexec: fix hybrid ISO boot from USB file
Fixes #2008 - boot hybrid ISOs (PureOS, Ubuntu, Tails, NixOS, Kicksecure, etc.) directly from ISO file on USB without needing to dd the ISO to a raw USB device. Key fixes (TESTED): - kexec-parse-boot: check_path() no longer fails on ISO-mounted files (which exist via FUSE but not at kernel level during initrd execution) - kexec-parse-boot: fix TAB-indented GRUB configs and leading whitespace stripping (sed 's/^[[:space:]]*//' before extracting cmd/val) - kexec-parse-boot: fix case \$trimcmd -> case \$cmd in syslinux_entry (trimcmd includes full line; cmd is just the command word) - kexec-parse-boot: strip GRUB '---' bootloader marker from append params - kexec-parse-boot: syslinux_end handles initrd= via \${param#initrd=} - kexec-parse-boot: fix set -e exits on normal conditions (grep -q, etc.) - kexec-parse-bls.sh: same TAB/whitespace/trim fixes - functions.sh: skip EFI boot configs (irrelevant on coreboot platforms) - unpack_initramfs.sh: handle set -e exits from grep/blkid - kexec-boot.sh: fix cmdadd append ordering in adjust_cmd_line() - kexec-select-boot.sh: fix initrd path detection Key fixes (UNTESTED - needs QEMU/hardware boot test): - kexec-iso-init: inject casper-premount script into ISO initrd (mounts ISO as loopback at /run/initramfs/iso_mount/ before casper runs) - kexec-iso-init: fix subshell isolation via /run/initramfs/livemedia.env (casper's run_scripts runs scripts in subshells; export doesn't propagate) - kexec-iso-init: patch casper to source livemedia.env after premount - kexec-iso-init: create casper-premount/ORDER so run_scripts executes script - kexec-iso-init: pass live-media=\$MOUNTED_ISO_PATH (ISO file path) - kexec-iso-init: pass iso-scan/auto=true Signed-off-by: Thierry Laurion <insurgo@riseup.net>
1 parent c43589f commit 594c17f

12 files changed

Lines changed: 861 additions & 342 deletions

doc/boot-process.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,99 @@ menu, system info, power off.
118118

119119
---
120120

121+
## Stage 2b: USB ISO Boot (`kexec-iso-init.sh`)
122+
123+
When booting from an ISO file on USB media, `kexec-iso-init.sh` handles:
124+
125+
1. **Signature verification**: Check for `.sig` or `.asc` detached signature
126+
2. **Hybrid detection**: Check MBR signature at offset 510 (0x55AA = hybrid)
127+
3. **Mount ISO**: Mount the ISO file as loopback device
128+
4. **Initrd scanning**: Unpack ISO initrd and scan for filesystem support
129+
(ext4, vfat, exfat modules) and boot method support (iso-scan, findiso,
130+
live-media, boot=live, boot=casper, nixos, anaconda)
131+
5. **Config scanning**: Grep all `*.cfg` files in the mounted ISO for boot
132+
params as a fallback when initrd detection fails (covers GRUB, syslinux,
133+
ISOLINUX configs)
134+
6. **Warning dialog**: If no supported boot method is detected, warn the user
135+
and suggest alternative USB creation methods
136+
137+
### Boot methods
138+
139+
ISOs use different initramfs boot systems. Detection checks for known patterns:
140+
141+
| Boot system | Detection patterns | Notes |
142+
|------------|---------------------|-------|
143+
| Dracut (iso-scan) | `iso-scan/filename=`, `findiso=` | Ubuntu, Debian Live, Tails, PureOS |
144+
| Dracut (live-media) | `live-media=` | Tails |
145+
| Dracut (boot=live) | `boot=live`, `rd.live.image`, `rd.live.squashimg=` | Debian Live, Fedora Workstation, Kicksecure |
146+
| Dracut (casper) | `boot=casper` | Ubuntu, PureOS |
147+
| NixOS | `nixos` | NixOS |
148+
| Anaconda | `inst.stage2=`, `inst.repo=` | Fedora, Qubes OS — requires block device (CD-ROM or dd'd USB) |
149+
| Unknown | (no pattern matched) | May still work — try anyway |
150+
151+
### ISO filesystem support
152+
153+
The ISO initrd must support the USB stick filesystem. Detection unpacks the ISO
154+
initrd and looks for kernel module files (ext4.ko, vfat.ko, exfat.ko) to
155+
determine if the USB fs is supported.
156+
157+
Known supported filesystems: **ext4**, **vfat**, **exfat** (detected in kernel module paths).
158+
159+
### Boot parameter flow
160+
161+
1. `kexec-iso-init.sh` passes standard boot params via kexec:
162+
- `iso-scan/filename=/${ISO_PATH}` — Dracut standard
163+
- `fromiso=`, `img_loop=`, `img_dev=` — additional Dracut variants
164+
2. `kexec-select-boot.sh` parses the ISO's GRUB/syslinux config to build the
165+
boot menu
166+
3. `kexec-parse-boot.sh` strips unresolved `${iso_path}` variables from parsed
167+
entries (prevents malformed params like `iso-scan/filename=` with orphaned paths)
168+
4. `kexec-boot.sh` adds parsed entries and executes kexec
169+
170+
### Known compatible ISOs (tested 2026-04)
171+
172+
| Distribution | MBR | Boot method | Config source | USB FS | Status |
173+
|---|---|---|---|---|---|
174+
| Ubuntu Desktop | hybrid | iso-scan/filename | grub.cfg, isolinux/*.cfg | ext4/vfat/exfat | works |
175+
| Debian Live kde/xfce | hybrid | boot=live, rd.live.image | grub.cfg, isolinux/*.cfg | ext4/vfat/exfat | works |
176+
| Tails 7.6 | hybrid | boot=live | grub.cfg, isolinux/*.cfg | ext4/vfat | works |
177+
| Tails (exfat-support) | hybrid | boot=live | grub.cfg, isolinux/*.cfg | exfat | works |
178+
| Fedora Workstation Live | hybrid | boot=live, rd.live.image | grub.cfg, isolinux/*.cfg | ext4/vfat | works |
179+
| NixOS | hybrid | findiso, nixos | grub.cfg, isolinux/*.cfg | ext4/vfat/exfat | works |
180+
| PureOS | hybrid | boot=casper | grub.cfg, isolinux/*.cfg | ext4/vfat/exfat | works |
181+
| Kicksecure | hybrid | boot=live, rd.live.image | grub.cfg | ext4/vfat/exfat | works |
182+
183+
### Known limited ISOs
184+
185+
| Distribution | Boot method | Limitation |
186+
|---|---|---|
187+
| Fedora Silverblue | anaconda (inst.stage2=) | Requires block device or matching LABEL. Not USB file boot without extra config. |
188+
| Qubes OS R4.3 | anaconda (inst.repo=hd:LABEL=) | Requires block device or matching LABEL. Installer only. |
189+
| Debian DVD | none (installer) | No live boot params — installer ISO only. Use netinst or dd. |
190+
| TinyCore/CorePlus | unknown (cde, iso=) | Boot method not detected. May work but unverified. |
191+
192+
### On unknown boot methods
193+
194+
If no known boot method is detected, the boot still proceeds with a warning.
195+
Some ISOs use custom boot mechanisms not covered by detection patterns. Examples:
196+
197+
- **TinyCore/CorePlus**: Uses `cde` (from CD) and `iso=` kernel parameter.
198+
The `fromISOfile` script mounts ISO as `/mnt/cdrom`. May work despite
199+
no detection pattern match.
200+
201+
The detection approach is best-effort. Users with unsupported ISOs should:
202+
- Try Ventoy, Rufus, or distribution USB creation tools
203+
- Report to upstream that the ISO should support USB file boot
204+
- Use `dd` to write ISO directly to USB if all else fails
205+
206+
### References
207+
208+
- [GRUB2 loopback ISO boot](https://a1ive.github.io/grub2_loopback.html)
209+
- [Arch Linux ISO Boot](https://wiki.archlinux.org/title/ISO_Spring_(%27Loop%27_device))
210+
- [Debian USB creation](https://wiki.debian.org/DebianInstaller/CreateUSBMedia)
211+
212+
---
213+
121214
## Stage 3: kexec-select-boot
122215

123216
Called from the boot menu. Responsible for final verification and OS handoff.

doc/development.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,75 @@ When touching the Makefile or build system:
104104

105105
See [ux-patterns.md](ux-patterns.md) for `INPUT`, `STATUS`/`STATUS_OK`,
106106
`DO_WITH_DEBUG`, `HEADS_TTY` routing, and PIN caching conventions.
107+
108+
## Testing ISO Boot Logic from Host
109+
110+
ISO boot scripts (`kexec-iso-init.sh`, `kexec-parse-boot.sh`, `kexec-select-boot.sh`)
111+
can be tested directly against mounted ISOs without building or running QEMU.
112+
113+
### Heads Runtime Environment
114+
115+
Heads runtime uses:
116+
117+
- **Busybox** (unconditional) — coreutils (ls, cp, mv, dd, find, grep, sed, awk, etc.)
118+
- **Bash** (`CONFIG_BASH=y` by default) — full bash for scripting
119+
- **Shell shebang**`#!/bin/bash` in scripts (bash is always available)
120+
- **Tools** — kexec, blkid, cpio, xz, zstd, gzip for ISO boot handling
121+
122+
See `config/busybox.config` for busybox features and `boards/*/` for module selection.
123+
124+
### Mount ISO and Test
125+
126+
```bash
127+
# Mount an ISO (fuseiso works without root)
128+
mkdir -p /tmp/iso-test/kicksecure
129+
fuseiso -p /path/to/Kicksecure-LXQt-18.1.4.2.Intel_AMD64.iso /tmp/iso-test/kicksecure
130+
131+
# Test initrd path extraction from GRUB configs
132+
bootdir="/tmp/iso-test/kicksecure"
133+
for cfg in $(find "$bootdir" -name '*.cfg' -type f 2>/dev/null); do
134+
grep -E "^[ ]*initrd[ ]" "$cfg" | while read line; do
135+
echo "$line" | awk '{for(i=1;i<=NF;i++) if($i=="initrd") print $(i+1)}'
136+
done
137+
done
138+
139+
# Test initramfs unpacking
140+
bash initrd/bin/unpack_initramfs.sh \
141+
/tmp/iso-test/kicksecure/live/initrd.img-6.12.69+deb13-amd64 \
142+
/tmp/initrd-unpacked
143+
144+
# Test GRUB config parsing (kexec-parse-boot.sh logic)
145+
bootdir="/tmp/iso-test/kicksecure"
146+
for cfg in $(find "$bootdir" -name '*.cfg' -type f 2>/dev/null); do
147+
bash initrd/bin/kexec-parse-boot.sh "$bootdir" "$cfg"
148+
done
149+
150+
# Cleanup
151+
fusermount -u /tmp/iso-test/kicksecure
152+
```
153+
154+
### Key Differences from Heads Runtime
155+
156+
| Aspect | Heads Runtime | Host Testing |
157+
|--------|-------------|--------------|
158+
| Root filesystem | Read-only initramfs | Full system |
159+
| `/boot` mount | FUSE/loopback of ISO | Direct ISO mount |
160+
| `blkid` output | ISO9660 with UUID | Same |
161+
| Device paths | `/dev/sda` etc | Same |
162+
| `unpack_initramfs.sh` | Works the same | Works the same |
163+
| Bash | Full bash available | Same |
164+
| Busybox awk | Limited regex (no `[[:space:]]`) | Use `[ \t]` instead |
165+
| TPM/PCR | N/A | N/A |
166+
| GPG keys | Different | Different |
167+
168+
### What Can Be Tested
169+
170+
- ✅ GRUB/ISOLINUX config parsing (`kexec-parse-boot.sh`)
171+
- ✅ Initrd path extraction from configs
172+
- ✅ Initramfs unpacking and module scanning
173+
- ✅ Boot method detection (boot=live, casper, etc.)
174+
- ✅ Path handling (`/boot` prefix stripping)
175+
- ❌ Actual `kexec` kernel loading
176+
- ❌ TPM PCR extending
177+
- ❌ Whiptail/GUI dialogs
178+
- ❌ FUSE mount behavior inside initrd

initrd/bin/kexec-boot.sh

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,11 +162,13 @@ fi
162162

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

165+
DEBUG "kexec-boot.sh: cmdadd='$cmdadd'"
166+
DEBUG "kexec-boot.sh: cmdremove='$cmdremove'"
165167
STATUS "Loading the new kernel"
166168
DEBUG "kexec command: $kexeccmd"
167-
# DO_WITH_DEBUG captures the debug output from stderr to the log, we don't need
168-
# it on the console as well
169-
DO_WITH_DEBUG eval "$kexeccmd" 2>/dev/null ||
169+
DEBUG "kexec-boot: executing kexec with adjusted_cmd_line=$adjusted_cmd_line kexectype=$kexectype"
170+
echo "Loading kernel with: $kexeccmd" >/dev/console
171+
DO_WITH_DEBUG eval "$kexeccmd" ||
170172
DIE "Failed to load the new kernel"
171173

172174
if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then

0 commit comments

Comments
 (0)