From 642b39dcffa086566a7fa9a2d47fa1980e437aeb Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Sun, 12 Apr 2026 20:34:31 -0400 Subject: [PATCH 01/10] kexec-iso-init: improve hybrid ISO detection and boot param handling - Add check_hybrid_iso() using MBR signature at offset 510 (0x55AA) - Add detect_iso_boot_method() to extract boot params from initrd via strings - Add inspect_iso_boot_config() to extract boot params from GRUB configs - Simplify header to document Dracut vs Anaconda boot methods - Use DEBUG level for NOTE/WARN/STATUS spam per logging.md - Change terminal prompts to [Y,d] style with Enter defaulting to yes - Remove Anaconda blocking - let user attempt boot (Qubes R4.3 works) - Keep combined boot params approach (let ISO initrd pick what it needs) Tested with Qubes R4.3 on Q35 QEMU (works). Ref: linuxboot/heads#2083, linuxboot/heads#2008 Signed-off-by: Thierry Laurion --- initrd/bin/kexec-iso-init.sh | 185 ++++++++++++++++++++++++++++++-- initrd/bin/kexec-select-boot.sh | 11 +- 2 files changed, 183 insertions(+), 13 deletions(-) diff --git a/initrd/bin/kexec-iso-init.sh b/initrd/bin/kexec-iso-init.sh index fa7b85ce9..2d09a2ebc 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 (detected via initrd strings analysis): +# - Dracut-based: iso-scan/filename=, findiso=, live-media=, boot=casper +# Works: Ubuntu, Debian Live, Tails, NixOS, Fedora Workstation Live, PureOS +# - Anaconda-based: inst.stage2=hd:LABEL=, inst.repo=hd:LABEL= +# Requires block device (CD-ROM or dd'd USB) - CANNOT boot from ISO file +# Examples: Fedora Silverblue, Fedora Server, Qubes OS, Kicksecure +# +# Anaconda ISOs require: dd if=image.iso of=/dev/sdX 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,166 @@ else NOTE "Proceeding with unsigned ISO boot" fi +check_hybrid_iso() { + local iso_path="$1" + local mbr_sig=$(dd if="$iso_path" bs=2 skip=255 count=2 2>/dev/null | xxd -p) + if [ "$mbr_sig" = "55aa" ]; then + local efi_magic=$(dd if="$iso_path" bs=1 skip=135 count=8 2>/dev/null | xxd -p) + if [ -n "$efi_magic" ]; then + echo "hybrid" + else + echo "cdrom" + fi + 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" + +if [ "$ISO_BOOT_TYPE" != "hybrid" ]; then + DEBUG "Non-hybrid ISO detected (CD-ROM only)" +fi + 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_iso_boot_method() { + local method="" + 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 "iso.scan|findiso"; then + method="${method}iso-scan/findiso " + found=1 + fi + if find "$tmpdir" -type f 2>/dev/null | xargs strings 2>/dev/null | grep -qE "live.media|live-media"; then + method="${method}live-media= " + found=1 + fi + if find "$tmpdir" -type f 2>/dev/null | xargs strings 2>/dev/null | grep -qE "inst.stage2|inst\.stage2"; then + method="${method}inst.stage2= " + found=1 + fi + if find "$tmpdir" -type f 2>/dev/null | xargs strings 2>/dev/null | grep -qE "inst.repo"; then + method="${method}inst.repo= " + found=1 + fi + if find "$tmpdir" -type f 2>/dev/null | xargs strings 2>/dev/null | grep -qE "boot.casper|live-boot|casper"; then + method="${method}boot=casper " + found=1 + fi + if find "$tmpdir" -type f 2>/dev/null | xargs strings 2>/dev/null | grep -qE "nixos"; then + method="${method}nixos " + found=1 + fi + rm -rf "$tmpdir" + done + + if [ $found -eq 0 ]; then + return 1 + fi + echo "$method" + return 0 +} + +inspect_iso_boot_config() { + local grub_cfg="$1" + local boot_params="" + + [ -f "$grub_cfg" ] || return 1 + + while IFS= read -r line; do + case "$line" in + *inst.stage2=*) + params="${line##*inst.stage2=}" + params="${params%% *}" + [ -n "$params" ] && boot_params="${boot_params} inst.stage2=${params}" + ;; + *inst.repo=*) + params="${line##*inst.repo=}" + params="${params%% *}" + [ -n "$params" ] && boot_params="${boot_params} inst.repo=${params}" + ;; + *live-media=*) + params="${line##*live-media=}" + params="${params%% *}" + [ -n "$params" ] && boot_params="${boot_params} live-media=${params}" + ;; + *iso-scan/filename=* | *findiso=*) + params="${line##*iso-scan/filename=}" + [ "$params" = "$line" ] && params="${line##*findiso=}" + params="${params%% *}" + [ -n "$params" ] && boot_params="${boot_params} iso-scan/filename=${params}" + ;; + *boot=casper*) + boot_params="${boot_params} boot=casper" + ;; + esac + done <"$grub_cfg" + + echo "$boot_params" + return 0 +} + +STATUS "Detecting ISO boot method..." +BOOT_METHODS=$(detect_iso_boot_method) || BOOT_METHODS="" +EXTRACTED_PARAMS="" + +if [ -n "$BOOT_METHODS" ]; then + DEBUG "Detected boot methods: $BOOT_METHODS" +else + DEBUG "No built-in ISO boot support in initrd; checking GRUB config..." + + for cfg in $(find /boot -name '*.cfg' -type f 2>/dev/null); do + [ -r "$cfg" ] || continue + if grep -qE "iso.scan|findiso|live.media=|boot=casper" "$cfg" 2>/dev/null; then + BOOT_METHODS="${BOOT_METHODS}grub " + break + fi + if grep -qE "inst.repo=|inst.stage2=" "$cfg" 2>/dev/null; then + BOOT_METHODS="${BOOT_METHODS}anaconda " + fi + done + + if [ -n "$BOOT_METHODS" ]; then + DEBUG "Found boot support: $BOOT_METHODS" + else + WARN "ISO may not boot from USB file: no boot support in initrd" + 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 have iso-scan/findiso/live-media in its initrd - it was designed for CD/DVD or dd-to-USB.\n\nKernel parameters passed externally may not be sufficient.\n\nTry:\n- Use distribution-specific ISO (e.g., Debian hd-media)\n- Write ISO directly to USB with dd\n- Use a live USB image\n\nDo you want to proceed anyway?" \ + 0 80; then + DIE "ISO boot cancelled - unsupported ISO on USB file" + fi + else + INPUT "Proceed with boot anyway? [y/N]:" -n 1 response + [ "$response" != "y" ] && [ "$response" != "Y" ] && DIE "ISO boot cancelled - unsupported ISO on USB file" + fi + fi +fi + +if echo "$BOOT_METHODS" | grep -qE "anaconda|inst.repo|inst.stage2"; then + DEBUG "Anaconda-based ISO detected (inst.stage2=)" +fi + +if [ -z "$BOOT_METHODS" ]; then + for cfg in $(find /boot -name '*.cfg' -type f 2>/dev/null); do + EXTRACTED_PARAMS=$(inspect_iso_boot_config "$cfg") + [ -n "$EXTRACTED_PARAMS" ] && break + done + DEBUG "Extracted boot params from GRUB: $EXTRACTED_PARAMS" +fi -DEV_UUID=`blkid $DEV | tail -1 | tr " " "\n" | grep UUID | cut -d\" -f2` +# Detect USB stick filesystem and validate initrd supports it +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 +228,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-select-boot.sh b/initrd/bin/kexec-select-boot.sh index 9f5a07543..6efb0ae87 100755 --- a/initrd/bin/kexec-select-boot.sh +++ b/initrd/bin/kexec-select-boot.sh @@ -194,7 +194,8 @@ confirm_menu_option() { STATUS "Confirm boot details for $name:" INFO "$option" - INPUT "Confirm selection by pressing 'y', make default with 'd':" -n 1 option_confirm + INPUT "Confirm selection by pressing 'Y' or 'd' to make default [Y,d]:" -n 1 option_confirm + [ -z "$option_confirm" ] && option_confirm="y" fi } @@ -219,11 +220,11 @@ scan_options() { save_default_option() { if [ "$gui_menu" != "y" ]; then - INPUT "Saving a default will modify the disk. Proceed? (Y/n):" -n 1 default_confirm + INPUT "Saving a default will modify the disk. Proceed? [Y/n]:" -n 1 default_confirm + [ -z "$default_confirm" ] && default_confirm="y" fi - [ "$default_confirm" = "" ] && default_confirm="y" - if [[ "$default_confirm" = "y" || "$default_confirm" = "Y" ]]; then + if [[ "$default_confirm" = [yY] ]]; then if kexec-save-default.sh \ -b "$bootdir" \ -d "$paramsdev" \ @@ -287,7 +288,7 @@ user_select() { # No default expected boot parameters, ask user option_confirm="" - while [ "$option_confirm" != "y" -a "$option_confirm" != "d" ]; do + while [[ "$option_confirm" != [yY] && "$option_confirm" != "d" ]]; do get_menu_option # In force boot mode, no need offer the option to set a default, just boot if [[ "$force_boot" = "y" || "$skip_confirm" = "y" ]]; then From bf4efa4b0e996874874ee2eb4541bcdebb622cc6 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Sun, 12 Apr 2026 20:43:29 -0400 Subject: [PATCH 02/10] kexec-iso-init: resolve GRUB variable references in boot params Add resolve_grub_vars() to substitute GRUB variables like ${iso_path} and ${isofile} with the actual ISO path when extracting boot params from GRUB configs. Fixes boot failure where iso-scan/filename=${iso_path} wasn't being resolved to the actual ISO path. Ref: linuxboot/heads#2083 Signed-off-by: Thierry Laurion --- initrd/bin/kexec-iso-init.sh | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/initrd/bin/kexec-iso-init.sh b/initrd/bin/kexec-iso-init.sh index 2d09a2ebc..63577cab1 100755 --- a/initrd/bin/kexec-iso-init.sh +++ b/initrd/bin/kexec-iso-init.sh @@ -130,8 +130,22 @@ detect_iso_boot_method() { return 0 } +resolve_grub_vars() { + local params="$1" + local iso_path="$2" + local resolved="" + + resolved="${params//\$\{iso_path\}/$iso_path}" + resolved="${resolved//\$\{isofile\}/$iso_path}" + resolved="${resolved//\$iso_path/$iso_path}" + resolved="${resolved//\$isofile/$iso_path}" + + echo "$resolved" +} + inspect_iso_boot_config() { local grub_cfg="$1" + local iso_path="$2" local boot_params="" [ -f "$grub_cfg" ] || return 1 @@ -165,6 +179,10 @@ inspect_iso_boot_config() { esac done <"$grub_cfg" + if [ -n "$iso_path" ]; then + boot_params=$(resolve_grub_vars "$boot_params" "$iso_path") + fi + echo "$boot_params" return 0 } @@ -212,7 +230,7 @@ fi if [ -z "$BOOT_METHODS" ]; then for cfg in $(find /boot -name '*.cfg' -type f 2>/dev/null); do - EXTRACTED_PARAMS=$(inspect_iso_boot_config "$cfg") + EXTRACTED_PARAMS=$(inspect_iso_boot_config "$cfg" "/${ISO_PATH}") [ -n "$EXTRACTED_PARAMS" ] && break done DEBUG "Extracted boot params from GRUB: $EXTRACTED_PARAMS" From 9e318a64b336554a4dabd2463432df6e87420a93 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Sun, 12 Apr 2026 20:47:57 -0400 Subject: [PATCH 03/10] kexec-boot.sh: add DEBUG traces for cmdline and cmdadd Add TRACE/DEBUG to understand how boot parameters flow through kexec-boot.sh, especially the cmdline and cmdadd parameters. Ref: linuxboot/heads#2083 Signed-off-by: Thierry Laurion --- initrd/bin/kexec-boot.sh | 53 +++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/initrd/bin/kexec-boot.sh b/initrd/bin/kexec-boot.sh index 4043c3118..13268df0b 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" +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 From ead931cd6d197eb8cfe3cf9114126faa6b7e789d Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Mon, 13 Apr 2026 21:41:27 -0400 Subject: [PATCH 04/10] kexec: fix hybrid ISO boot from USB file Fixes linuxboot/heads#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 --- doc/boot-process.md | 93 +++++++ doc/development.md | 72 +++++ initrd/bin/kexec-boot.sh | 8 +- initrd/bin/kexec-iso-init.sh | 469 ++++++++++++++++++++++---------- initrd/bin/kexec-parse-bls.sh | 64 ++--- initrd/bin/kexec-parse-boot.sh | 286 ++++++++++--------- initrd/bin/kexec-select-boot.sh | 14 +- initrd/bin/unpack_initramfs.sh | 12 +- initrd/etc/functions.sh | 3 + sim_iso_menu.sh | 85 ++++++ simulate_boot_menu.sh | 74 +++++ tests/iso-parser/run.sh | 93 +++++++ 12 files changed, 931 insertions(+), 342 deletions(-) create mode 100755 sim_iso_menu.sh create mode 100644 simulate_boot_menu.sh create mode 100755 tests/iso-parser/run.sh diff --git a/doc/boot-process.md b/doc/boot-process.md index 6491aaf86..1289aca5c 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, 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. diff --git a/doc/development.md b/doc/development.md index b07ec8c13..ece614222 100644 --- a/doc/development.md +++ b/doc/development.md @@ -104,3 +104,75 @@ 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) +- ❌ Actual `kexec` kernel loading +- ❌ TPM PCR extending +- ❌ Whiptail/GUI dialogs +- ❌ FUSE mount behavior inside initrd diff --git a/initrd/bin/kexec-boot.sh b/initrd/bin/kexec-boot.sh index 13268df0b..9a913081c 100755 --- a/initrd/bin/kexec-boot.sh +++ b/initrd/bin/kexec-boot.sh @@ -162,11 +162,13 @@ fi if [ "$dryrun" = "y" ]; then exit 0; fi +DEBUG "kexec-boot.sh: cmdadd='$cmdadd'" +DEBUG "kexec-boot.sh: cmdremove='$cmdremove'" 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 || +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 diff --git a/initrd/bin/kexec-iso-init.sh b/initrd/bin/kexec-iso-init.sh index 63577cab1..0fe27b1ca 100755 --- a/initrd/bin/kexec-iso-init.sh +++ b/initrd/bin/kexec-iso-init.sh @@ -5,14 +5,14 @@ # - https://wiki.archlinux.org/title/ISO_Spring_(%27Loop%27_device) # - https://a1ive.github.io/grub2_loopback.html # -# Boot Methods (detected via initrd strings analysis): -# - Dracut-based: iso-scan/filename=, findiso=, live-media=, boot=casper -# Works: Ubuntu, Debian Live, Tails, NixOS, Fedora Workstation Live, PureOS -# - Anaconda-based: inst.stage2=hd:LABEL=, inst.repo=hd:LABEL= -# Requires block device (CD-ROM or dd'd USB) - CANNOT boot from ISO file -# Examples: Fedora Silverblue, Fedora Server, Qubes OS, Kicksecure +# 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. # -# Anaconda ISOs require: dd if=image.iso of=/dev/sdX or distribution media tool. # See: https://github.com/linuxboot/heads/issues/2008 set -e -o pipefail . /etc/functions.sh @@ -61,15 +61,15 @@ else fi check_hybrid_iso() { + TRACE_FUNC local iso_path="$1" - local mbr_sig=$(dd if="$iso_path" bs=2 skip=255 count=2 2>/dev/null | xxd -p) + 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 - local efi_magic=$(dd if="$iso_path" bs=1 skip=135 count=8 2>/dev/null | xxd -p) - if [ -n "$efi_magic" ]; then - echo "hybrid" - else - echo "cdrom" - fi + echo "hybrid" else echo "cdrom" fi @@ -79,166 +79,349 @@ STATUS "Checking ISO boot capability..." ISO_BOOT_TYPE=$(check_hybrid_iso "$MOUNTED_ISO_PATH") DEBUG "ISO boot type: $ISO_BOOT_TYPE" -if [ "$ISO_BOOT_TYPE" != "hybrid" ]; then - DEBUG "Non-hybrid ISO detected (CD-ROM only)" -fi - STATUS "Mounting ISO and booting" mount -t iso9660 -o loop $MOUNTED_ISO_PATH /boot || DIE '$MOUNTED_ISO_PATH: Unable to mount /boot' -detect_iso_boot_method() { - local method="" - local found=0 +inject_casper_iso_mount() { + local initrd="/boot/casper/initrd.img" + local tmpdir="" + local newinitrd="" + local magic="" + + [ -r "$initrd" ] || return 1 + DEBUG "Injecting casper-premount/iso_mount script into initrd..." + + tmpdir=$(mktemp -d) + + magic=$(head -c 6 "$initrd" 2>/dev/null | xxd -p) || magic="" + + # Detect compression type and extract + case "$magic" in + 1f8b*) + # gzip + gzip -dc "$initrd" | cpio -id --no-absolute-filenames -D "$tmpdir" 2>/dev/null || { + rm -rf "$tmpdir" + DEBUG "Failed to extract gzip initrd" + return 1 + } + ;; + fd373a63) + # xz + xz -dc "$initrd" | cpio -id --no-absolute-filenames -D "$tmpdir" 2>/dev/null || { + rm -rf "$tmpdir" + DEBUG "Failed to extract xz initrd" + return 1 + } + ;; + *) + # Unknown or uncompressed - try both + if gzip -dc "$initrd" 2>/dev/null | cpio -id --no-absolute-filenames -D "$tmpdir" 2>/dev/null; then + : + elif xz -dc "$initrd" 2>/dev/null | cpio -id --no-absolute-filenames -D "$tmpdir" 2>/dev/null; then + : + else + rm -rf "$tmpdir" + DEBUG "Failed to extract initrd (unknown compression)" + return 1 + fi + ;; + esac - 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 + # Create casper-premount directory + mkdir -p "$tmpdir/scripts/casper-premount" - if find "$tmpdir" -type f 2>/dev/null | xargs strings 2>/dev/null | grep -qE "iso.scan|findiso"; then - method="${method}iso-scan/findiso " - found=1 - fi - if find "$tmpdir" -type f 2>/dev/null | xargs strings 2>/dev/null | grep -qE "live.media|live-media"; then - method="${method}live-media= " - found=1 - fi - if find "$tmpdir" -type f 2>/dev/null | xargs strings 2>/dev/null | grep -qE "inst.stage2|inst\.stage2"; then - method="${method}inst.stage2= " - found=1 - fi - if find "$tmpdir" -type f 2>/dev/null | xargs strings 2>/dev/null | grep -qE "inst.repo"; then - method="${method}inst.repo= " - found=1 - fi - if find "$tmpdir" -type f 2>/dev/null | xargs strings 2>/dev/null | grep -qE "boot.casper|live-boot|casper"; then - method="${method}boot=casper " - found=1 - fi - if find "$tmpdir" -type f 2>/dev/null | xargs strings 2>/dev/null | grep -qE "nixos"; then - method="${method}nixos " - found=1 + cat >"$tmpdir/scripts/casper-premount/iso_mount" <<'SCRIPT' +#!/bin/sh +PREREQ="" +prereqs() { + echo "$PREREQ" +} +case $1 in prereqs) + prereqs + exit 0 + ;; +esac + +if [ -z "$LIVEMEDIA" ]; then + exit 0 +fi + +# Only process if live-media points to an ISO file (not a block device) +if [ -b "$LIVEMEDIA" ]; then + exit 0 +fi + +# Skip if already processed (live-media is a directory we created) +if [ -d "$LIVEMEDIA" ] && [ -d "$LIVEMEDIA/casper" ]; then + exit 0 +fi + +# Only process if live-media is a regular file (ISO) +if [ ! -f "$LIVEMEDIA" ]; then + exit 0 +fi + +ISOPATH="$LIVEMEDIA" +MOUNTPOINT="/run/initramfs/iso_mount" + +mkdir -p "$MOUNTPOINT" + +if mount -o ro,loop "$ISOPATH" "$MOUNTPOINT" 2>/dev/null; then + # Write mount path to file that casper can source + # (subshell can't export to parent, so we use a file) + echo "export LIVEMEDIA=\"$MOUNTPOINT\"" > /run/initramfs/livemedia.env + echo "export LIVE_MEDIA_PATH=\"casper\"" >> /run/initramfs/livemedia.env +fi +SCRIPT + chmod +x "$tmpdir/scripts/casper-premount/iso_mount" + + # Create ORDER file so run_scripts actually runs our script + echo '/scripts/casper-premount/iso_mount "$@"' >"$tmpdir/scripts/casper-premount/ORDER" + + # Patch casper to source livemedia.env after premount scripts + # This is needed because run_scripts runs our iso_mount in a subshell, + # so the export doesn't propagate to the parent. By sourcing a file, + # casper picks up the LIVEMEDIA path set by our iso_mount script. + if [ -f "$tmpdir/scripts/casper" ]; then + if ! grep -q "livemedia.env" "$tmpdir/scripts/casper" 2>/dev/null; then + sed -i 's|^ run_scripts /scripts/casper-premount$|&\n [ -f /run/initramfs/livemedia.env ] \&\& . /run/initramfs/livemedia.env # Heads: ISO mount|' \ + "$tmpdir/scripts/casper" + DEBUG "Patched casper script to source livemedia.env" fi - rm -rf "$tmpdir" - done + fi - if [ $found -eq 0 ]; then + # Repack initrd with same compression as original + newinitrd=$(mktemp) + case "$magic" in + 1f8b*) + (cd "$tmpdir" && find . | cpio -H newc -o 2>/dev/null | gzip -c >"$newinitrd") || { + rm -rf "$tmpdir" "$newinitrd" + DEBUG "Failed to repack initrd as gzip" + return 1 + } + ;; + fd373a63) + (cd "$tmpdir" && find . | cpio -H newc -o 2>/dev/null | xz -c >"$newinitrd") || { + rm -rf "$tmpdir" "$newinitrd" + DEBUG "Failed to repack initrd as xz" + return 1 + } + ;; + *) + (cd "$tmpdir" && find . | cpio -H newc -o 2>/dev/null | gzip -c >"$newinitrd") || { + rm -rf "$tmpdir" "$newinitrd" + DEBUG "Failed to repack initrd" + return 1 + } + ;; + esac + + # Replace the initrd in the ISO mount + cp "$newinitrd" "$initrd" || { + rm -rf "$tmpdir" "$newinitrd" + DEBUG "Failed to replace initrd" return 1 - fi - echo "$method" + } + + rm -rf "$tmpdir" "$newinitrd" + DEBUG "Successfully injected casper-premount/iso_mount into initrd" return 0 } -resolve_grub_vars() { - local params="$1" - local iso_path="$2" - local resolved="" +inject_casper_iso_mount - resolved="${params//\$\{iso_path\}/$iso_path}" - resolved="${resolved//\$\{isofile\}/$iso_path}" - resolved="${resolved//\$iso_path/$iso_path}" - resolved="${resolved//\$isofile/$iso_path}" +scan_initramfs() { + local path="$1" + local tmpdir="" + local boot_content="" - echo "$resolved" -} + [ -r "$path" ] || return 1 + + tmpdir=$(mktemp -d) + /bin/bash /bin/unpack_initramfs.sh "$path" "$tmpdir" 2>/dev/null || true + + if [ -d "$tmpdir" ] && [ "$(ls -A "$tmpdir" 2>/dev/null)" ]; then + while read ko; do + name=$(basename "$ko") + case "$name" in + ext4*) supported_fses="${supported_fses}ext4 " ;; + vfat* | msdos*) supported_fses="${supported_fses}vfat " ;; + exfat*) supported_fses="${supported_fses}exfat " ;; + ntfs*) supported_fses="${supported_fses}ntfs " ;; + btrfs*) supported_fses="${supported_fses}btrfs " ;; + xfs*) supported_fses="${supported_fses}xfs " ;; + esac + done < <(find "$tmpdir" -type f \( -name "*.ko" -o -name "*.ko.xz" \) 2>/dev/null) -inspect_iso_boot_config() { - local grub_cfg="$1" - local iso_path="$2" - local boot_params="" - - [ -f "$grub_cfg" ] || return 1 - - while IFS= read -r line; do - case "$line" in - *inst.stage2=*) - params="${line##*inst.stage2=}" - params="${params%% *}" - [ -n "$params" ] && boot_params="${boot_params} inst.stage2=${params}" - ;; - *inst.repo=*) - params="${line##*inst.repo=}" - params="${params%% *}" - [ -n "$params" ] && boot_params="${boot_params} inst.repo=${params}" - ;; - *live-media=*) - params="${line##*live-media=}" - params="${params%% *}" - [ -n "$params" ] && boot_params="${boot_params} live-media=${params}" - ;; - *iso-scan/filename=* | *findiso=*) - params="${line##*iso-scan/filename=}" - [ "$params" = "$line" ] && params="${line##*findiso=}" - params="${params%% *}" - [ -n "$params" ] && boot_params="${boot_params} iso-scan/filename=${params}" - ;; - *boot=casper*) - boot_params="${boot_params} boot=casper" - ;; - esac - done <"$grub_cfg" - - if [ -n "$iso_path" ]; then - boot_params=$(resolve_grub_vars "$boot_params" "$iso_path") + boot_content=$(find "$tmpdir" -type f \( -name "*.sh" -o -name "*.conf" -o -name "*.cfg" -o -name "init" -o -name "*.txt" -o -path "*/scripts/*" -o -path "*/conf/*" \) -print 2>/dev/null | xargs cat 2>/dev/null) || boot_content="" + rm -rf "$tmpdir" + else + rm -rf "$tmpdir" + boot_content=$(strings "$path" 2>/dev/null) || true fi - echo "$boot_params" - return 0 + echo "$boot_content" | grep -qEi "iso.scan|findiso" && + supported_boot="${supported_boot}iso-scan/findiso " || true + echo "$boot_content" | grep -qEi "live.media|live-media" && + supported_boot="${supported_boot}live-media= " || true + echo "$boot_content" | grep -qEi "boot=live|rd.live.image|rd.live.squash" && + supported_boot="${supported_boot}boot=live " || true + echo "$boot_content" | grep -qEi "boot.casper|casper" && + supported_boot="${supported_boot}boot=casper " || true + echo "$boot_content" | grep -qEi "nixos" && + supported_boot="${supported_boot}nixos " || true + echo "$boot_content" | grep -qEi "inst.stage2|inst.repo" && + supported_boot="${supported_boot}anaconda " || true } -STATUS "Detecting ISO boot method..." -BOOT_METHODS=$(detect_iso_boot_method) || BOOT_METHODS="" -EXTRACTED_PARAMS="" +detect_initrd_boot_support() { + local supported_fses="" + local supported_boot="" + local initrd_paths="" -if [ -n "$BOOT_METHODS" ]; then - DEBUG "Detected boot methods: $BOOT_METHODS" -else - DEBUG "No built-in ISO boot support in initrd; checking GRUB config..." + for cfg in $(find /boot -name '*.cfg' -type f 2>/dev/null); do + [ -r "$cfg" ] || continue + # kexec-parse-boot.sh outputs: name|$kexectype|kernel $path|initrd $path|append $append + # It only produces output for valid boot entries (menuentry/LABEL lines). + # Non-boot configs (theme.cfg, memtest.cfg, etc.) produce no output. + while IFS= read -r entry; do + [ -z "$entry" ] && continue + # Extract initrd paths from the |initrd $path| field + # Multiple initrds are comma-separated; handle both 'initrd ' and 'initrd' prefix + initrd_field=$(echo "$entry" | tr '|' '\n' | grep '^initrd' | tail -1) || continue + [ -z "$initrd_field" ] && continue + initrd_val=$(echo "$initrd_field" | sed 's/^initrd //') || continue + [ -z "$initrd_val" ] && continue + # Split comma-separated initrds (same as echo_entry does) + for init in $(echo "$initrd_val" | tr ',' ' '); do + [ -z "$init" ] && continue + case " $initrd_paths " in + *" $init "*) continue ;; + esac + initrd_paths="${initrd_paths}${init} " + done + done < <(/bin/bash /bin/kexec-parse-boot.sh /boot "$cfg" 2>/dev/null || true) + done + [ -z "$initrd_paths" ] && return 0 + + total=$(echo $initrd_paths | wc -w) + count=0 + for ipath in $initrd_paths; do + count=$((count + 1)) + DEBUG "Scanning initrd $count of $total: $ipath" + full_path="/boot/${ipath#/}" + [ -r "$full_path" ] && scan_initramfs "$full_path" + done + + [ -n "$supported_fses" ] && echo "fs:$supported_fses" + [ -n "$supported_boot" ] && echo "boot:$supported_boot" + return 0 +} + +extract_boot_params_from_cfg() { for cfg in $(find /boot -name '*.cfg' -type f 2>/dev/null); do [ -r "$cfg" ] || continue - if grep -qE "iso.scan|findiso|live.media=|boot=casper" "$cfg" 2>/dev/null; then - BOOT_METHODS="${BOOT_METHODS}grub " - break - fi - if grep -qE "inst.repo=|inst.stage2=" "$cfg" 2>/dev/null; then - BOOT_METHODS="${BOOT_METHODS}anaconda " + if ! grep -qE '^[^#]*(linux|menuentry|label|append)[[:space:]]' "$cfg" 2>/dev/null; then + continue fi + 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 +} - if [ -n "$BOOT_METHODS" ]; then - DEBUG "Found boot support: $BOOT_METHODS" - else - WARN "ISO may not boot from USB file: no boot support in initrd" - 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 have iso-scan/findiso/live-media in its initrd - it was designed for CD/DVD or dd-to-USB.\n\nKernel parameters passed externally may not be sufficient.\n\nTry:\n- Use distribution-specific ISO (e.g., Debian hd-media)\n- Write ISO directly to USB with dd\n- Use a live USB image\n\nDo you want to proceed anyway?" \ - 0 80; then - DIE "ISO boot cancelled - unsupported ISO on USB file" - fi - else - INPUT "Proceed with boot anyway? [y/N]:" -n 1 response - [ "$response" != "y" ] && [ "$response" != "Y" ] && DIE "ISO boot cancelled - unsupported ISO on USB file" - fi - fi +STATUS "Detecting USB filesystem and boot method support..." +SUPPORTED_FSES="" +SUPPORTED_BOOT="" +CFG_BOOT="" +DETECTED_METHODS="" + +STATUS "Detecting boot method support in initramfs..." +STATUS "Scanning initrds for filesystem and boot method support..." +tmp_support=$(detect_initrd_boot_support 2>/dev/null) || tmp_support="" +SUPPORTED_FSES=$(echo "$tmp_support" | grep "^fs:" | sed 's/^fs://') || SUPPORTED_FSES="" +SUPPORTED_BOOT=$(echo "$tmp_support" | grep "^boot:" | sed 's/^boot://') || SUPPORTED_BOOT="" +DEBUG "SUPPORTED_FSES='$SUPPORTED_FSES'" +DEBUG "SUPPORTED_BOOT from initrd='$SUPPORTED_BOOT'" +STATUS_OK "Detected boot method support in initramfs" + +if [ -z "$SUPPORTED_BOOT" ]; then + DEBUG "No boot method in initrd, scanning *.cfg files..." + CFG_BOOT=$(extract_boot_params_from_cfg 2>/dev/null | grep "^cfg:" | sed 's/^cfg://') || CFG_BOOT="" + DEBUG "CFG_BOOT='$CFG_BOOT'" +else + DEBUG "Boot method found in initrd, skipping cfg extraction" fi -if echo "$BOOT_METHODS" | grep -qE "anaconda|inst.repo|inst.stage2"; then - DEBUG "Anaconda-based ISO detected (inst.stage2=)" +if [ -n "$SUPPORTED_FSES" ]; then + DEBUG "Initrd supports USB filesystems: $SUPPORTED_FSES" + DEBUG "Extracting USB device filesystem type from blkid..." + DEV_FSTYPE=$(blkid "$DEV" 2>/dev/null | tail -1 | grep -oE 'TYPE="[^"]+"' | sed 's/TYPE="//;s/"$//') || DEV_FSTYPE="" + DEBUG "USB device filesystem type: '$DEV_FSTYPE'" + if [ -n "$DEV_FSTYPE" ] && ! echo "$SUPPORTED_FSES" | grep -q "$DEV_FSTYPE" 2>/dev/null; then + WARN "USB filesystem ($DEV_FSTYPE) may not be supported by this ISO's initrd" + DEBUG "Supported filesystems: $SUPPORTED_FSES" + fi || true + DEBUG "FS compatibility check: passed" +else + 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 -if [ -z "$BOOT_METHODS" ]; then - for cfg in $(find /boot -name '*.cfg' -type f 2>/dev/null); do - EXTRACTED_PARAMS=$(inspect_iso_boot_config "$cfg" "/${ISO_PATH}") - [ -n "$EXTRACTED_PARAMS" ] && break - done - DEBUG "Extracted boot params from GRUB: $EXTRACTED_PARAMS" +STATUS "Detecting boot method..." +if [ -n "$SUPPORTED_BOOT" ]; then + DETECTED_METHODS="$SUPPORTED_BOOT" + DEBUG "Initrd supports boot methods: $DETECTED_METHODS" +elif [ -n "$CFG_BOOT" ]; then + DETECTED_METHODS="$CFG_BOOT" + DEBUG "Boot config (*.cfg) indicates boot methods: $DETECTED_METHODS" +else + DEBUG "No boot method detected in initrd or *.cfg files" +fi + +DEBUG "DETECTED_METHODS='$DETECTED_METHODS'" +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 -# Detect USB stick filesystem and validate initrd supports it -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" +DEBUG "Proceeding to kexec-select-boot..." +DEV_UUID=$(blkid "$DEV" 2>/dev/null | tail -1 | tr " " "\n" | grep UUID | cut -d\" -f2) || DEV_UUID="" +DEBUG "DEV_UUID='$DEV_UUID'" +ADD="live-media=$MOUNTED_ISO_PATH live-media-path=casper fromiso=$MOUNTED_ISO_PATH img_dev=$DEV iso-scan/filename=/${ISO_PATH} img_loop=$ISO_PATH iso-scan/auto=true" REMOVE="" paramsdir="/media/kexec_iso/$ISO_PATH" @@ -260,6 +443,6 @@ DEBUG "Overriding ISO kernel arguments with suppressions: $REMOVE" # Call kexec and indicate that hashes have been verified DO_WITH_DEBUG kexec-select-boot.sh -b /boot -d /media -p "$paramsdir" \ - -a "$ADD" -r "$REMOVE" -c "*.cfg" -u -i + -a "$ADD" -r "$REMOVE" -c "*.cfg" -u -i -s DIE "Something failed in selecting boot" diff --git a/initrd/bin/kexec-parse-bls.sh b/initrd/bin/kexec-parse-bls.sh index 98b1a3020..5c5e80f20 100755 --- a/initrd/bin/kexec-parse-bls.sh +++ b/initrd/bin/kexec-parse-bls.sh @@ -1,15 +1,13 @@ #!/bin/bash set -e -o pipefail . /etc/functions.sh -TRACE_FUNC - bootdir="$1" file="$2" blsdir="$3" kernelopts="" if [ -z "$bootdir" -o -z "$file" ]; then - DIE "Usage: $0 /boot /boot/grub/grub.cfg blsdir" + DIE "Usage: $0 /boot /path/to/grub.cfg blsdir" fi reset_entry() { @@ -21,7 +19,7 @@ reset_entry() { append="$kernelopts" } -filedir=`dirname $file` +filedir=$(dirname $file) bootdir="${bootdir%%/}" bootlen="${#bootdir}" appenddir="${filedir:$bootlen}" @@ -62,41 +60,39 @@ echo_entry() { bls_entry() { # add info to menuentry - trimcmd=`echo $line | tr '\t ' ' ' | tr -s ' '` - cmd=`echo $trimcmd | cut -d\ -f1` - val=`echo $trimcmd | cut -d\ -f2-` + trimcmd=$(echo $line | tr '\t ' ' ' | tr -s ' ') + cmd=$(echo "$trimcmd" | sed 's/^[[:space:]]*//' | cut -d\ -f1) + val=$(echo "$trimcmd" | sed 's/^[[:space:]]*//' | cut -d\ -f2-) case $cmd in - title) - name=$val - ;; - linux*) - kernel=${val#"$bootdir"} - ;; - initrd*) - initrd=${val#"$bootdir"} - ;; - options) - # default is "options $kernelopts" - # need to substitute that variable if set in .cfg/grubenv - append=`echo "$val" | sed "s@\\$kernelopts@$kernelopts@"` - ;; + title) + name=$val + ;; + linux*) + kernel=${val#"$bootdir"} + ;; + initrd*) + initrd=${val#"$bootdir"} + ;; + options) + # default is "options $kernelopts" + # need to substitute that variable if set in .cfg/grubenv + append=$(echo "$val" | sed "s@\$kernelopts@$kernelopts@") + ;; esac } # This is the default append value if no options field in bls entry -grep -q "set default_kernelopts" "$file" && - kernelopts=`grep "set default_kernelopts" "$file" | - tr "'" "\"" | cut -d\" -f 2` +grep -q "set default_kernelopts" "$file" && + kernelopts=$(grep "set default_kernelopts" "$file" | + tr "'" "\"" | cut -d\" -f 2) [ -f "$grubenv" ] && grep -q "^kernelopts" "$grubenv" && - kernelopts=`grep "^kernelopts" "$grubenv" | tr '@' '_' | cut -d= -f 2-` + kernelopts=$(grep "^kernelopts" "$grubenv" | tr '@' '_' | cut -d= -f 2-) reset_entry find $blsdir -type f -name \*.conf | -while read f -do - while read line - do - bls_entry - done < "$f" - echo_entry - reset_entry -done + while read f; do + while read line; do + bls_entry + done <"$f" + echo_entry + reset_entry + done diff --git a/initrd/bin/kexec-parse-boot.sh b/initrd/bin/kexec-parse-boot.sh index 852bc00ee..627756ea0 100755 --- a/initrd/bin/kexec-parse-boot.sh +++ b/initrd/bin/kexec-parse-boot.sh @@ -2,13 +2,11 @@ set -e -o pipefail . /etc/functions.sh -TRACE_FUNC - bootdir="$1" file="$2" if [ -z "$bootdir" -o -z "$file" ]; then - DIE "Usage: $0 /boot /boot/grub/grub.cfg" + DIE "Usage: $0 /boot /path/to/config.cfg" fi reset_entry() { @@ -20,22 +18,13 @@ reset_entry() { append="" } -filedir=`dirname $file` -DEBUG "filedir= $filedir" +filedir=$(dirname $file) bootdir="${bootdir%%/}" -DEBUG "bootdir= $bootdir" bootlen="${#bootdir}" -DEBUG "bootlen= $bootlen" appenddir="${filedir:$bootlen}" -DEBUG "appenddir= $appenddir" fix_path() { path="$@" - if [ "${path:0:1}" != "/" ]; then - DEBUG "fix_path: path was $@" - path="$appenddir/$path" - DEBUG "fix_path: path is now $path" - fi } # GRUB kernel lines (linux/multiboot) can include a command line. Check whether @@ -45,8 +34,7 @@ check_path() { checkpath="$1" firstval="$(echo "$checkpath" | cut -d\ -f1)" if ! [ -r "$bootdir$firstval" ]; then - DEBUG "$bootdir$firstval doesn't exist" - return 1; + return 1 fi return 0 } @@ -55,52 +43,46 @@ echo_entry() { if [ -z "$kernel" ]; then return; fi fix_path $kernel - # The kernel must exist - if it doesn't, ignore this entry, it - # wouldn't work anyway. This could happen if there was a - # GRUB variable in the kernel path, etc. - if ! check_path "$path"; then return; fi + check_path "$path" 2>/dev/null || true entry="$name|$kexectype|kernel $path" case "$kexectype" in - elf) - if [ -n "$initrd" ]; then - for init in $(echo $initrd | tr ',' ' '); do - fix_path $init - # The initrd must also exist - if ! check_path "$path"; then return; fi - entry="$entry|initrd $path" - done - fi - if [ -n "$append" ]; then - entry="$entry|append $append" - fi - ;; - multiboot|xen) - entry="$entry$modules" - ;; - *) - return - ;; + elf) + if [ -n "$initrd" ]; then + for init in $(echo $initrd | tr ',' ' '); do + fix_path $init + check_path "$path" 2>/dev/null || true + entry="$entry|initrd $path" + done + fi + if [ -n "$append" ]; then + entry="$entry|append $append" + fi + ;; + multiboot | xen) + entry="$entry$modules" + ;; + *) + return + ;; esac - # Double-expand here in case there are variables in the kernel - # parameters - some configs do this and can boot with empty - # expansions (Debian Live ISOs use this for loopback boots) echo $(eval "echo \"$entry\"") } search_entry() { case $line in - menuentry* | MENUENTRY* ) - state="grub" - reset_entry - name=`echo $line | tr "'" "\"" | cut -d\" -f 2` - ;; - - label* | LABEL* ) - state="syslinux" - reset_entry - name=`echo $line | cut -c6- ` + menuentry* | MENUENTRY*) + state="grub" + reset_entry + name=$(echo $line | tr "'" "\"" | cut -d\" -f 2) + ;; + + label* | LABEL*) + state="syslinux" + reset_entry + name=$(echo $line | cut -c6-) + ;; esac } @@ -112,39 +94,59 @@ grub_entry() { fi # add info to menuentry - trimcmd=`echo $line | tr '\t ' ' ' | tr -s ' '` - cmd=`echo $trimcmd | cut -d\ -f1` - val=`echo $trimcmd | cut -d\ -f2-` + trimcmd=$(echo $line | tr '\t ' ' ' | tr -s ' ') + cmd=$(echo "$trimcmd" | sed 's/^[[:space:]]*//' | cut -d\ -f1) + val=$(echo "$trimcmd" | sed 's/^[[:space:]]*//' | cut -d\ -f2-) case $cmd in - multiboot*) - # TODO: differentiate between Xen and other multiboot kernels - kexectype="xen" - kernel="$val" - DEBUG " grub_entry multiboot kernel= $kernel" - ;; - module*) - case $val in - --nounzip*) val=`echo $val | cut -d\ -f2-` ;; - esac - fix_path $val - modules="$modules|module $path" - DEBUG " grub_entry linux modules= $modules" - ;; - linux*) - # Some configs have a device specification in the kernel - # or initrd path. Assume this would be /boot and remove - # it. Keep the '/' following the device, since this - # path is relative to the device root, not the config - # location. - 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-` - ;; - initrd*) - # Trim off device specification as above - initrd="$(echo "$val" | sed "s/([^)]*)//g")" - DEBUG " grub_entry: linux initrd= $initrd" - ;; + multiboot*) + # TODO: differentiate between Xen and other multiboot kernels + kexectype="xen" + kernel="$val" + ;; + module*) + case $val in + --nounzip*) val=$(echo $val | cut -d\ -f2-) ;; + esac + fix_path $val + modules="$modules|module $path" + ;; + linux*) + # Some configs have a device specification in the kernel + # or initrd path. Assume this would be /boot and remove + # it. Keep the '/' following the device, since this + # path is relative to the device root, not the config + # location. + 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. + # Also strip ISO boot parameters that are injected via -a by kexec-iso-init.sh + # so they don't clutter the boot entry display. They are added to the kexec + # command separately via cmdadd. + 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|fromiso=[^ ]*| |g' \ + -e 's|img_dev=[^ ]*| |g' \ + -e 's|img_loop=[^ ]*| |g' \ + -e 's|iso=[^ ]*| |g' \ + -e 's|live-media=[^ ]*| |g' \ + -e 's| *| |g' \ + -e 's|^ ||' \ + -e 's| $||') + # Strip GRUB bootloader marker "---" (used by Ubuntu) used as append/initrd separator + append=$(echo "$append" | sed 's|[[:space:]]*---[[:space:]]*| |g' | sed 's|^ ||;s| $||') + + ;; + initrd*) + # Trim off device specification as above + initrd="$(echo "$val" | sed "s/([^)]*)//g")" + ;; esac } @@ -156,10 +158,10 @@ syslinux_end() { newappend="" for param in $append; do case $param in - initrd=*) - initrd=`echo $param | cut -d\= -f2` - ;; - *) newappend="$newappend $param" ;; + initrd=*) + initrd="${param#initrd=}" + ;; + *) newappend="$newappend $param" ;; esac done append="${newappend##' '}" @@ -171,87 +173,81 @@ syslinux_end() { } syslinux_multiboot_append() { - splitval=`echo "${val// --- /|}" | tr '|' '\n'` - while read line - do + splitval=$(echo "${val// --- /|}" | tr '|' '\n') + while read line; do if [ -z "$kernel" ]; then kernel="$line" else fix_path $line modules="$modules|module $path" fi - done << EOF + done </tmp/whiptail || DIE "Aborting boot attempt" @@ -194,8 +195,7 @@ confirm_menu_option() { STATUS "Confirm boot details for $name:" INFO "$option" - INPUT "Confirm selection by pressing 'Y' or 'd' to make default [Y,d]:" -n 1 option_confirm - [ -z "$option_confirm" ] && option_confirm="y" + INPUT "Confirm selection by pressing 'y', make default with 'd':" -n 1 option_confirm fi } @@ -220,11 +220,11 @@ scan_options() { save_default_option() { if [ "$gui_menu" != "y" ]; then - INPUT "Saving a default will modify the disk. Proceed? [Y/n]:" -n 1 default_confirm - [ -z "$default_confirm" ] && default_confirm="y" + INPUT "Saving a default will modify the disk. Proceed? (Y/n):" -n 1 default_confirm fi - if [[ "$default_confirm" = [yY] ]]; then + [ "$default_confirm" = "" ] && default_confirm="y" + if [[ "$default_confirm" = "y" || "$default_confirm" = "Y" ]]; then if kexec-save-default.sh \ -b "$bootdir" \ -d "$paramsdev" \ @@ -288,7 +288,7 @@ user_select() { # No default expected boot parameters, ask user option_confirm="" - while [[ "$option_confirm" != [yY] && "$option_confirm" != "d" ]]; do + while [ "$option_confirm" != "y" -a "$option_confirm" != "d" ]; do get_menu_option # In force boot mode, no need offer the option to set a default, just boot if [[ "$force_boot" = "y" || "$skip_confirm" = "y" ]]; then diff --git a/initrd/bin/unpack_initramfs.sh b/initrd/bin/unpack_initramfs.sh index 25f0b5caf..fe0b0a446 100755 --- a/initrd/bin/unpack_initramfs.sh +++ b/initrd/bin/unpack_initramfs.sh @@ -69,21 +69,18 @@ unpack_first_segment() { # lib/decompress.c (gzip) case "$magic" in 00*) - DEBUG "archive segment $magic: uncompressed cpio" # Skip zero bytes and copy the first nonzero byte consume_zeros # Copy the remaining data cat ;; 303730373031* | 303730373032*) # plain cpio - DEBUG "archive segment $magic: plain cpio" # Unpack the plain cpio, this stops reading after the trailer unpack_cpio # Copy the remaining data cat ;; 1f8b* | 1f9e*) # gzip - DEBUG "archive segment $magic: gzip" # gunzip won't stop when reaching the end of the gzipped member, # so we can't read another segment after this. We can't # reasonably determine the member length either, this requires @@ -91,11 +88,9 @@ unpack_first_segment() { gunzip | unpack_cpio ;; fd37*) # xz - DEBUG "archive segment $magic: xz" unxz | unpack_cpio ;; 28b5*) # zstd - DEBUG "archive segment $magic: zstd" # Like gunzip, this will not stop when reaching the end of the # frame, and determining the frame length requires walking all # of its blocks. @@ -106,7 +101,7 @@ unpack_first_segment() { # The following are magic values for other compression formats # but not added because not tested. # TODO: open an issue for unsupported magic number reported on DIE. - # + # #425a*) # bzip2 # DEBUG "archive segment $magic: bzip2" # bunzip2 | unpack_cpio @@ -127,12 +122,9 @@ unpack_first_segment() { esac ) <"$unpack_archive" >"$rest_archive" - orig_size="$(stat -c %s "$unpack_archive")" - rest_size="$(stat -c %s "$rest_archive")" - DEBUG "archive segment $magic: $((orig_size - rest_size)) bytes" } -DEBUG "Unpacking $INITRAMFS_ARCHIVE to $DEST_DIR" +TRACE "Unpacking $INITRAMFS_ARCHIVE to $DEST_DIR" next_archive="$INITRAMFS_ARCHIVE" rest_archive="/tmp/unpack_initramfs_rest" diff --git a/initrd/etc/functions.sh b/initrd/etc/functions.sh index 20802909f..11b089fc9 100644 --- a/initrd/etc/functions.sh +++ b/initrd/etc/functions.sh @@ -2479,6 +2479,9 @@ scan_boot_options() { if [ -r "$option_file" ]; then rm "$option_file"; fi for i in $(find "$bootdir" -name "$config"); do + case "$i" in + *EFI* | *efi* | *x86_64-efi*) continue ;; + esac DO_WITH_DEBUG kexec-parse-boot.sh "$bootdir" "$i" >>"$option_file" done # FC29/30+ may use BLS format grub config files diff --git a/sim_iso_menu.sh b/sim_iso_menu.sh new file mode 100755 index 000000000..5688c870a --- /dev/null +++ b/sim_iso_menu.sh @@ -0,0 +1,85 @@ +#!/bin/bash +# Simulate boot menu parsing for all ISOs using the real kexec-parse-boot.sh +# Tests parser output without menu-level filtering + +ISO_DIR="/home/user/Downloads/ISOs" +SIM_BOOT="/tmp/sim_iso_boot" +STUB_ETC="/tmp/host_etc/functions.sh" + +# Create stub functions.sh if not exists +[ -f "$STUB_ETC" ] || { + mkdir -p "$(dirname "$STUB_ETC")" + cat >"$STUB_ETC" <<'STUB' +TRACE_FUNC() { :; } +DEBUG() { :; } +TRACE() { :; } +ERROR() { echo "ERROR: $*" >&2; } +DIE() { echo "DIE: $*" >&2; exit 1; } +DO_WITH_DEBUG() { "$@"; } +STUB +} + +echo "============================================================" +printf "%-70s %6s\n" "ISO" "ENTRIES" +echo "============================================================" + +for iso in "$ISO_DIR"/*.iso; do + [ -f "$iso" ] || continue + + iso_name=$(basename "$iso") + echo "" + echo ">>> $iso_name" + + mnt="/tmp/sim_iso_$$" + mkdir -p "$mnt" + fuseiso -n "$iso" "$mnt" 2>/dev/null || { + rmdir "$mnt" 2>/dev/null + continue + } + + rm -rf "$SIM_BOOT" + ln -s "$mnt" "$SIM_BOOT" + + output=$(mktemp) + + # Create patched parser that uses our stub + PATCHED_PARSER="/tmp/sim_parser_$$.sh" + sed 's|\. /etc/functions\.sh|. /tmp/host_etc/functions.sh|' \ + /home/user/heads-master/initrd/bin/kexec-parse-boot.sh >"$PATCHED_PARSER" + chmod +x "$PATCHED_PARSER" + + # Parse all non-EFI configs + for cfg in $(find "$mnt" -name '*.cfg' -type f 2>/dev/null | grep -v -i -E 'efi|x86_64-efi'); do + "$PATCHED_PARSER" "$SIM_BOOT" "$cfg" >>"$output" 2>/dev/null || true + done + + count=$(wc -l <"$output" 2>/dev/null || echo 0) + echo " entries: $count" + echo "" + + n=0 + while read -r entry; do + n=$((n + 1)) + name=$(echo "$entry" | sed -n 's/|kernel .*$//; s/|elf$//; s/|xen$//; p' | head -c 50) + kernel=$(echo "$entry" | sed -n 's/.*|kernel \([^|]*\).*/\1/p' | head -c 50) + initrd=$(echo "$entry" | sed -n 's/.*|initrd \([^|]*\).*/\1/p' | head -c 30) + append=$(echo "$entry" | sed -n 's/.*|append \(.*\)/\1/p' | head -c 50) + echo " $n. [$name]" + echo " KERNEL: $kernel" + [ -n "$initrd" ] && echo " INITRD: $initrd" + [ -n "$append" ] && echo " APPEND: $append" + [ $n -ge 10 ] && { + remaining=$((count - 10)) + [ $remaining -gt 0 ] && echo " ... and $remaining more" + break + } + done <"$output" + + rm -f "$output" "$PATCHED_PARSER" + rm -f "$SIM_BOOT" + fusermount -zu "$mnt" 2>/dev/null + rmdir "$mnt" +done + +echo "" +echo "============================================================" diff --git a/simulate_boot_menu.sh b/simulate_boot_menu.sh new file mode 100644 index 000000000..0cece47a7 --- /dev/null +++ b/simulate_boot_menu.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# Simulate boot menu entries for all ISOs — show counts before/after dedup+filtering + +ISO_DIR="/media/ISOs" +PARSER="/home/user/heads-master/initrd/bin/kexec-parse-boot.sh" + +total_before=0 +total_after=0 + +echo "============================================================" +printf "%-70s %6s %6s\n" "ISO" "BEFORE" "AFTER" +echo "============================================================" + +for iso in "$ISO_DIR"/*.iso "$ISO_DIR"/**/*.iso; do + [ -f "$iso" ] || continue + + # Mount ISO temporarily + mnt=$(mktemp -d) + mount -t iso9660 -o loop,ro "$iso" "$mnt" 2>/dev/null || { + rmdir "$mnt" 2>/dev/null + continue + } + + # Collect all boot entries from all .cfg files + raw_entries=$(mktemp) + tmp_menu=$(mktemp) + filtered=$(mktemp) + + for cfg in $(find "$mnt" -name '*.cfg' -type f 2>/dev/null); do + "$PARSER" /boot "$cfg" 2>/dev/null >>"$raw_entries" || true + done + + # Count before + before=$(wc -l <"$raw_entries" 2>/dev/null || echo 0) + total_before=$((total_before + before)) + + # Deduplicate (sort | uniq) — like -u flag + sort -r "$raw_entries" 2>/dev/null | uniq >"$tmp_menu" + + # Filter installer noise (like -s mode does) + grep -vEi '\|[^|]*\b(Install|Expert install|Automated install|Rescue mode|Start installer)\b' \ + "$tmp_menu" >"$filtered" 2>/dev/null || true + + # Count after + after=$(wc -l <"$filtered" 2>/dev/null || echo 0) + total_after=$((total_after + after)) + + # Show top entries + echo "" + echo ">>> $(basename "$iso") ($before -> $after)" + n=0 + while read -r entry; do + n=$((n + 1)) + name=$(echo "$entry" | cut -d'|' -f1 | head -c 50) + kernel=$(echo "$entry" | cut -d'|' -f3 | sed 's|kernel ||' | head -c 40) + append=$(echo "$entry" | cut -d'|' -f5 | sed 's|append ||' | head -c 40) + echo " $n. [$name]" + echo " KERNEL: $kernel" + [ -n "$append" ] && echo " APPEND: $append" + [ $n -ge 10 ] && { + echo " ... and $((after - 10)) more" + break + } + done <"$filtered" + + umount "$mnt" 2>/dev/null + rmdir "$mnt" 2>/dev/null + rm -f "$raw_entries" "$tmp_menu" "$filtered" +done + +echo "" +echo "============================================================" +printf "%-70s %6d %6d\n" "TOTAL" "$total_before" "$total_after" +echo "============================================================" diff --git a/tests/iso-parser/run.sh b/tests/iso-parser/run.sh new file mode 100755 index 000000000..cf9151d32 --- /dev/null +++ b/tests/iso-parser/run.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# ISO parser test — mirrors how scan_boot_options() calls the parser in Heads +# Usage: ISO_DIR=/path/to/isos ./run.sh + +set -e + +: "${ISO_DIR:=/home/user/Downloads/ISOs}" +: "${PARSER:=$(dirname "$0")/../../initrd/bin/kexec-parse-boot.sh}" +: "${FUNCTIONS:=$(dirname "$0")/../../initrd/etc/functions.sh}" + +for cmd in fuseiso fusermount; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "Missing: $cmd" + echo "Install: apt install fuseiso # Debian/Ubuntu" + echo " pacman -S fuseiso # Arch" + echo " dnf install fuseiso # Fedora" + exit 1 + fi +done + +if [ ! -d "$ISO_DIR" ]; then + echo "ISO_DIR '$ISO_DIR' does not exist" + echo "Set ISO_DIR=/path/to/isos before running" + exit 1 +fi + +if [ ! -f "$PARSER" ]; then + echo "Parser not found: $PARSER" + echo "Set PARSER=/path/to/kexec-parse-boot.sh" + exit 1 +fi + +if [ ! -f "$FUNCTIONS" ]; then + echo "Functions not found: $FUNCTIONS" + echo "Set FUNCTIONS=/path/to/functions.sh" + exit 1 +fi + +STUB=$(mktemp) +cat >"$STUB" <<'STUB' +TRACE_FUNC() { :; } +DEBUG() { :; } +ERROR() { echo "ERROR: $*" >&2; } +DIE() { echo "DIE: $*" >&2; exit 1; } +WARN() { echo "WARN: $*" >&2; } +check_config() { :; } +STUB + +TEMP_PARSER=$(mktemp) +# Stub out TRACE/DEBUG/WARN before sourcing real functions.sh +sed "s|^\. /etc/functions\.sh|. $STUB|" "$PARSER" >"$TEMP_PARSER" +chmod +x "$TEMP_PARSER" + +printf "%-60s %8s %10s %s\n" "ISO" "ENTRIES" "HYBRID" "SAMPLE BOOT PARAMS" +printf "%-60s %8s %10s %s\n" "---" "-------" "------" "------------------" + +for iso in "$ISO_DIR"/*.iso; do + [ -f "$iso" ] || continue + mnt=$(mktemp -d) + if ! fuseiso "$iso" "$mnt" 2>/dev/null; then + rmdir "$mnt" 2>/dev/null + printf "%-60s %8s %10s %s\n" "$(basename "$iso")" "SKIP" "?" "fuseiso failed" + continue + fi + if [ ! -d "$mnt/boot" ] && [ ! -d "$mnt/isolinux" ]; then + fusermount -u "$mnt" 2>/dev/null + rmdir "$mnt" 2>/dev/null + printf "%-60s %8s %10s %s\n" "$(basename "$iso")" "SKIP" "?" "mount empty" + continue + fi + sim=$(mktemp -u) + rm -rf "$sim" + ln -sf "$mnt" "$sim" + + entries=$(mktemp) + >"$entries" + for cfg in $(find "$mnt" -name "*.cfg" -type f 2>/dev/null | grep -v -i -E "efi|x86_64-efi"); do + "$TEMP_PARSER" "$sim" "$cfg" >>"$entries" 2>/dev/null || true + done + + count=$(wc -l <"$entries" 2>/dev/null || echo 0) + boot=$(sed -n 's/.*|append \(.*\)/\1/p' "$entries" 2>/dev/null | head -1) + mbr=$(dd if="$iso" bs=1 skip=510 count=2 2>/dev/null | od -An -tx1 | tr -d ' \n') + hybrid=$([ "$mbr" = "55aa" ] && echo "yes" || echo "no") + + printf "%-60s %8s %10s %s\n" "$(basename "$iso")" "$count" "$hybrid" "${boot:0:60}" + + fusermount -u "$mnt" 2>/dev/null || umount "$mnt" 2>/dev/null + rmdir "$mnt" 2>/dev/null + rm -rf "$sim" "$entries" +done + +rm -f "$STUB" "$TEMP_PARSER" From a0f7282e8267635536544ace0497f66e5e9734c4 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Wed, 15 Apr 2026 15:25:23 -0400 Subject: [PATCH 05/10] kexec-iso-init: add initramfs detection for boot method and filesystem support - Add scan_initramfs() to detect filesystem drivers (ext4, vfat, exfat, etc.) and boot methods (iso-scan, live-media, boot=live, boot=casper, etc.) - Add detect_initrd_boot_support() to scan initrds from parsed boot entries - Add extract_boot_params_from_cfg() to scan *.cfg files for boot params - Warn if USB filesystem is not supported by ISO initramfs - Warn if no boot method detected, offer to try anyway - Keep ADD params matching origin/master (fromiso=/dev/disk/by-uuid/...) Signed-off-by: Thierry Laurion --- initrd/bin/kexec-iso-init.sh | 211 +---------------------------------- 1 file changed, 4 insertions(+), 207 deletions(-) diff --git a/initrd/bin/kexec-iso-init.sh b/initrd/bin/kexec-iso-init.sh index 0fe27b1ca..e5cdbaf09 100755 --- a/initrd/bin/kexec-iso-init.sh +++ b/initrd/bin/kexec-iso-init.sh @@ -1,19 +1,5 @@ #!/bin/bash -# 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 +# Boot from signed ISO set -e -o pipefail . /etc/functions.sh . /etc/gui_functions.sh @@ -60,176 +46,11 @@ else NOTE "Proceeding with unsigned ISO boot" fi -check_hybrid_iso() { - TRACE_FUNC - 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' -inject_casper_iso_mount() { - local initrd="/boot/casper/initrd.img" - local tmpdir="" - local newinitrd="" - local magic="" - - [ -r "$initrd" ] || return 1 - DEBUG "Injecting casper-premount/iso_mount script into initrd..." - - tmpdir=$(mktemp -d) - - magic=$(head -c 6 "$initrd" 2>/dev/null | xxd -p) || magic="" - - # Detect compression type and extract - case "$magic" in - 1f8b*) - # gzip - gzip -dc "$initrd" | cpio -id --no-absolute-filenames -D "$tmpdir" 2>/dev/null || { - rm -rf "$tmpdir" - DEBUG "Failed to extract gzip initrd" - return 1 - } - ;; - fd373a63) - # xz - xz -dc "$initrd" | cpio -id --no-absolute-filenames -D "$tmpdir" 2>/dev/null || { - rm -rf "$tmpdir" - DEBUG "Failed to extract xz initrd" - return 1 - } - ;; - *) - # Unknown or uncompressed - try both - if gzip -dc "$initrd" 2>/dev/null | cpio -id --no-absolute-filenames -D "$tmpdir" 2>/dev/null; then - : - elif xz -dc "$initrd" 2>/dev/null | cpio -id --no-absolute-filenames -D "$tmpdir" 2>/dev/null; then - : - else - rm -rf "$tmpdir" - DEBUG "Failed to extract initrd (unknown compression)" - return 1 - fi - ;; - esac - - # Create casper-premount directory - mkdir -p "$tmpdir/scripts/casper-premount" - - cat >"$tmpdir/scripts/casper-premount/iso_mount" <<'SCRIPT' -#!/bin/sh -PREREQ="" -prereqs() { - echo "$PREREQ" -} -case $1 in prereqs) - prereqs - exit 0 - ;; -esac - -if [ -z "$LIVEMEDIA" ]; then - exit 0 -fi - -# Only process if live-media points to an ISO file (not a block device) -if [ -b "$LIVEMEDIA" ]; then - exit 0 -fi - -# Skip if already processed (live-media is a directory we created) -if [ -d "$LIVEMEDIA" ] && [ -d "$LIVEMEDIA/casper" ]; then - exit 0 -fi - -# Only process if live-media is a regular file (ISO) -if [ ! -f "$LIVEMEDIA" ]; then - exit 0 -fi - -ISOPATH="$LIVEMEDIA" -MOUNTPOINT="/run/initramfs/iso_mount" - -mkdir -p "$MOUNTPOINT" - -if mount -o ro,loop "$ISOPATH" "$MOUNTPOINT" 2>/dev/null; then - # Write mount path to file that casper can source - # (subshell can't export to parent, so we use a file) - echo "export LIVEMEDIA=\"$MOUNTPOINT\"" > /run/initramfs/livemedia.env - echo "export LIVE_MEDIA_PATH=\"casper\"" >> /run/initramfs/livemedia.env -fi -SCRIPT - chmod +x "$tmpdir/scripts/casper-premount/iso_mount" - - # Create ORDER file so run_scripts actually runs our script - echo '/scripts/casper-premount/iso_mount "$@"' >"$tmpdir/scripts/casper-premount/ORDER" - - # Patch casper to source livemedia.env after premount scripts - # This is needed because run_scripts runs our iso_mount in a subshell, - # so the export doesn't propagate to the parent. By sourcing a file, - # casper picks up the LIVEMEDIA path set by our iso_mount script. - if [ -f "$tmpdir/scripts/casper" ]; then - if ! grep -q "livemedia.env" "$tmpdir/scripts/casper" 2>/dev/null; then - sed -i 's|^ run_scripts /scripts/casper-premount$|&\n [ -f /run/initramfs/livemedia.env ] \&\& . /run/initramfs/livemedia.env # Heads: ISO mount|' \ - "$tmpdir/scripts/casper" - DEBUG "Patched casper script to source livemedia.env" - fi - fi - - # Repack initrd with same compression as original - newinitrd=$(mktemp) - case "$magic" in - 1f8b*) - (cd "$tmpdir" && find . | cpio -H newc -o 2>/dev/null | gzip -c >"$newinitrd") || { - rm -rf "$tmpdir" "$newinitrd" - DEBUG "Failed to repack initrd as gzip" - return 1 - } - ;; - fd373a63) - (cd "$tmpdir" && find . | cpio -H newc -o 2>/dev/null | xz -c >"$newinitrd") || { - rm -rf "$tmpdir" "$newinitrd" - DEBUG "Failed to repack initrd as xz" - return 1 - } - ;; - *) - (cd "$tmpdir" && find . | cpio -H newc -o 2>/dev/null | gzip -c >"$newinitrd") || { - rm -rf "$tmpdir" "$newinitrd" - DEBUG "Failed to repack initrd" - return 1 - } - ;; - esac - - # Replace the initrd in the ISO mount - cp "$newinitrd" "$initrd" || { - rm -rf "$tmpdir" "$newinitrd" - DEBUG "Failed to replace initrd" - return 1 - } - - rm -rf "$tmpdir" "$newinitrd" - DEBUG "Successfully injected casper-premount/iso_mount into initrd" - return 0 -} - -inject_casper_iso_mount +DEV_UUID=$(blkid $DEV | tail -1 | tr " " "\n" | grep UUID | cut -d\" -f2) scan_initramfs() { local path="$1" @@ -282,18 +103,12 @@ detect_initrd_boot_support() { for cfg in $(find /boot -name '*.cfg' -type f 2>/dev/null); do [ -r "$cfg" ] || continue - # kexec-parse-boot.sh outputs: name|$kexectype|kernel $path|initrd $path|append $append - # It only produces output for valid boot entries (menuentry/LABEL lines). - # Non-boot configs (theme.cfg, memtest.cfg, etc.) produce no output. while IFS= read -r entry; do [ -z "$entry" ] && continue - # Extract initrd paths from the |initrd $path| field - # Multiple initrds are comma-separated; handle both 'initrd ' and 'initrd' prefix initrd_field=$(echo "$entry" | tr '|' '\n' | grep '^initrd' | tail -1) || continue [ -z "$initrd_field" ] && continue initrd_val=$(echo "$initrd_field" | sed 's/^initrd //') || continue [ -z "$initrd_val" ] && continue - # Split comma-separated initrds (same as echo_entry does) for init in $(echo "$initrd_val" | tr ',' ' '); do [ -z "$init" ] && continue case " $initrd_paths " in @@ -306,11 +121,7 @@ detect_initrd_boot_support() { [ -z "$initrd_paths" ] && return 0 - total=$(echo $initrd_paths | wc -w) - count=0 for ipath in $initrd_paths; do - count=$((count + 1)) - DEBUG "Scanning initrd $count of $total: $ipath" full_path="/boot/${ipath#/}" [ -r "$full_path" ] && scan_initramfs "$full_path" done @@ -360,14 +171,11 @@ SUPPORTED_BOOT="" CFG_BOOT="" DETECTED_METHODS="" -STATUS "Detecting boot method support in initramfs..." -STATUS "Scanning initrds for filesystem and boot method support..." tmp_support=$(detect_initrd_boot_support 2>/dev/null) || tmp_support="" SUPPORTED_FSES=$(echo "$tmp_support" | grep "^fs:" | sed 's/^fs://') || SUPPORTED_FSES="" SUPPORTED_BOOT=$(echo "$tmp_support" | grep "^boot:" | sed 's/^boot://') || SUPPORTED_BOOT="" DEBUG "SUPPORTED_FSES='$SUPPORTED_FSES'" DEBUG "SUPPORTED_BOOT from initrd='$SUPPORTED_BOOT'" -STATUS_OK "Detected boot method support in initramfs" if [ -z "$SUPPORTED_BOOT" ]; then DEBUG "No boot method in initrd, scanning *.cfg files..." @@ -379,28 +187,20 @@ fi if [ -n "$SUPPORTED_FSES" ]; then DEBUG "Initrd supports USB filesystems: $SUPPORTED_FSES" - DEBUG "Extracting USB device filesystem type from blkid..." DEV_FSTYPE=$(blkid "$DEV" 2>/dev/null | tail -1 | grep -oE 'TYPE="[^"]+"' | sed 's/TYPE="//;s/"$//') || DEV_FSTYPE="" DEBUG "USB device filesystem type: '$DEV_FSTYPE'" if [ -n "$DEV_FSTYPE" ] && ! echo "$SUPPORTED_FSES" | grep -q "$DEV_FSTYPE" 2>/dev/null; then WARN "USB filesystem ($DEV_FSTYPE) may not be supported by this ISO's initrd" DEBUG "Supported filesystems: $SUPPORTED_FSES" fi || true - DEBUG "FS compatibility check: passed" -else - 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" elif [ -n "$CFG_BOOT" ]; then DETECTED_METHODS="$CFG_BOOT" DEBUG "Boot config (*.cfg) indicates boot methods: $DETECTED_METHODS" -else - DEBUG "No boot method detected in initrd or *.cfg files" fi DEBUG "DETECTED_METHODS='$DETECTED_METHODS'" @@ -418,10 +218,7 @@ if [ -z "$DETECTED_METHODS" ]; then fi fi -DEBUG "Proceeding to kexec-select-boot..." -DEV_UUID=$(blkid "$DEV" 2>/dev/null | tail -1 | tr " " "\n" | grep UUID | cut -d\" -f2) || DEV_UUID="" -DEBUG "DEV_UUID='$DEV_UUID'" -ADD="live-media=$MOUNTED_ISO_PATH live-media-path=casper fromiso=$MOUNTED_ISO_PATH img_dev=$DEV iso-scan/filename=/${ISO_PATH} img_loop=$ISO_PATH iso-scan/auto=true" +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="" paramsdir="/media/kexec_iso/$ISO_PATH" @@ -443,6 +240,6 @@ DEBUG "Overriding ISO kernel arguments with suppressions: $REMOVE" # Call kexec and indicate that hashes have been verified DO_WITH_DEBUG kexec-select-boot.sh -b /boot -d /media -p "$paramsdir" \ - -a "$ADD" -r "$REMOVE" -c "*.cfg" -u -i -s + -a "$ADD" -r "$REMOVE" -c "*.cfg" -u -i DIE "Something failed in selecting boot" From d68ec25a27fa0d8aa6046458589982f97164b05c Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Wed, 15 Apr 2026 15:36:32 -0400 Subject: [PATCH 06/10] tests: enhance iso-parser to detect initramfs boot params - Use unpack_initramfs.sh to extract initrds - Detect what boot params each ISO's initramfs understands - Show init script name and relevant lines for debugging Signed-off-by: Thierry Laurion --- tests/iso-parser/run.sh | 68 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/tests/iso-parser/run.sh b/tests/iso-parser/run.sh index cf9151d32..0cb718b48 100755 --- a/tests/iso-parser/run.sh +++ b/tests/iso-parser/run.sh @@ -7,6 +7,22 @@ set -e : "${ISO_DIR:=/home/user/Downloads/ISOs}" : "${PARSER:=$(dirname "$0")/../../initrd/bin/kexec-parse-boot.sh}" : "${FUNCTIONS:=$(dirname "$0")/../../initrd/etc/functions.sh}" +: "${UNPACK:=$(dirname "$0")/../../initrd/bin/unpack_initramfs.sh}" + +FUNC_STUB=$(mktemp) +cat >"$FUNC_STUB" <<'STUB' +TRACE_FUNC() { :; } +TRACE() { :; } +DEBUG() { :; } +ERROR() { echo "ERROR: $*" >&2; } +DIE() { echo "DIE: $*" >&2; exit 1; } +WARN() { echo "WARN: $*" >&2; } +check_config() { :; } +STUB + +UNPACK_TEMP=$(mktemp) +sed "s|^\\. /etc/functions\\.sh|. $FUNC_STUB|" "$UNPACK" >"$UNPACK_TEMP" +chmod +x "$UNPACK_TEMP" for cmd in fuseiso fusermount; do if ! command -v "$cmd" >/dev/null 2>&1; then @@ -90,4 +106,54 @@ for iso in "$ISO_DIR"/*.iso; do rm -rf "$sim" "$entries" done -rm -f "$STUB" "$TEMP_PARSER" +echo "" +echo "=== Initramfs Boot Param Detection ===" +echo "Detecting what boot params each ISO's initramfs understands" +echo "" + +for iso in "$ISO_DIR"/*.iso; do + [ -f "$iso" ] || continue + basenameiso=$(basename "$iso") + mnt=$(mktemp -d) + if ! fuseiso "$iso" "$mnt" 2>/dev/null; then + rmdir "$mnt" 2>/dev/null + continue + fi + + params="" + for initrd in $(find "$mnt" -name "initrd*" -type f 2>/dev/null | head -1); do + extr=$(mktemp -d) + bash "$UNPACK_TEMP" "$initrd" "$extr" >/dev/null 2>&1 || true + + if [ -d "$extr/scripts" ]; then + scripts_content=$(find "$extr/scripts" -type f \( -name "*.sh" -o -name "*.conf" \) -print 2>/dev/null | xargs cat 2>/dev/null) + echo "$scripts_content" | grep -qE "fromiso|iso-scan|findiso" && params="${params}fromiso " || true + echo "$scripts_content" | grep -qE "live.media|live-media" && params="${params}live-media " || true + echo "$scripts_content" | grep -qE "boot=live|rd.live.image|rd.live.squash" && params="${params}boot=live " || true + echo "$scripts_content" | grep -qE "boot.casper|casper" && params="${params}boot=casper " || true + echo "$scripts_content" | grep -qE "nixos" && params="${params}nixos " || true + echo "$scripts_content" | grep -qE "inst.stage2|inst.repo" && params="${params}anaconda " || true + + if [ -f "$extr/scripts/casper" ]; then + echo " init: casper" + grep -E "fromiso|iso-scan|findiso|live.media|img_dev|check_dev" "$extr/scripts/casper" 2>/dev/null | head -5 | sed 's/^/ /' + elif [ -f "$extr/init" ]; then + echo " init: custom init" + grep -E "fromiso|iso-scan|findiso|live.media|img_dev" "$extr/init" 2>/dev/null | head -5 | sed 's/^/ /' + else + script_files=$(find "$extr/scripts" -name "*.sh" 2>/dev/null | head -5 | xargs -I{} basename {} | tr '\n' ' ') + echo " scripts: $script_files" + fi + else + params="(no scripts)" + fi + rm -rf "$extr" + break + done + + echo "$basenameiso: ${params:-unknown}" + fusermount -u "$mnt" 2>/dev/null || umount "$mnt" 2>/dev/null + rmdir "$mnt" 2>/dev/null +done + +rm -f "$STUB" "$TEMP_PARSER" "$FUNC_STUB" "$UNPACK_TEMP" From e9d1a49c4061079c8d2a51814d753d6123f63f77 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Wed, 15 Apr 2026 15:37:22 -0400 Subject: [PATCH 07/10] kexec-iso-init: add live-media params for casper-based ISOs Add live-media=/dev/disk/by-uuid/.../path/to/iso.iso and live-media-path=casper to ADD params. PureOS casper initramfs needs live-media= pointing to the ISO file path, not /boot. This complements existing fromiso=, iso-scan/filename=, img_loop=, and iso= params. Signed-off-by: Thierry Laurion --- initrd/bin/kexec-iso-init.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/initrd/bin/kexec-iso-init.sh b/initrd/bin/kexec-iso-init.sh index e5cdbaf09..8e5899dff 100755 --- a/initrd/bin/kexec-iso-init.sh +++ b/initrd/bin/kexec-iso-init.sh @@ -218,7 +218,7 @@ if [ -z "$DETECTED_METHODS" ]; then fi fi -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" +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 live-media=/dev/disk/by-uuid/$DEV_UUID/$ISO_PATH live-media-path=casper" REMOVE="" paramsdir="/media/kexec_iso/$ISO_PATH" From d35f02f86553fd99b5b1afe77e5f9813ade82881 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Tue, 21 Apr 2026 16:36:22 -0400 Subject: [PATCH 08/10] WIP: ISO boot detection fixes and test improvements - Fix duplicate iso-scan/filename parameter injection - Add deduplication to cfg boot method extraction - Add selective parameter injection (boot=live, boot=casper, live-media) - Add header comments to kexec-iso-init.sh, kexec-parse-boot.sh, kexec-boot.sh - Add param injection validation to test suite - Fix Tails boot parameter regression (was injecting boot=casper incorrectly) - Show boot params in boot menu options --- initrd/bin/kexec-boot.sh | 22 +- initrd/bin/kexec-iso-init.sh | 207 +++++++++++++++--- initrd/bin/kexec-parse-boot.sh | 10 + initrd/bin/kexec-select-boot.sh | 13 +- tests/iso-parser/run.sh | 370 +++++++++++++++++++++++++++----- 5 files changed, 532 insertions(+), 90 deletions(-) diff --git a/initrd/bin/kexec-boot.sh b/initrd/bin/kexec-boot.sh index 9a913081c..b13e0d151 100755 --- a/initrd/bin/kexec-boot.sh +++ b/initrd/bin/kexec-boot.sh @@ -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 @@ -65,6 +82,8 @@ 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") @@ -164,6 +183,7 @@ 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" DEBUG "kexec-boot: executing kexec with adjusted_cmd_line=$adjusted_cmd_line kexectype=$kexectype" diff --git a/initrd/bin/kexec-iso-init.sh b/initrd/bin/kexec-iso-init.sh index 8e5899dff..129de6eff 100755 --- a/initrd/bin/kexec-iso-init.sh +++ b/initrd/bin/kexec-iso-init.sh @@ -1,5 +1,33 @@ #!/bin/bash -# Boot from signed ISO +# Boot from signed ISO file on USB media +# +# This script handles booting from ISO files stored on USB storage. +# It works by mounting the ISO, detecting boot mechanisms supported by +# the ISO's initrd, injecting appropriate kernel parameters, and +# executing kexec to boot the OS. +# +# Detection approach: +# 1. Mount the ISO as a loopback device +# 2. Extract and scan the initrd for supported boot mechanisms +# 3. Fall back to scanning *.cfg files if initrd detection yields nothing +# 4. If no known boot-from-ISO mechanism is found, warn and guide user +# +# Supported boot mechanisms (detected in initrd or config): +# - iso-scan/findiso: Dracut-based (Ubuntu, Debian Live, Tails, etc.) +# - live-media: Dracut live-media parameter +# - boot=live: Debian Live / Fedora Live +# - boot=casper: Ubuntu Casper +# - nixos: NixOS +# - anaconda: Fedora/RHEL Anaconda (block device required) +# - overlay: OverlayFS support +# - toram: Load-to-RAM support +# +# If no mechanism is detected, the user is warned that the ISO may not +# support booting from ISO file on USB, and is given alternative options: +# - Write ISO directly to USB with dd +# - Use Ventoy in USB emulation mode +# - Boot from real DVD drive +# set -e -o pipefail . /etc/functions.sh . /etc/gui_functions.sh @@ -52,6 +80,18 @@ mount -t iso9660 -o loop $MOUNTED_ISO_PATH /boot || DEV_UUID=$(blkid $DEV | tail -1 | tr " " "\n" | grep UUID | cut -d\" -f2) +# Scan an initrd for supported filesystems and boot mechanisms. +# This function unpacks the initrd and searches for: +# - Kernel modules (*.ko/*.ko.xz) -> supported filesystems +# - Scripts and configs (*.sh, *.conf, init, scripts/*) -> boot mechanisms +# +# Supported filesystems detected: ext4, vfat, exfat, ntfs, btrfs, xfs +# Supported boot mechanisms detected: iso-scan, live-media, boot-live, +# casper, nixos, anaconda, overlay, toram, device +# +# Results are stored in global variables: +# - supported_fses: Space-separated list of supported filesystem types +# - supported_boot: Space-separated list of supported boot mechanisms scan_initramfs() { local path="$1" local tmpdir="" @@ -82,20 +122,40 @@ scan_initramfs() { boot_content=$(strings "$path" 2>/dev/null) || true fi - echo "$boot_content" | grep -qEi "iso.scan|findiso" && - supported_boot="${supported_boot}iso-scan/findiso " || true - echo "$boot_content" | grep -qEi "live.media|live-media" && - supported_boot="${supported_boot}live-media= " || true - echo "$boot_content" | grep -qEi "boot=live|rd.live.image|rd.live.squash" && - supported_boot="${supported_boot}boot=live " || true - echo "$boot_content" | grep -qEi "boot.casper|casper" && - supported_boot="${supported_boot}boot=casper " || true - echo "$boot_content" | grep -qEi "nixos" && - supported_boot="${supported_boot}nixos " || true - echo "$boot_content" | grep -qEi "inst.stage2|inst.repo" && - supported_boot="${supported_boot}anaconda " || true + for pattern in "iso.scan|findiso" "live.media|live-media" "boot=live|rd.live.image|rd.live.squash" "boot.casper|casper" "nixos" "inst.stage2|inst.repo" "overlay|overlayfs" "toram" "CDLABEL|img_dev|check_dev"; do + case "$pattern" in + iso.scan|findiso) label="iso-scan" ;; + live.media|live-media) label="live-media" ;; + boot=live|rd.live.image|rd.live.squash) label="boot-live" ;; + boot.casper|casper) label="casper" ;; + nixos) label="nixos" ;; + inst.stage2|inst.repo) label="anaconda" ;; + overlay|overlayfs) label="overlay" ;; + toram) label="toram" ;; + CDLABEL|img_dev|check_dev) label="device" ;; + esac + echo "$boot_content" | grep -qEi "$pattern" && + supported_boot="${supported_boot}${label} " || true + done } +# Detect if the mounted ISO is an installer ISO (not a live/bootable ISO). +# Installer ISOs (like Debian DVD installer) do not support booting from +# ISO file on USB - they only work with physical CD/DVD or PXE boot. +# +# Detection checks for: +# - /boot/install* directory (installer content) +# - /boot/isolinux or /boot/grub (boot configs, but no live boot) +# - /boot/install.amd/vmlinuz and initrd.gz (installer kernel/initrd) +# +# Detect boot mechanisms supported by the ISO's initrd. +# This function: +# 1. Parses all *.cfg files to find initrd paths +# 2. For each initrd, calls scan_initramfs() to extract supported features +# 3. Outputs two lines: "fs:..." and "boot:..." with detected support +# +# This is the primary detection method - scanning initrd content directly +# provides the most accurate picture of what the ISO can do. detect_initrd_boot_support() { local supported_fses="" local supported_boot="" @@ -131,6 +191,13 @@ detect_initrd_boot_support() { return 0 } +# Fallback detection: scan *.cfg files for boot parameters. +# This is used when initrd detection fails or yields no results. +# It greps through boot config files (GRUB, syslinux, ISOLINUX) for +# known boot parameters that indicate ISO-on-USB support. +# +# This method is less accurate than initrd scanning but can provide +# hints when initrd extraction fails. extract_boot_params_from_cfg() { for cfg in $(find /boot -name '*.cfg' -type f 2>/dev/null); do [ -r "$cfg" ] || continue @@ -141,22 +208,49 @@ extract_boot_params_from_cfg() { while IFS= read -r line; do case "$line" in *boot=live* | *rd.live.image* | *rd.live.squashimg=*) - boot_params="${boot_params}boot=live " + if ! echo "$boot_params" | grep -q "boot-live"; then + boot_params="${boot_params}boot-live " + fi ;; *iso-scan/filename=* | *findiso=*) - boot_params="${boot_params}iso-scan/findiso " + if ! echo "$boot_params" | grep -q "iso-scan"; then + boot_params="${boot_params}iso-scan " + fi ;; *live-media=* | *live.media=*) - boot_params="${boot_params}live-media= " + if ! echo "$boot_params" | grep -q "live-media"; then + boot_params="${boot_params}live-media " + fi ;; *boot=casper* | *casper*) - boot_params="${boot_params}boot=casper " + if ! echo "$boot_params" | grep -q "casper"; then + boot_params="${boot_params}casper " + fi ;; *inst.stage2=* | *inst.repo=*) - boot_params="${boot_params}anaconda " + if ! echo "$boot_params" | grep -q "anaconda"; then + boot_params="${boot_params}anaconda " + fi ;; *nixos*) - boot_params="${boot_params}nixos " + if ! echo "$boot_params" | grep -q "nixos"; then + boot_params="${boot_params}nixos " + fi + ;; + *overlay=* | *overlayfs*) + if ! echo "$boot_params" | grep -q "overlay"; then + boot_params="${boot_params}overlay " + fi + ;; + *toram*) + if ! echo "$boot_params" | grep -q "toram"; then + boot_params="${boot_params}toram " + fi + ;; + *CDLABEL=* | *img_dev=* | *check_dev*) + if ! echo "$boot_params" | grep -q "device"; then + boot_params="${boot_params}device " + fi ;; esac done <"$cfg" @@ -165,6 +259,15 @@ extract_boot_params_from_cfg() { return 1 } +# ============================================================================ +# Main detection flow +# ============================================================================ +# Step 1: Scan initrd for supported boot mechanisms +# Step 2: If no boot method found, fall back to cfg file scanning +# Step 3: Check USB filesystem compatibility +# Step 4: If no known mechanism found, warn user with guidance +# ============================================================================ + STATUS "Detecting USB filesystem and boot method support..." SUPPORTED_FSES="" SUPPORTED_BOOT="" @@ -177,12 +280,16 @@ SUPPORTED_BOOT=$(echo "$tmp_support" | grep "^boot:" | sed 's/^boot://') || SUPP DEBUG "SUPPORTED_FSES='$SUPPORTED_FSES'" DEBUG "SUPPORTED_BOOT from initrd='$SUPPORTED_BOOT'" -if [ -z "$SUPPORTED_BOOT" ]; then - DEBUG "No boot method in initrd, scanning *.cfg files..." - CFG_BOOT=$(extract_boot_params_from_cfg 2>/dev/null | grep "^cfg:" | sed 's/^cfg://') || CFG_BOOT="" - DEBUG "CFG_BOOT='$CFG_BOOT'" -else - DEBUG "Boot method found in initrd, skipping cfg extraction" +DEBUG "Scanning *.cfg files to augment initrd results..." +CFG_BOOT=$(extract_boot_params_from_cfg 2>/dev/null | grep "^cfg:" | sed 's/^cfg://') || CFG_BOOT="" +DEBUG "CFG_BOOT='$CFG_BOOT'" + +if [ -n "$SUPPORTED_BOOT" ] && [ -n "$CFG_BOOT" ]; then + SUPPORTED_BOOT="$SUPPORTED_BOOT $CFG_BOOT" + DEBUG "Combined boot methods: $SUPPORTED_BOOT" +elif [ -z "$SUPPORTED_BOOT" ] && [ -n "$CFG_BOOT" ]; then + SUPPORTED_BOOT="$CFG_BOOT" + DEBUG "Using cfg boot methods: $SUPPORTED_BOOT" fi if [ -n "$SUPPORTED_FSES" ]; then @@ -197,28 +304,58 @@ fi if [ -n "$SUPPORTED_BOOT" ]; then DETECTED_METHODS="$SUPPORTED_BOOT" - DEBUG "Initrd supports boot methods: $DETECTED_METHODS" -elif [ -n "$CFG_BOOT" ]; then - DETECTED_METHODS="$CFG_BOOT" - DEBUG "Boot config (*.cfg) indicates boot methods: $DETECTED_METHODS" + DEBUG "Detected boot methods: $DETECTED_METHODS" fi DEBUG "DETECTED_METHODS='$DETECTED_METHODS'" 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?" \ + if ! whiptail_warning --title 'ISO BOOT NOT SUPPORTED' --yesno \ + "This ISO does not support booting from ISO file on USB.\n\nThe initrd does not include boot-from-ISO mechanisms (no live-boot, casper, fromiso, iso-scan, anaconda, or nixos support detected).\n\nTo use this ISO, write the hybrid image directly to a USB flash drive:\n\nLinux: sudo cp image.iso /dev/sdX (Be cautious!)\nWindows/Mac: Use Rufus, select DD mode (NOT ISO mode)\n\nWrite to whole-disk device (NOT a partition, e.g. /dev/sdX not /dev/sdX1),\nthen boot from USB device directly (not as ISO file).\n\nSee Debian wiki: https://wiki.debian.org/DebianInstall" \ 0 80; then - DIE "ISO boot cancelled - unsupported ISO on USB file" + DIE "ISO boot cancelled - initrd does not support USB file boot" 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" + ERROR "ISO initrd has no boot-from-ISO support (no live-boot/casper/iso-scan)" + ERROR "Write hybrid image to USB: Linux: cp iso /dev/sdX | Win/Mac: Rufus DD mode" + INPUT "Try anyway? [y/N]:" -n 1 response + [ "$response" != "y" ] && [ "$response" != "Y" ] && DIE "ISO boot cancelled" fi fi -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 live-media=/dev/disk/by-uuid/$DEV_UUID/$ISO_PATH live-media-path=casper" +# ============================================================================ +# Boot parameter injection +# ============================================================================ +# Inject all known boot-from-ISO parameters. The ISO's initrd will use +# whichever parameters it understands and ignore the rest. +# +# Parameters injected (covering all major boot systems): +# - findiso, fromiso, iso-scan/filename: Dracut standard +# - img_dev, img_loop: additional Dracut variants +# - iso: alternative parameter +# - live-media, live-media-path: live-boot parameters +# - boot=live, boot=casper: casper/live-boot parameters +# ============================================================================ + +ISO_DEV="/dev/disk/by-uuid/$DEV_UUID" +ISO_PATH_ABS="/$ISO_PATH" + +base_params="findiso=$ISO_DEV/$ISO_PATH fromiso=$ISO_DEV/$ISO_PATH iso-scan/filename=$ISO_PATH_ABS img_dev=$ISO_DEV img_loop=$ISO_PATH iso=$DEV_UUID/$ISO_PATH" + +add_params="" +if echo "$DETECTED_METHODS" | grep -q "casper"; then + add_params="$add_params boot=casper live-media-path=casper" +fi +if echo "$DETECTED_METHODS" | grep -q "boot-live"; then + add_params="$add_params boot=live" +fi +if echo "$DETECTED_METHODS" | grep -q "live-media"; then + add_params="$add_params live-media=$ISO_DEV/$ISO_PATH" +fi + +ADD="$base_params $add_params" +DEBUG "Injecting boot params: $ADD" REMOVE="" paramsdir="/media/kexec_iso/$ISO_PATH" diff --git a/initrd/bin/kexec-parse-boot.sh b/initrd/bin/kexec-parse-boot.sh index 627756ea0..b579cdfb9 100755 --- a/initrd/bin/kexec-parse-boot.sh +++ b/initrd/bin/kexec-parse-boot.sh @@ -1,4 +1,14 @@ #!/bin/bash +# Parse boot loader configs (GRUB, syslinux, ISOLINUX) to extract boot entries +# +# This script parses boot configuration files to build a list of boot entries +# that can be used by kexec-boot.sh to boot an OS. It handles: +# - GRUB config files (grub.cfg) +# - SYSLINUX/ISOLINUX config files (isolinux.cfg, syslinux.cfg) +# - Multiboot kernels (Xen) +# +# Output format: name|kexectype|kernel path[|initrd path][|append params] +# set -e -o pipefail . /etc/functions.sh diff --git a/initrd/bin/kexec-select-boot.sh b/initrd/bin/kexec-select-boot.sh index 1c933d792..0fd45b5c8 100755 --- a/initrd/bin/kexec-select-boot.sh +++ b/initrd/bin/kexec-select-boot.sh @@ -144,7 +144,11 @@ get_menu_option() { while read option; do parse_option n=$(expr $n + 1) - MENU_OPTIONS+=("$n" "$name") + if [ -n "$params" ]; then + MENU_OPTIONS+=("$n" "$name ($params)") + else + MENU_OPTIONS+=("$n" "$name") + fi done <$TMP_MENU_FILE whiptail_type $BG_COLOR_MAIN_MENU --title "Select your boot option" \ @@ -165,7 +169,7 @@ get_menu_option() { # because DO_WITH_DEBUG pipes stdout through tee for debug logging, # making it fully buffered — the last option would appear after the # INPUT prompt. - printf '%d. %s [%s]\n' "$n" "$name" "$kernel" >"${HEADS_TTY:-/dev/stderr}" + printf '%d. %s %s [%s]\n' "$n" "$name" "${params:+($params)}" "$kernel" >"${HEADS_TTY:-/dev/stderr}" done <$TMP_MENU_FILE INPUT "Choose the boot option [1-$n, a to abort]:" -r option_index @@ -202,6 +206,7 @@ confirm_menu_option() { parse_option() { name=$(echo $option | cut -d\| -f1) kernel=$(echo $option | cut -d\| -f3) + params=$(echo $option | cut -d\| -f5 | sed 's/append //' | xargs) } scan_options() { @@ -212,10 +217,12 @@ scan_options() { DIE "Failed to parse any boot options" fi if [ "$unique" = 'y' ]; then - sort -r $option_file | uniq >$TMP_MENU_FILE + sed 's/|append \([^|]*\)---[^|]*/|append \1/g' "$option_file" | sort -r | uniq >"$TMP_MENU_FILE" else cp $option_file $TMP_MENU_FILE fi + DEBUG "Parsed boot options for user selection:" + cat "$TMP_MENU_FILE" | while read line; do DEBUG " Option: $line"; done } save_default_option() { diff --git a/tests/iso-parser/run.sh b/tests/iso-parser/run.sh index 0cb718b48..43d7004ac 100755 --- a/tests/iso-parser/run.sh +++ b/tests/iso-parser/run.sh @@ -1,29 +1,46 @@ #!/bin/bash -# ISO parser test — mirrors how scan_boot_options() calls the parser in Heads -# Usage: ISO_DIR=/path/to/isos ./run.sh +# ISO parser test — mirrors how kexec-iso-init.sh detects boot support in Heads +# +# This script tests ISO boot compatibility by: +# 1. Mounting each ISO +# 2. Extracting and scanning initrd for boot mechanism support +# 3. Checking for installer ISOs (which don't support USB file boot) +# 4. Reporting supported boot methods and overall compatibility +# +# Usage: +# ./run.sh # test all ISOs in default dir +# ./run.sh /path/to/iso.iso # test single ISO +# +# Output: +# - First section: ISO metadata (entries, hybrid, sample boot params) +# - Second section: Initramfs boot support detection +# +# Compatibility status: +# - OK: Known boot mechanism detected, should work +# - WARN: No known mechanism detected, may work but unverified +# - SKIP: Installer ISO - use dd/Ventoy instead +# +# Tested ISOs (2026-04): +# - Ubuntu Desktop, Debian Live, Tails, Fedora Live, NixOS, PureOS, Kicksecure: OK +# - Debian DVD installer: SKIP (use dd/Ventoy) +# - TinyCore/CorePlus: WARN (unverified) set -e +if [ -n "$1" ] && [ -f "$1" ]; then + ISO_DIR=$(dirname "$1") + SINGLE_ISO="$1" +elif [ -n "$1" ]; then + echo "Error: '$1' is not a valid ISO file" + exit 1 +fi + : "${ISO_DIR:=/home/user/Downloads/ISOs}" +: "${ISO_INIT:=$(dirname "$0")/../../initrd/bin/kexec-iso-init.sh}" : "${PARSER:=$(dirname "$0")/../../initrd/bin/kexec-parse-boot.sh}" : "${FUNCTIONS:=$(dirname "$0")/../../initrd/etc/functions.sh}" : "${UNPACK:=$(dirname "$0")/../../initrd/bin/unpack_initramfs.sh}" -FUNC_STUB=$(mktemp) -cat >"$FUNC_STUB" <<'STUB' -TRACE_FUNC() { :; } -TRACE() { :; } -DEBUG() { :; } -ERROR() { echo "ERROR: $*" >&2; } -DIE() { echo "DIE: $*" >&2; exit 1; } -WARN() { echo "WARN: $*" >&2; } -check_config() { :; } -STUB - -UNPACK_TEMP=$(mktemp) -sed "s|^\\. /etc/functions\\.sh|. $FUNC_STUB|" "$UNPACK" >"$UNPACK_TEMP" -chmod +x "$UNPACK_TEMP" - for cmd in fuseiso fusermount; do if ! command -v "$cmd" >/dev/null 2>&1; then echo "Missing: $cmd" @@ -52,6 +69,49 @@ if [ ! -f "$FUNCTIONS" ]; then exit 1 fi +if [ ! -f "$ISO_INIT" ]; then + echo "ISO_INIT not found: $ISO_INIT" + echo "Set ISO_INIT=/path/to/kexec-iso-init.sh" + exit 1 +fi + +if [ ! -f "$UNPACK" ]; then + echo "UNPACK not found: $UNPACK" + echo "Set UNPACK=/path/to/unpack_initramfs.sh" + exit 1 +fi + +STUB=$(mktemp) +cat >"$STUB" <<'STUB' +TRACE_FUNC() { :; } +TRACE() { :; } +DEBUG() { :; } +ERROR() { echo "ERROR: $*" >&2; } +DIE() { echo "DIE: $*" >&2; exit 1; } +WARN() { echo "WARN: $*" >&2; } +check_config() { :; } +STUB + +FUNC_STUB=$(mktemp) +cat >"$FUNC_STUB" <<'STUB' +TRACE_FUNC() { :; } +TRACE() { :; } +DEBUG() { :; } +ERROR() { echo "ERROR: $*" >&2; } +DIE() { echo "DIE: $*" >&2; exit 1; } +WARN() { echo "WARN: $*" >&2; } +check_config() { :; } +zstd-decompress() { zstd -d "$@"; } +STUB + +UNPACK_TEMP=$(mktemp) +sed "s|^\\. /etc/functions\\.sh|. $FUNC_STUB|" "$UNPACK" >"$UNPACK_TEMP" +chmod +x "$UNPACK_TEMP" + +ISO_INIT_TEMP=$(mktemp) +sed "s|^\\. /etc/functions\\.sh|. $STUB|" "$ISO_INIT" >"$ISO_INIT_TEMP" +chmod +x "$ISO_INIT_TEMP" + STUB=$(mktemp) cat >"$STUB" <<'STUB' TRACE_FUNC() { :; } @@ -72,6 +132,7 @@ printf "%-60s %8s %10s %s\n" "---" "-------" "------" "------------------" for iso in "$ISO_DIR"/*.iso; do [ -f "$iso" ] || continue + [ -n "$SINGLE_ISO" ] && [ "$(realpath "$iso")" != "$(realpath "$SINGLE_ISO")" ] && continue mnt=$(mktemp -d) if ! fuseiso "$iso" "$mnt" 2>/dev/null; then rmdir "$mnt" 2>/dev/null @@ -79,7 +140,7 @@ for iso in "$ISO_DIR"/*.iso; do continue fi if [ ! -d "$mnt/boot" ] && [ ! -d "$mnt/isolinux" ]; then - fusermount -u "$mnt" 2>/dev/null + fusermount -uz "$mnt" 2>/dev/null || umount "$mnt" 2>/dev/null || true rmdir "$mnt" 2>/dev/null printf "%-60s %8s %10s %s\n" "$(basename "$iso")" "SKIP" "?" "mount empty" continue @@ -94,66 +155,273 @@ for iso in "$ISO_DIR"/*.iso; do "$TEMP_PARSER" "$sim" "$cfg" >>"$entries" 2>/dev/null || true done - count=$(wc -l <"$entries" 2>/dev/null || echo 0) + count=$(sort -u "$entries" 2>/dev/null | wc -l || echo 0) boot=$(sed -n 's/.*|append \(.*\)/\1/p' "$entries" 2>/dev/null | head -1) mbr=$(dd if="$iso" bs=1 skip=510 count=2 2>/dev/null | od -An -tx1 | tr -d ' \n') hybrid=$([ "$mbr" = "55aa" ] && echo "yes" || echo "no") printf "%-60s %8s %10s %s\n" "$(basename "$iso")" "$count" "$hybrid" "${boot:0:60}" - fusermount -u "$mnt" 2>/dev/null || umount "$mnt" 2>/dev/null + fusermount -uz "$mnt" 2>/dev/null || umount "$mnt" 2>/dev/null || true rmdir "$mnt" 2>/dev/null rm -rf "$sim" "$entries" done echo "" -echo "=== Initramfs Boot Param Detection ===" -echo "Detecting what boot params each ISO's initramfs understands" +echo "=== Initramfs ISO Boot Support ===" +echo "Detecting supported boot mechanisms and quirks" echo "" +if [ -n "$SINGLE_ISO" ]; then + printf "\n%-60s %-40s %s\n" "ISO" "DETECTED MECHANISM" "SUPPORTED" + printf "\n%-60s %-40s %s\n" "---" "--------------------" "---------" +fi + +check_compatibility() { + local supported="$1" + local status="" + local note="" + case "$supported" in + installer*) status="SKIP" ; note=" (use dd/Ventoy)" ;; + anaconda*) status="WARN" ; note=" (block device req)" ;; + std) status="WARN" ;; + "") status="WARN" ;; + *) status="OK" ;; + esac + echo "${status}${note}" +} + for iso in "$ISO_DIR"/*.iso; do [ -f "$iso" ] || continue + [ -n "$SINGLE_ISO" ] && [ "$(realpath "$iso")" != "$(realpath "$SINGLE_ISO")" ] && continue basenameiso=$(basename "$iso") mnt=$(mktemp -d) if ! fuseiso "$iso" "$mnt" 2>/dev/null; then rmdir "$mnt" 2>/dev/null continue fi + if [ ! -d "$mnt/boot" ] && [ ! -d "$mnt/isolinux" ]; then + fusermount -uz "$mnt" 2>/dev/null || umount "$mnt" 2>/dev/null || true + rmdir "$mnt" 2>/dev/null + continue + fi + + sim=$(mktemp -u) + rm -rf "$sim" + ln -sf "$mnt" "$sim" + + entries=$(mktemp) + >"$entries" + for cfg in $(find "$mnt" -name "*.cfg" -type f 2>/dev/null | grep -v -i -E "efi|x86_64-efi"); do + "$TEMP_PARSER" "$sim" "$cfg" >>"$entries" 2>/dev/null || true + done + boot_params=$(sed -n 's/.*|append \(.*\)/\1/p' "$entries" 2>/dev/null | head -1) + rm -f "$entries" + + mechanism="" + + if [ -d "$mnt/install.amd" ] && [ -f "$mnt/install.amd/vmlinuz" ] && [ -f "$mnt/install.amd/initrd.gz" ]; then + mechanism="installer" + fi + + if [ -z "$mechanism" ]; then + tmp_boot=$(mktemp -d) + ln -sf "$mnt/boot" "$tmp_boot/boot" 2>/dev/null || ln -sf "$mnt" "$tmp_boot/boot" + ln -sf "$mnt/isolinux" "$tmp_boot/isolinux" 2>/dev/null || true + ln -sf "$mnt/install.amd" "$tmp_boot/install.amd" 2>/dev/null || true + + scan_initramfs_test() { + local path="$1" + local tmpdir="" + local boot_content="" - params="" - for initrd in $(find "$mnt" -name "initrd*" -type f 2>/dev/null | head -1); do - extr=$(mktemp -d) - bash "$UNPACK_TEMP" "$initrd" "$extr" >/dev/null 2>&1 || true - - if [ -d "$extr/scripts" ]; then - scripts_content=$(find "$extr/scripts" -type f \( -name "*.sh" -o -name "*.conf" \) -print 2>/dev/null | xargs cat 2>/dev/null) - echo "$scripts_content" | grep -qE "fromiso|iso-scan|findiso" && params="${params}fromiso " || true - echo "$scripts_content" | grep -qE "live.media|live-media" && params="${params}live-media " || true - echo "$scripts_content" | grep -qE "boot=live|rd.live.image|rd.live.squash" && params="${params}boot=live " || true - echo "$scripts_content" | grep -qE "boot.casper|casper" && params="${params}boot=casper " || true - echo "$scripts_content" | grep -qE "nixos" && params="${params}nixos " || true - echo "$scripts_content" | grep -qE "inst.stage2|inst.repo" && params="${params}anaconda " || true - - if [ -f "$extr/scripts/casper" ]; then - echo " init: casper" - grep -E "fromiso|iso-scan|findiso|live.media|img_dev|check_dev" "$extr/scripts/casper" 2>/dev/null | head -5 | sed 's/^/ /' - elif [ -f "$extr/init" ]; then - echo " init: custom init" - grep -E "fromiso|iso-scan|findiso|live.media|img_dev" "$extr/init" 2>/dev/null | head -5 | sed 's/^/ /' + [ -r "$path" ] || return 1 + + tmpdir=$(mktemp -d) + bash "$UNPACK_TEMP" "$path" "$tmpdir" 2>/dev/null || true + + if [ -d "$tmpdir" ] && [ "$(ls -A "$tmpdir" 2>/dev/null)" ]; then + boot_content=$(find "$tmpdir" -type f \( -name "*.sh" -o -name "*.conf" -o -name "*.cfg" -o -name "init" -o -name "*.txt" -o -path "*/scripts/*" -o -path "*/conf/*" -o -path "*/lib/live/boot/*" -o -path "*/usr/lib/live/boot/*" \) -print 2>/dev/null | xargs cat 2>/dev/null | tr -d '\0' || true) || boot_content="" + rm -rf "$tmpdir" else - script_files=$(find "$extr/scripts" -name "*.sh" 2>/dev/null | head -5 | xargs -I{} basename {} | tr '\n' ' ') - echo " scripts: $script_files" + rm -rf "$tmpdir" + boot_content=$(strings "$path" 2>/dev/null | tr -d '\0') || true + fi + + if echo "$boot_content" | grep -qEi "iso.scan|findiso"; then + supported_boot="${supported_boot}iso-scan " + fi + if echo "$boot_content" | grep -qEi "live.media|live-media"; then + supported_boot="${supported_boot}live-media " + fi + if echo "$boot_content" | grep -qEi "boot=live|rd.live.image|rd.live.squash"; then + supported_boot="${supported_boot}boot-live " + fi + if echo "$boot_content" | grep -qEi "boot.casper|casper"; then + supported_boot="${supported_boot}casper " + fi + if echo "$boot_content" | grep -qEi "nixos"; then + supported_boot="${supported_boot}nixos " + fi + if echo "$boot_content" | grep -qEi "inst.stage2|inst.repo"; then + supported_boot="${supported_boot}anaconda " + fi + if echo "$boot_content" | grep -qEi "overlay|overlayfs"; then + supported_boot="${supported_boot}overlay " + fi + if echo "$boot_content" | grep -qEi "toram"; then + supported_boot="${supported_boot}toram " + fi + if echo "$boot_content" | grep -qEi "CDLABEL|img_dev|check_dev"; then + supported_boot="${supported_boot}device " + fi + } + + supported_fses="" + supported_boot="" + initrd="" + + initrds=$(find "$mnt" -name "initrd*" -type f 2>/dev/null) + for p in live/initrd.img live/initrd boot/initrd* casper/initrd* install/initrd.gz install.amd/initrd.gz; do + [ -f "$mnt/$p" ] && { initrd="$mnt/$p"; break; } + done + [ -z "$initrd" ] && initrd=$(echo "$initrds" | head -1) + + if [ -n "$initrd" ]; then + timeout 30 scan_initramfs_test "$initrd" 2>/dev/null || true + fi + + for cfg in $(find "$mnt" -name "*.cfg" -type f 2>/dev/null | grep -v -i -E "efi|x86_64-efi"); do + cfg_content=$(cat "$cfg" 2>/dev/null | tr -d '\0') || true + if echo "$cfg_content" | grep -qEi "boot=live|rd.live.image|rd.live.squash"; then + supported_boot="${supported_boot}boot-live " fi - else - params="(no scripts)" + if echo "$cfg_content" | grep -qEi "iso-scan|findiso"; then + supported_boot="${supported_boot}iso-scan " + fi + if echo "$cfg_content" | grep -qiE "live.media"; then + supported_boot="${supported_boot}live-media " + fi + if echo "$cfg_content" | grep -qiE "boot=casper"; then + supported_boot="${supported_boot}casper " + fi + if echo "$cfg_content" | grep -qiE "inst.stage2|inst.repo"; then + supported_boot="${supported_boot}anaconda " + fi + if echo "$cfg_content" | grep -qiE "nixos"; then + supported_boot="${supported_boot}nixos " + fi + if echo "$cfg_content" | grep -qiE "overlay"; then + supported_boot="${supported_boot}overlay " + fi + if echo "$cfg_content" | grep -qiE "toram"; then + supported_boot="${supported_boot}toram " + fi + if echo "$cfg_content" | grep -qiE "CDLABEL|img_dev"; then + supported_boot="${supported_boot}device " + fi + done + + rm -rf "$tmp_boot" + + mechanism=$(echo "${supported_boot:-std}" | tr ' ' '\n' | sort -u | tr '\n' ' ' | sed 's/^ *//;s/ $//') + fi + + compatibility=$(check_compatibility "$mechanism") + mechanism_short=$(echo "$mechanism" | cut -c1-38) + + printf "%-60s %-40s %s\n" "$basenameiso" "$mechanism_short" "$compatibility" + + simulate_param_injection() { + local detected="$1" + local params="" + + params="findiso=... fromiso=... iso-scan/filename=... img_dev=... img_loop=... iso=..." + + if echo "$detected" | grep -q "casper"; then + params="$params boot=casper live-media-path=casper" + fi + if echo "$detected" | grep -q "boot-live"; then + params="$params boot=live" + fi + if echo "$detected" | grep -q "live-media"; then + params="$params live-media=..." + fi + + echo "$params" + } + + injected_params=$(simulate_param_injection "$mechanism") + + has_casper=$(echo "$injected_params" | grep -qo "boot=casper" && echo "yes" || echo "no") + has_boot_live=$(echo "$injected_params" | grep -qo "boot=live" && echo "yes" || echo "no") + + if [ "$has_casper" = "yes" ] && [ "$has_boot_live" = "yes" ]; then + echo "WARNING: Conflicting boot params (casper + boot-live) for $basenameiso" >&2 + fi + + fusermount -uz "$mnt" 2>/dev/null || umount "$mnt" 2>/dev/null || true + rmdir "$mnt" 2>/dev/null + rm -rf "$sim" +done + +echo "" +echo "=== Parameter Injection Validation ===" +echo "" + +for iso in "$ISO_DIR"/*.iso; do + [ -f "$iso" ] || continue + basenameiso=$(basename "$iso") + + mnt=$(mktemp -d) + if ! fuseiso "$iso" "$mnt" 2>/dev/null; then + rmdir "$mnt" 2>/dev/null + continue + fi + + supported_boot="" + for cfg in $(find "$mnt" -name "*.cfg" -type f 2>/dev/null | grep -v -i -E "efi|x86_64-efi"); do + cfg_content=$(cat "$cfg" 2>/dev/null | tr -d '\0') || true + if echo "$cfg_content" | grep -qEi "boot=live|rd.live.image|rd.live.squash"; then + supported_boot="${supported_boot}boot-live " + fi + if echo "$cfg_content" | grep -qiE "boot=casper"; then + supported_boot="${supported_boot}casper " + fi + if echo "$cfg_content" | grep -qiE "live.media"; then + supported_boot="${supported_boot}live-media " fi - rm -rf "$extr" - break done - echo "$basenameiso: ${params:-unknown}" - fusermount -u "$mnt" 2>/dev/null || umount "$mnt" 2>/dev/null + mechanism=$(echo "${supported_boot:-std}" | tr ' ' '\n' | sort -u | tr '\n' ' ' | sed 's/^ *//;s/ $//') + + injected="" + if echo "$mechanism" | grep -q "casper"; then + injected="$injected boot=casper" + fi + if echo "$mechanism" | grep -q "boot-live"; then + injected="$injected boot=live" + fi + if echo "$mechanism" | grep -q "live-media"; then + injected="$injected live-media" + fi + + conflicts="" + has_casper=$(echo "$injected" | grep -qo "boot=casper" && echo "y" || echo "n") + has_live=$(echo "$injected" | grep -qo "boot=live" && echo "y" || echo "n") + + if [ "$has_casper" = "y" ] && [ "$has_live" = "y" ]; then + conflicts="CONFLICT" + elif [ -z "$injected" ]; then + conflicts="NO_PARAMS" + else + conflicts="OK" + fi + + printf "%-60s %-20s %s\n" "$basenameiso" "$injected" "$conflicts" + + fusermount -uz "$mnt" 2>/dev/null || umount "$mnt" 2>/dev/null || true rmdir "$mnt" 2>/dev/null done -rm -f "$STUB" "$TEMP_PARSER" "$FUNC_STUB" "$UNPACK_TEMP" +rm -f "$STUB" "$TEMP_PARSER" "$FUNC_STUB" "$UNPACK_TEMP" "$ISO_INIT_TEMP" From ca51e2334628d4ef4b4f4180bfa390c6c5c53b48 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Wed, 22 Apr 2026 13:49:57 -0400 Subject: [PATCH 09/10] WIP: Simplify ISO boot param injection, add installer detection - Simplify parameter injection to always inject iso-scan/filename as primary - Add installer ISO detection (warns user to use dd instead) - Remove Ventoy recommendations (not supported by Heads) - Keep USB filesystem check from initrd scanning --- initrd/bin/kexec-iso-init.sh | 53 +++++++++++++++++++----------------- tests/iso-parser/run.sh | 6 ++-- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/initrd/bin/kexec-iso-init.sh b/initrd/bin/kexec-iso-init.sh index 129de6eff..3d94db9c7 100755 --- a/initrd/bin/kexec-iso-init.sh +++ b/initrd/bin/kexec-iso-init.sh @@ -25,7 +25,7 @@ # If no mechanism is detected, the user is warned that the ISO may not # support booting from ISO file on USB, and is given alternative options: # - Write ISO directly to USB with dd -# - Use Ventoy in USB emulation mode +# - Write ISO directly to USB with dd # - Boot from real DVD drive # set -e -o pipefail @@ -262,12 +262,30 @@ extract_boot_params_from_cfg() { # ============================================================================ # Main detection flow # ============================================================================ -# Step 1: Scan initrd for supported boot mechanisms -# Step 2: If no boot method found, fall back to cfg file scanning +# Step 1: Check if ISO is an installer (not bootable from USB file) +# Step 2: Scan initrd for USB filesystem support and boot mechanisms # Step 3: Check USB filesystem compatibility # Step 4: If no known mechanism found, warn user with guidance # ============================================================================ +STATUS "Detecting ISO type..." +if [ -d "/boot/install.amd" ] || [ -d "/boot/install" ]; then + if [ -f "/boot/install.amd/vmlinuz" ] || [ -f "/boot/install/vmlinuz" ]; then + WARN "This appears to be an installer ISO" + WARN "Installer ISOs do not support booting from ISO file on USB" + if [ -x /bin/whiptail ]; then + if ! whiptail_warning --title 'INSTALLER ISO NOT SUPPORTED' --yesno \ + "This ISO is an installer and does not support booting from ISO file on USB.\n\nInstaller ISOs only work when written directly to USB with dd or when used as a DVD.\n\nTo use this ISO:\n- Linux: sudo cp image.iso /dev/sdX\n- Windows/Mac: Use Rufus in DD mode\n\nDo you want to try anyway?" \ + 0 80; then + DIE "Installer ISO - write to USB with dd" + fi + else + INPUT "Installer ISO - may not work from USB file. Try anyway? [y/N]:" -n 1 response + [ "$response" != "y" ] && [ "$response" != "Y" ] && DIE "Installer ISO - write to USB with dd" + fi + fi +fi + STATUS "Detecting USB filesystem and boot method support..." SUPPORTED_FSES="" SUPPORTED_BOOT="" @@ -312,7 +330,7 @@ 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 NOT SUPPORTED' --yesno \ - "This ISO does not support booting from ISO file on USB.\n\nThe initrd does not include boot-from-ISO mechanisms (no live-boot, casper, fromiso, iso-scan, anaconda, or nixos support detected).\n\nTo use this ISO, write the hybrid image directly to a USB flash drive:\n\nLinux: sudo cp image.iso /dev/sdX (Be cautious!)\nWindows/Mac: Use Rufus, select DD mode (NOT ISO mode)\n\nWrite to whole-disk device (NOT a partition, e.g. /dev/sdX not /dev/sdX1),\nthen boot from USB device directly (not as ISO file).\n\nSee Debian wiki: https://wiki.debian.org/DebianInstall" \ + "This ISO does not support booting from ISO file on USB.\n\nThe initrd does not include boot-from-ISO mechanisms (no live-boot, casper, fromiso, iso-scan, anaconda, or nixos support detected).\n\nTo use this ISO, write the hybrid image directly to a USB flash drive:\n\nLinux: sudo cp image.iso /dev/sdX (Be cautious!)\nWindows/Mac: Use Rufus, select DD mode (NOT ISO mode)\n\nWrite to whole-disk device (NOT a partition, e.g. /dev/sdX not /dev/sdX1),\nthen boot from USB device directly (not as ISO file)." \ 0 80; then DIE "ISO boot cancelled - initrd does not support USB file boot" fi @@ -327,34 +345,19 @@ fi # ============================================================================ # Boot parameter injection # ============================================================================ -# Inject all known boot-from-ISO parameters. The ISO's initrd will use +# Inject minimal boot-from-ISO parameters. The ISO's initrd will use # whichever parameters it understands and ignore the rest. # -# Parameters injected (covering all major boot systems): -# - findiso, fromiso, iso-scan/filename: Dracut standard -# - img_dev, img_loop: additional Dracut variants -# - iso: alternative parameter -# - live-media, live-media-path: live-boot parameters -# - boot=live, boot=casper: casper/live-boot parameters +# We inject iso-scan/filename as the primary parameter - this is +# the most widely supported boot-from-ISO parameter across distros. +# Other parameters (findiso, fromiso, img_dev, etc.) are injected +# as fallback for distros that need them. # ============================================================================ ISO_DEV="/dev/disk/by-uuid/$DEV_UUID" ISO_PATH_ABS="/$ISO_PATH" -base_params="findiso=$ISO_DEV/$ISO_PATH fromiso=$ISO_DEV/$ISO_PATH iso-scan/filename=$ISO_PATH_ABS img_dev=$ISO_DEV img_loop=$ISO_PATH iso=$DEV_UUID/$ISO_PATH" - -add_params="" -if echo "$DETECTED_METHODS" | grep -q "casper"; then - add_params="$add_params boot=casper live-media-path=casper" -fi -if echo "$DETECTED_METHODS" | grep -q "boot-live"; then - add_params="$add_params boot=live" -fi -if echo "$DETECTED_METHODS" | grep -q "live-media"; then - add_params="$add_params live-media=$ISO_DEV/$ISO_PATH" -fi - -ADD="$base_params $add_params" +ADD="iso-scan/filename=$ISO_PATH_ABS findiso=$ISO_DEV/$ISO_PATH fromiso=$ISO_DEV/$ISO_PATH img_dev=$ISO_DEV img_loop=$ISO_PATH iso=$DEV_UUID/$ISO_PATH" DEBUG "Injecting boot params: $ADD" REMOVE="" diff --git a/tests/iso-parser/run.sh b/tests/iso-parser/run.sh index 43d7004ac..607842fb4 100755 --- a/tests/iso-parser/run.sh +++ b/tests/iso-parser/run.sh @@ -18,11 +18,11 @@ # Compatibility status: # - OK: Known boot mechanism detected, should work # - WARN: No known mechanism detected, may work but unverified -# - SKIP: Installer ISO - use dd/Ventoy instead +# - SKIP: Installer ISO - use dd instead # # Tested ISOs (2026-04): # - Ubuntu Desktop, Debian Live, Tails, Fedora Live, NixOS, PureOS, Kicksecure: OK -# - Debian DVD installer: SKIP (use dd/Ventoy) +# - Debian DVD installer: SKIP (use dd) # - TinyCore/CorePlus: WARN (unverified) set -e @@ -182,7 +182,7 @@ check_compatibility() { local status="" local note="" case "$supported" in - installer*) status="SKIP" ; note=" (use dd/Ventoy)" ;; + installer*) status="SKIP" ; note=" (use dd)" ;; anaconda*) status="WARN" ; note=" (block device req)" ;; std) status="WARN" ;; "") status="WARN" ;; From a2f93051e1ec3f73ff9fd9958f069c4e8123e640 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Wed, 22 Apr 2026 13:50:59 -0400 Subject: [PATCH 10/10] WIP: Update docs and sim_iso_menu path fix - Add ISO boot documentation to development.md - Fix kexec-parse-boot.sh path in sim_iso_menu.sh --- doc/development.md | 32 ++++++++++++++++++++++++++++++++ sim_iso_menu.sh | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/doc/development.md b/doc/development.md index ece614222..cdf0e64d9 100644 --- a/doc/development.md +++ b/doc/development.md @@ -172,7 +172,39 @@ fusermount -u /tmp/iso-test/kicksecure - ✅ 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). diff --git a/sim_iso_menu.sh b/sim_iso_menu.sh index 5688c870a..290f9f247 100755 --- a/sim_iso_menu.sh +++ b/sim_iso_menu.sh @@ -45,7 +45,7 @@ for iso in "$ISO_DIR"/*.iso; do # Create patched parser that uses our stub PATCHED_PARSER="/tmp/sim_parser_$$.sh" sed 's|\. /etc/functions\.sh|. /tmp/host_etc/functions.sh|' \ - /home/user/heads-master/initrd/bin/kexec-parse-boot.sh >"$PATCHED_PARSER" + ./initrd/bin/kexec-parse-boot.sh >"$PATCHED_PARSER" chmod +x "$PATCHED_PARSER" # Parse all non-EFI configs