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..cdf0e64d9 100644 --- a/doc/development.md +++ b/doc/development.md @@ -104,3 +104,107 @@ When touching the Makefile or build system: See [ux-patterns.md](ux-patterns.md) for `INPUT`, `STATUS`/`STATUS_OK`, `DO_WITH_DEBUG`, `HEADS_TTY` routing, and PIN caching conventions. + +## Testing ISO Boot Logic from Host + +ISO boot scripts (`kexec-iso-init.sh`, `kexec-parse-boot.sh`, `kexec-select-boot.sh`) +can be tested directly against mounted ISOs without building or running QEMU. + +### Heads Runtime Environment + +Heads runtime uses: + +- **Busybox** (unconditional) — coreutils (ls, cp, mv, dd, find, grep, sed, awk, etc.) +- **Bash** (`CONFIG_BASH=y` by default) — full bash for scripting +- **Shell shebang** — `#!/bin/bash` in scripts (bash is always available) +- **Tools** — kexec, blkid, cpio, xz, zstd, gzip for ISO boot handling + +See `config/busybox.config` for busybox features and `boards/*/` for module selection. + +### Mount ISO and Test + +```bash +# Mount an ISO (fuseiso works without root) +mkdir -p /tmp/iso-test/kicksecure +fuseiso -p /path/to/Kicksecure-LXQt-18.1.4.2.Intel_AMD64.iso /tmp/iso-test/kicksecure + +# Test initrd path extraction from GRUB configs +bootdir="/tmp/iso-test/kicksecure" +for cfg in $(find "$bootdir" -name '*.cfg' -type f 2>/dev/null); do + grep -E "^[ ]*initrd[ ]" "$cfg" | while read line; do + echo "$line" | awk '{for(i=1;i<=NF;i++) if($i=="initrd") print $(i+1)}' + done +done + +# Test initramfs unpacking +bash initrd/bin/unpack_initramfs.sh \ + /tmp/iso-test/kicksecure/live/initrd.img-6.12.69+deb13-amd64 \ + /tmp/initrd-unpacked + +# Test GRUB config parsing (kexec-parse-boot.sh logic) +bootdir="/tmp/iso-test/kicksecure" +for cfg in $(find "$bootdir" -name '*.cfg' -type f 2>/dev/null); do + bash initrd/bin/kexec-parse-boot.sh "$bootdir" "$cfg" +done + +# Cleanup +fusermount -u /tmp/iso-test/kicksecure +``` + +### Key Differences from Heads Runtime + +| Aspect | Heads Runtime | Host Testing | +|--------|-------------|--------------| +| Root filesystem | Read-only initramfs | Full system | +| `/boot` mount | FUSE/loopback of ISO | Direct ISO mount | +| `blkid` output | ISO9660 with UUID | Same | +| Device paths | `/dev/sda` etc | Same | +| `unpack_initramfs.sh` | Works the same | Works the same | +| Bash | Full bash available | Same | +| Busybox awk | Limited regex (no `[[:space:]]`) | Use `[ \t]` instead | +| TPM/PCR | N/A | N/A | +| GPG keys | Different | Different | + +### What Can Be Tested + +- ✅ GRUB/ISOLINUX config parsing (`kexec-parse-boot.sh`) +- ✅ Initrd path extraction from configs +- ✅ Initramfs unpacking and module scanning +- ✅ Boot method detection (boot=live, casper, etc.) +- ✅ Path handling (`/boot` prefix stripping) +- ⚠️ Combined boot params (injected params tested conceptually, not end-to-end) +- ❌ Actual `kexec` kernel loading +- ❌ TPM PCR extending +- ❌ Whiptail/GUI dialogs +- ❌ FUSE mount behavior inside initrd + +### Test Suite: `tests/iso-parser/run.sh` + +The test suite validates ISO boot compatibility: + +```bash +cd tests/iso-parser +./run.sh # test all ISOs +./run.sh /path/to/iso.iso # test single ISO +``` + +Output shows: +- **First section**: ISO metadata (entries count, hybrid MBR, sample boot params) +- **Second section**: Initramfs boot support detection (mechanisms found, compatibility) + +Compatibility status: +- **OK**: Known boot mechanism detected → should work via kexec-ISO-boot +- **WARN**: No known mechanism detected → may work but unverified +- **SKIP**: Installer ISO → use dd/Ventoy instead + +The test scans both: +1. **Initrd content** (primary): Unpacks initrd and searches for boot mechanisms +2. **Config files** (fallback): Greps *.cfg for known boot params + +Runtime injection (not tested): +- `findiso=`, `fromiso=`, `iso-scan/filename=`, `img_dev=`, `img_loop=` +- `live-media=`, `boot=live`, `boot=casper` + +These are injected unconditionally by `kexec-iso-init.sh` and combined with +parsed params in `kexec-boot.sh`. Duplicates resolve naturally (kernel uses +last value). diff --git a/initrd/bin/kexec-boot.sh b/initrd/bin/kexec-boot.sh index 4043c3118..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 @@ -11,13 +28,19 @@ printfiles="n" printinitrd="n" while getopts "b:e:r:a:o:fi" arg; do case $arg in - b) bootdir="$OPTARG" ;; - e) entry="$OPTARG" ;; - r) cmdremove="$OPTARG" ;; - a) cmdadd="$OPTARG" ;; - o) override_initrd="$OPTARG" ;; - f) dryrun="y"; printfiles="y" ;; - i) dryrun="y"; printinitrd="y" ;; + b) bootdir="$OPTARG" ;; + e) entry="$OPTARG" ;; + r) cmdremove="$OPTARG" ;; + a) cmdadd="$OPTARG" ;; + o) override_initrd="$OPTARG" ;; + f) + dryrun="y" + printfiles="y" + ;; + i) + dryrun="y" + printinitrd="y" + ;; esac done @@ -27,10 +50,15 @@ fi bootdir="${bootdir%%/}" -kexectype=`echo $entry | cut -d\| -f2` -kexecparams=`echo $entry | cut -d\| -f3- | tr '|' '\n'` +kexectype=$(echo $entry | cut -d\| -f2) +kexecparams=$(echo $entry | cut -d\| -f3- | tr '|' '\n') kexeccmd="kexec" +DEBUG "kexec-boot.sh: entry='$entry'" +DEBUG "kexec-boot.sh: kexectype='$kexectype'" +DEBUG "kexec-boot.sh: kexecparams='$kexecparams'" +DEBUG "kexec-boot.sh: cmdadd='$cmdadd'" + cmdadd="$CONFIG_BOOT_KERNEL_ADD $cmdadd" cmdremove="$CONFIG_BOOT_KERNEL_REMOVE $cmdremove" @@ -53,6 +81,9 @@ 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") @@ -60,22 +91,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 +144,7 @@ do fi fi fi - module_number=`expr $module_number + 1` + module_number=$(expr $module_number + 1) kexeccmd="$kexeccmd --module \"$filepath $cmdline\"" fi if [ "$key" = "initrd" ]; then @@ -135,7 +167,7 @@ do adjust_cmd_line kexeccmd="$kexeccmd --append=\"$cmdline\"" fi -done << EOF +done </dev/null \ -|| DIE "Failed to load the new kernel" +DEBUG "kexec-boot: executing kexec with adjusted_cmd_line=$adjusted_cmd_line kexectype=$kexectype" +echo "Loading kernel with: $kexeccmd" >/dev/console +DO_WITH_DEBUG eval "$kexeccmd" || + DIE "Failed to load the new kernel" -if [ "$CONFIG_DEBUG_OUTPUT" = "y" ];then +if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then #Ask user if they want to continue booting without echoing back the input (-s) INPUT "[DEBUG] Continue booting? [Y/n]:" -s -n 1 debug_boot_confirm if [ "${debug_boot_confirm^^}" = N ]; then diff --git a/initrd/bin/kexec-iso-init.sh b/initrd/bin/kexec-iso-init.sh index fa7b85ce9..3d94db9c7 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 +# - Write ISO directly to USB with dd +# - Boot from real DVD drive +# set -e -o pipefail . /etc/functions.sh . /etc/gui_functions.sh @@ -22,8 +50,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 @@ -47,11 +75,290 @@ else 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' -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" +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="" + local boot_content="" + + [ -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) + + 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 + + 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="" + local initrd_paths="" + + for cfg in $(find /boot -name '*.cfg' -type f 2>/dev/null); do + [ -r "$cfg" ] || continue + while IFS= read -r entry; do + [ -z "$entry" ] && continue + 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 + 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 + + for ipath in $initrd_paths; do + 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 +} + +# 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 + 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=*) + if ! echo "$boot_params" | grep -q "boot-live"; then + boot_params="${boot_params}boot-live " + fi + ;; + *iso-scan/filename=* | *findiso=*) + if ! echo "$boot_params" | grep -q "iso-scan"; then + boot_params="${boot_params}iso-scan " + fi + ;; + *live-media=* | *live.media=*) + if ! echo "$boot_params" | grep -q "live-media"; then + boot_params="${boot_params}live-media " + fi + ;; + *boot=casper* | *casper*) + if ! echo "$boot_params" | grep -q "casper"; then + boot_params="${boot_params}casper " + fi + ;; + *inst.stage2=* | *inst.repo=*) + if ! echo "$boot_params" | grep -q "anaconda"; then + boot_params="${boot_params}anaconda " + fi + ;; + *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" + [ -n "$boot_params" ] && echo "cfg:$boot_params" && return 0 + done + return 1 +} + +# ============================================================================ +# Main detection flow +# ============================================================================ +# 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="" +CFG_BOOT="" +DETECTED_METHODS="" + +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'" + +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 + DEBUG "Initrd supports USB filesystems: $SUPPORTED_FSES" + 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 +fi + +if [ -n "$SUPPORTED_BOOT" ]; then + DETECTED_METHODS="$SUPPORTED_BOOT" + 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 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)." \ + 0 80; then + DIE "ISO boot cancelled - initrd does not support USB file boot" + fi + else + 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 + +# ============================================================================ +# Boot parameter injection +# ============================================================================ +# Inject minimal boot-from-ISO parameters. The ISO's initrd will use +# whichever parameters it understands and ignore the rest. +# +# 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" + +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="" paramsdir="/media/kexec_iso/$ISO_PATH" @@ -59,14 +366,14 @@ check_config $paramsdir ADD_FILE=/tmp/kexec/kexec_iso_add.txt if [ -r $ADD_FILE ]; then - NEW_ADD=`cat $ADD_FILE` + NEW_ADD=$(cat $ADD_FILE) ADD=$(eval "echo \"$NEW_ADD\"") fi DEBUG "Overriding ISO kernel arguments with additions: $ADD" REMOVE_FILE=/tmp/kexec/kexec_iso_remove.txt if [ -r $REMOVE_FILE ]; then - NEW_REMOVE=`cat $REMOVE_FILE` + NEW_REMOVE=$(cat $REMOVE_FILE) REMOVE=$(eval "echo \"$NEW_REMOVE\"") fi DEBUG "Overriding ISO kernel arguments with suppressions: $REMOVE" diff --git a/initrd/bin/kexec-parse-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..b579cdfb9 100755 --- a/initrd/bin/kexec-parse-boot.sh +++ b/initrd/bin/kexec-parse-boot.sh @@ -1,14 +1,22 @@ #!/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 -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 +28,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 +44,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 +53,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` - ;; + 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- ` + label* | LABEL*) + state="syslinux" + reset_entry + name=$(echo $line | cut -c6-) + ;; esac } @@ -112,39 +104,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 +168,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 +183,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 <"${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 @@ -184,8 +188,9 @@ confirm_menu_option() { if [ "$gui_menu" = "y" ]; then default_text="Make default" [[ "$CONFIG_TPM_NO_LUKS_DISK_UNLOCK" = "y" ]] && default_text="${default_text} and boot" + kernel_path=$(echo "$kernel" | sed 's/^kernel \([^|]*\).*/\1/') whiptail_warning --title "Confirm boot details" \ - --menu "Confirm the boot details for $name:\n\n$(echo $kernel | fold -s -w 80) \n\n" 0 80 8 \ + --menu "Confirm the boot details for $name:\n\n$(echo "$kernel_path" | fold -s -w 80) \n\n" 0 80 8 \ -- 'd' "${default_text}" 'y' "Boot one time" \ 2>/tmp/whiptail || DIE "Aborting boot attempt" @@ -201,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() { @@ -211,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/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..290f9f247 --- /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|' \ + ./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..607842fb4 --- /dev/null +++ b/tests/iso-parser/run.sh @@ -0,0 +1,427 @@ +#!/bin/bash +# 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 instead +# +# Tested ISOs (2026-04): +# - Ubuntu Desktop, Debian Live, Tails, Fedora Live, NixOS, PureOS, Kicksecure: OK +# - Debian DVD installer: SKIP (use dd) +# - 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}" + +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 + +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() { :; } +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 + [ -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 + printf "%-60s %8s %10s %s\n" "$(basename "$iso")" "SKIP" "?" "fuseiso failed" + 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 + 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=$(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 -uz "$mnt" 2>/dev/null || umount "$mnt" 2>/dev/null || true + rmdir "$mnt" 2>/dev/null + rm -rf "$sim" "$entries" +done + +echo "" +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)" ;; + 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="" + + [ -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 + 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 + 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 + done + + 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" "$ISO_INIT_TEMP"