diff --git a/doc/boot-process.md b/doc/boot-process.md index 6491aaf86..8c8d95a83 100644 --- a/doc/boot-process.md +++ b/doc/boot-process.md @@ -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 | ext4/vfat/exfat | works | +| Debian Live kde/xfce | hybrid | boot=live, rd.live.image | grub.cfg | ext4/vfat/exfat | works | +| Tails 7.6 | hybrid | boot=live | grub.cfg | ext4/vfat | works | +| Tails (exfat-support) | hybrid | boot=live | grub.cfg | exfat | works | +| Fedora Workstation Live | hybrid | boot=live, rd.live.image | grub.cfg | ext4/vfat | works | +| NixOS | hybrid | findiso, nixos | grub.cfg | ext4/vfat/exfat | works | +| PureOS | hybrid | boot=casper | grub.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. diff --git a/initrd/bin/kexec-boot.sh b/initrd/bin/kexec-boot.sh index 4043c3118..ad68b1fb5 100755 --- a/initrd/bin/kexec-boot.sh +++ b/initrd/bin/kexec-boot.sh @@ -11,13 +11,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 @@ -27,10 +33,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" @@ -53,6 +64,7 @@ fix_file_path() { adjusted_cmd_line="n" adjust_cmd_line() { + DEBUG "adjust_cmd_line: original cmdline='$cmdline'" if [ -n "$cmdremove" ]; then for i in $cmdremove; do cmdline=$(echo $cmdline | sed "s/\b$i\b//g") @@ -60,22 +72,23 @@ adjust_cmd_line() { 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 @@ -112,7 +125,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 @@ -135,7 +148,7 @@ do adjust_cmd_line kexeccmd="$kexeccmd --append=\"$cmdline\"" fi -done << EOF +done </dev/null \ -|| DIE "Failed to load the new kernel" +DEBUG "kexec-boot: executing kexec with adjusted_cmd_line=$adjusted_cmd_line kexectype=$kexectype" +DO_WITH_DEBUG eval "$kexeccmd" 2>/dev/null || + 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 diff --git a/initrd/bin/kexec-iso-init.sh b/initrd/bin/kexec-iso-init.sh index fa7b85ce9..6be64c914 100755 --- a/initrd/bin/kexec-iso-init.sh +++ b/initrd/bin/kexec-iso-init.sh @@ -1,5 +1,19 @@ #!/bin/bash -# Boot from signed ISO +# Boot ISO file from USB media (ext4/fat/exfat USB stick) +# +# References: +# - https://wiki.archlinux.org/title/ISO_Spring_(%27Loop%27_device) +# - https://a1ive.github.io/grub2_loopback.html +# +# Boot Methods: Pass iso-scan/filename=, fromiso=, img_loop=, etc. via kexec. +# The ISO initrd picks what it needs. Hybrid ISOs (MBR sig 0x55AA) can boot from USB file. +# +# Known compatible: Ubuntu, Debian Live, Tails, NixOS, Fedora Workstation Live, +# PureOS, Kicksecure (Dracut-based, boot=live, iso-scan) +# Known incompatible: Fedora Silverblue, Fedora Server, Qubes OS (Anaconda-based, +# inst.stage2= requires block device or dd). Use dd or distribution media tool. +# +# See: https://github.com/linuxboot/heads/issues/2008 set -e -o pipefail . /etc/functions.sh . /etc/gui_functions.sh @@ -22,8 +36,8 @@ ISO_PATH="${ISO_PATH##/}" if [ -r "$ISOSIG" ]; then # Signature found, verify it - gpgv.sh --homedir=/etc/distro/ "$ISOSIG" "$MOUNTED_ISO_PATH" \ - || DIE 'ISO signature failed' + gpgv.sh --homedir=/etc/distro/ "$ISOSIG" "$MOUNTED_ISO_PATH" || + DIE 'ISO signature failed' STATUS_OK "ISO signature verified" else # No signature found, prompt user with warning @@ -46,11 +60,160 @@ else NOTE "Proceeding with unsigned ISO boot" fi +check_hybrid_iso() { + local iso_path="$1" + local mbr_sig + + [ -r "$iso_path" ] || return 1 + mbr_sig=$(dd if="$iso_path" bs=1 skip=510 count=2 2>/dev/null | xxd -p) + DEBUG "check_hybrid_iso: mbr_sig=$mbr_sig" + if [ "$mbr_sig" = "55aa" ]; then + echo "hybrid" + else + echo "cdrom" + fi +} + +STATUS "Checking ISO boot capability..." +ISO_BOOT_TYPE=$(check_hybrid_iso "$MOUNTED_ISO_PATH") +DEBUG "ISO boot type: $ISO_BOOT_TYPE" + STATUS "Mounting ISO and booting" -mount -t iso9660 -o loop $MOUNTED_ISO_PATH /boot \ - || DIE '$MOUNTED_ISO_PATH: Unable to mount /boot' +mount -t iso9660 -o loop $MOUNTED_ISO_PATH /boot || + DIE '$MOUNTED_ISO_PATH: Unable to mount /boot' + +detect_initrd_boot_support() { + local supported_fses="" + local supported_boot="" + local found=0 + + for path in $(find /boot -name 'initrd*' -type f 2>/dev/null | head -5); do + [ -r "$path" ] || continue + tmpdir=$(mktemp -d) + /bin/bash /bin/unpack_initramfs.sh "$path" "$tmpdir" 2>/dev/null + + if find "$tmpdir" -type f 2>/dev/null | xargs strings 2>/dev/null | grep -qE "\.ko\.xz.*ext4|ext4\.ko"; then + supported_fses="${supported_fses}ext4 " + found=1 + fi + if find "$tmpdir" -type f 2>/dev/null | xargs strings 2>/dev/null | grep -qE "\.ko\.xz.*vfat|vfat\.ko"; then + supported_fses="${supported_fses}vfat " + found=1 + fi + if find "$tmpdir" -type f 2>/dev/null | xargs strings 2>/dev/null | grep -qE "\.ko\.xz.*exfat|exfat\.ko"; then + supported_fses="${supported_fses}exfat " + found=1 + fi + + if find "$tmpdir" -type f 2>/dev/null | xargs strings 2>/dev/null | grep -qE "iso.scan|findiso"; then + supported_boot="${supported_boot}iso-scan/findiso " + fi + if find "$tmpdir" -type f 2>/dev/null | xargs strings 2>/dev/null | grep -qE "live.media|live-media"; then + supported_boot="${supported_boot}live-media= " + fi + if find "$tmpdir" -type f 2>/dev/null | xargs strings 2>/dev/null | grep -qE "boot=live|rd.live.image|rd.live.squash"; then + supported_boot="${supported_boot}boot=live " + fi + if find "$tmpdir" -type f 2>/dev/null | xargs strings 2>/dev/null | grep -qE "boot.casper|casper"; then + supported_boot="${supported_boot}boot=casper " + fi + if find "$tmpdir" -type f 2>/dev/null | xargs strings 2>/dev/null | grep -qE "nixos"; then + supported_boot="${supported_boot}nixos " + fi + if find "$tmpdir" -type f 2>/dev/null | xargs strings 2>/dev/null | grep -qE "inst.stage2|inst.repo"; then + supported_boot="${supported_boot}anaconda " + fi + rm -rf "$tmpdir" + done + + if [ -n "$supported_fses" ]; then + echo "fs:$supported_fses" + fi + if [ -n "$supported_boot" ]; then + echo "boot:$supported_boot" + fi +} + +extract_boot_params_from_cfg() { + for cfg in $(find /boot -name '*.cfg' -type f 2>/dev/null); do + [ -r "$cfg" ] || continue + local boot_params="" + while IFS= read -r line; do + case "$line" in + *boot=live* | *rd.live.image* | *rd.live.squashimg=*) + boot_params="${boot_params}boot=live " + ;; + *iso-scan/filename=* | *findiso=*) + boot_params="${boot_params}iso-scan/findiso " + ;; + *live-media=* | *live.media=*) + boot_params="${boot_params}live-media= " + ;; + *boot=casper* | *casper*) + boot_params="${boot_params}boot=casper " + ;; + *inst.stage2=* | *inst.repo=*) + boot_params="${boot_params}anaconda " + ;; + *nixos*) + boot_params="${boot_params}nixos " + ;; + esac + done <"$cfg" + [ -n "$boot_params" ] && echo "cfg:$boot_params" && return 0 + done + return 1 +} + +STATUS "Detecting USB filesystem and boot method support..." +SUPPORTED_FSES="" +SUPPORTED_BOOT="" +CFG_BOOT="" +DETECTED_METHODS="" + +SUPPORTED_FSES=$(detect_initrd_boot_support 2>/dev/null | grep "^fs:" | sed 's/^fs://') +CFG_BOOT=$(extract_boot_params_from_cfg 2>/dev/null | grep "^cfg:" | sed 's/^cfg://') + +if [ -n "$SUPPORTED_FSES" ]; then + DEBUG "Initrd supports USB filesystems: $SUPPORTED_FSES" + DEV_FSTYPE=$(blkid $DEV | tail -1 | grep -oE "TYPE="[^"]+" | sed 's/TYPE="//') + if ! echo "$SUPPORTED_FSES" | grep -q "$DEV_FSTYPE"; then + WARN "USB filesystem ($DEV_FSTYPE) may not be supported by this ISO's initrd" + DEBUG "Supported filesystems: $SUPPORTED_FSES" + fi +fi + +if [ -z "$SUPPORTED_FSES" ]; then + WARN "Could not detect filesystem support in ISO initrd" + DEBUG "USB boot may fail if ISO initrd does not support your USB stick filesystem" +fi + +STATUS "Detecting boot method..." +if [ -n "$SUPPORTED_BOOT" ]; then + DETECTED_METHODS="$SUPPORTED_BOOT" + DEBUG "Initrd supports boot methods: $DETECTED_METHODS" +else + if [ -n "$CFG_BOOT" ]; then + DETECTED_METHODS="$CFG_BOOT" + DEBUG "Boot config (*.cfg) indicates boot methods: $DETECTED_METHODS" + fi +fi + +if [ -z "$DETECTED_METHODS" ]; then + WARN "ISO may not boot from USB file: no supported boot method detected" + if [ -x /bin/whiptail ]; then + if ! whiptail_warning --title 'ISO BOOT COMPATIBILITY WARNING' --yesno \ + "ISO boot from USB file may not work.\n\nThis ISO does not appear to support booting from ISO file on USB stick.\n\nKnown compatible ISOs: Ubuntu, Debian Live, Tails, NixOS, Fedora Workstation, PureOS, Kicksecure.\n\nFor this ISO, try:\n- Use distribution USB creation tool (Ventoy, Rufus, etc)\n- Write ISO directly to USB with dd\n- Report to upstream that ISO should support USB file boot\n\nDo you want to try anyway?" \ + 0 80; then + DIE "ISO boot cancelled - unsupported ISO on USB file" + fi + else + INPUT "ISO may not support USB file boot. Try anyway? [y/N]:" -n 1 response + [ "$response" != "y" ] && [ "$response" != "Y" ] && DIE "ISO boot cancelled - unsupported ISO on USB file" + fi +fi -DEV_UUID=`blkid $DEV | tail -1 | tr " " "\n" | grep UUID | cut -d\" -f2` +DEV_UUID=$(blkid $DEV | tail -1 | tr " " "\n" | grep UUID | cut -d\" -f2) ADD="fromiso=/dev/disk/by-uuid/$DEV_UUID/$ISO_PATH img_dev=/dev/disk/by-uuid/$DEV_UUID iso-scan/filename=/${ISO_PATH} img_loop=$ISO_PATH iso=$DEV_UUID/$ISO_PATH" REMOVE="" @@ -59,14 +222,14 @@ check_config $paramsdir ADD_FILE=/tmp/kexec/kexec_iso_add.txt if [ -r $ADD_FILE ]; then - NEW_ADD=`cat $ADD_FILE` + NEW_ADD=$(cat $ADD_FILE) ADD=$(eval "echo \"$NEW_ADD\"") fi DEBUG "Overriding ISO kernel arguments with additions: $ADD" REMOVE_FILE=/tmp/kexec/kexec_iso_remove.txt if [ -r $REMOVE_FILE ]; then - NEW_REMOVE=`cat $REMOVE_FILE` + NEW_REMOVE=$(cat $REMOVE_FILE) REMOVE=$(eval "echo \"$NEW_REMOVE\"") fi DEBUG "Overriding ISO kernel arguments with suppressions: $REMOVE" diff --git a/initrd/bin/kexec-parse-boot.sh b/initrd/bin/kexec-parse-boot.sh index 852bc00ee..2c730714f 100755 --- a/initrd/bin/kexec-parse-boot.sh +++ b/initrd/bin/kexec-parse-boot.sh @@ -139,6 +139,20 @@ grub_entry() { DEBUG " grub_entry : linux trimcmd prior of kernel/append parsing: $trimcmd" kernel=`echo $trimcmd | sed "s/([^)]*)//g" | cut -d\ -f2` append=`echo $trimcmd | cut -d\ -f3-` + + # Strip unresolved GRUB variables that would expand to empty and break kexec. + # These create malformed params like "iso-scan/filename=" with orphaned paths. + append=$(echo "$append" | sed \ + -e 's|iso-scan/filename=${[^}]*}| |g' \ + -e 's|iso-scan/filename=$[a-zA-Z_][a-zA-Z0-9_]*| |g' \ + -e 's|iso-scan/filename=| |g' \ + -e 's|findiso=${[^}]*}| |g' \ + -e 's|findiso=$[a-zA-Z_][a-zA-Z0-9_]*| |g' \ + -e 's|findiso=| |g' \ + -e 's| *| |g' \ + -e 's|^ ||' \ + -e 's| $||') + ;; initrd*) # Trim off device specification as above