diff --git a/Makefile b/Makefile index 6f6451226..49d1c9eac 100644 --- a/Makefile +++ b/Makefile @@ -85,6 +85,7 @@ $(info Verbose example: ./docker_repro.sh make BOARD=$(BOARD) V=1) $(info ----------------------------------------------------------------------) endif $(info !!!!!! Build starts !!!!!!) +$(if $(BOARD),$(info COMMIT $(GIT_HASH) $(HEADS_GIT_VERSION)),) # Timestamps should be in ISO format diff --git a/boards/qemu-coreboot-fbwhiptail-tpm2-basic-usb-autoboot/qemu-coreboot-fbwhiptail-tpm2-basic-usb-autoboot.config b/boards/qemu-coreboot-fbwhiptail-tpm2-basic-usb-autoboot/qemu-coreboot-fbwhiptail-tpm2-basic-usb-autoboot.config new file mode 100644 index 000000000..326ccc01f --- /dev/null +++ b/boards/qemu-coreboot-fbwhiptail-tpm2-basic-usb-autoboot/qemu-coreboot-fbwhiptail-tpm2-basic-usb-autoboot.config @@ -0,0 +1,102 @@ +# Configuration for building a coreboot ROM that works in +# the qemu emulator in graphical mode thanks to FBWhiptail +# +# TPM can be used with a qemu software TPM (TIS, 2.0). +export CONFIG_COREBOOT=y +export CONFIG_COREBOOT_VERSION=25.09 +export CONFIG_LINUX_VERSION=6.1.8 + +CONFIG_COREBOOT_CONFIG=config/coreboot-qemu-tpm2.config +CONFIG_LINUX_CONFIG=config/linux-qemu.config + +#Enable only one RESTRICTED/BASIC boot modes below to test them manually (we cannot inject config under QEMU (no internal flashing) +#export CONFIG_RESTRICTED_BOOT=y +export CONFIG_BASIC=y +export CONFIG_BASIC_USB_AUTOBOOT=y + +#Enable HAVE_GPG_KEY_BACKUP to test GPG key backup drive (we cannot inject config under QEMU (no internal flashing)) +#export CONFIG_HAVE_GPG_KEY_BACKUP=y + + +#On-demand hardware support (modules.cpio) +CONFIG_LINUX_USB=y +CONFIG_LINUX_E1000=y +#CONFIG_MOBILE_TETHERING=y +#Runtime on-demand additional hardware support (modules.cpio) +export CONFIG_LINUX_USB_COMPANION_CONTROLLER=y + + + +#Modules packed into tools.cpio +ifeq "$(CONFIG_UROOT)" "y" +CONFIG_BUSYBOX=n +else +#Modules packed into tools.cpio +CONFIG_CRYPTSETUP2=y +CONFIG_FLASHPROG=y +CONFIG_FLASHTOOLS=y +CONFIG_GPG2=y +CONFIG_KEXEC=y +CONFIG_UTIL_LINUX=y +CONFIG_LVM2=y +CONFIG_MBEDTLS=y +CONFIG_PCIUTILS=y +#Runtime tools to write to MSR +CONFIG_MSRTOOLS=y +#Remote attestation support +# TPM2 requirements +CONFIG_TPM2_TSS=y +CONFIG_OPENSSL=y +#Remote Attestation common tools +CONFIG_POPT=y +CONFIG_QRENCODE=y +CONFIG_TPMTOTP=y +#HOTP based remote attestation for supported USB Security dongle +#With/Without TPM support +#CONFIG_HOTPKEY=y +#Nitrokey Storage admin tool (deprecated) +#CONFIG_NKSTORECLI=n +#GUI Support +#Console based Whiptail support(Console based, no FB): +#CONFIG_SLANG=y +#CONFIG_NEWT=y +#FBWhiptail based (Graphical): +CONFIG_CAIRO=y +CONFIG_FBWHIPTAIL=y +#Additional tools (tools.cpio): +#SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) +#CONFIG_DROPBEAR=y +endif + +#Runtime configuration +#Automatically boot if HOTP is valid +export CONFIG_AUTO_BOOT_TIMEOUT=5 +#TPM2 requirements +export CONFIG_TPM2_TOOLS=y +export CONFIG_PRIMARY_KEY_TYPE=ecc +#TPM1 requirements +#export CONFIG_TPM=y +#Enable DEBUG output +export CONFIG_DEBUG_OUTPUT=y +export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y +#Enable TPM2 pcap output under /tmp +# When enabled, tpmr writes TPM2 command/response capture to /tmp/tpm0.pcap +# (inside the Heads runtime). This can be inspected with Wireshark to debug +# TPM interaction similarly to a TPM bus sniffer. +export CONFIG_TPM2_CAPTURE_PCAP=y +#Enable quiet mode: technical information logged under /tmp/debug.log +export CONFIG_QUIET_MODE=n +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh +#text-based original init: +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh +export CONFIG_BOOT_REQ_HASH=n +export CONFIG_BOOT_REQ_ROLLBACK=n +export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" +export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0 debug" +export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" +export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm2-basic-usb-autoboot" +#export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" + +export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" + +BOARD_TARGETS := qemu diff --git a/config/busybox.config b/config/busybox.config index 7476779f2..3249969ac 100644 --- a/config/busybox.config +++ b/config/busybox.config @@ -601,7 +601,7 @@ CONFIG_DEFAULT_DEPMOD_FILE="modules.dep" # CONFIG_FEATURE_ACPID_COMPAT is not set # CONFIG_BLKDISCARD is not set CONFIG_BLKID=y -# CONFIG_FEATURE_BLKID_TYPE is not set +CONFIG_FEATURE_BLKID_TYPE=y CONFIG_BLOCKDEV=y # CONFIG_CAL is not set # CONFIG_CHRT is not set diff --git a/doc/boot-process.md b/doc/boot-process.md index 9e196f37f..99abd04bc 100644 --- a/doc/boot-process.md +++ b/doc/boot-process.md @@ -120,6 +120,24 @@ menu, system info, power off. When booting from an ISO file on USB media, `kexec-iso-init.sh` handles the ISO boot flow. Invoked from Options → Boot Options → "USB boot". +When a USB drive is a dd-written hybrid ISO (an iso9660 filesystem on the +whole raw device that also carries a partition table, e.g. Kicksecure's GPT +hybrid), `mount-usb.sh` probes the whole device with an actual read-only +`mount` via `first_mountable_usb_disk()` (centralized in +`initrd/etc/functions.sh`) instead of relying on `blkid TYPE="iso9660"` +tagging. This whole-disk probe runs only when `--whole-disk` is passed; +the default is partitions-only. The USB boot path opts in via +`mount_usb --whole-disk` (the `mount_usb()` wrapper in +`initrd/etc/gui_functions.sh` forwards its arguments to `mount-usb.sh`). +If the whole device mounts, `/dev/sdX` is used directly instead of a +partition. This is required because `list_usb_storage` returns only +the device's partitions, none of which are individually mountable. The +mount is validated by parse-boot (`kexec-select-boot.sh`): if the whole +device does not mount, or yields no boot entries, `media-scan.sh` retries +with the default partitions-only picker (`mount-usb.sh` with no flag). +The boot then proceeds via the bootable-USB path (kexec-select-boot) +rather than the ISO-file path. + ### Flow (execution order) The ISO boot flow consists of 7 steps, with branching after step 3: diff --git a/doc/busybox_perks.md b/doc/busybox_perks.md index a9183cea0..7c8e2489d 100644 --- a/doc/busybox_perks.md +++ b/doc/busybox_perks.md @@ -40,6 +40,20 @@ These are listed by `busybox --list` but the bash builtin takes precedence in th BusyBox awk supports `-F SEP` (field separator) and `-v VAR=VAL`. Sufficient for all Heads usage: `awk '{print $2}' /proc/mounts`, `index()`. +### blkid +BusyBox `blkid` emits `TYPE="..."` only when `CONFIG_FEATURE_BLKID_TYPE=y` +is set in `config/busybox.config`. Without it, blkid prints `UUID="..."` +but no TYPE, and any code parsing `TYPE=` silently gets empty. Which +filesystem types blkid can detect is gated by the `CONFIG_FEATURE_VOLUMEID_*` +probers (e.g. `FEATURE_VOLUMEID_ISO9660=y`). `blkid DEV` prints a single +line of space-separated `KEY="value"` pairs. Use `_get_blkid_fstype()` in +`initrd/etc/functions.sh` to extract the TYPE (prints empty when blkid +reports none); it is the shared helper for all fstype lookups. It is +currently used by `initrd/bin/kexec-iso-init.sh` for ISO filesystem-type +detection. `initrd/bin/mount-usb.sh` does not use blkid TYPE: it probes +whole USB disks with an actual read-only `mount` via +`first_mountable_usb_disk()`. + ### cpio **BusyBox quirk**: Stops at first TRAILER. GNU reads past it and exits 2. Heads pattern: `cpio -i -d "${CPIO_ARGS[@]}" 2>/dev/null || true`. diff --git a/doc/iso_boot.md b/doc/iso_boot.md index f9191dfaf..e96b907bc 100644 --- a/doc/iso_boot.md +++ b/doc/iso_boot.md @@ -170,9 +170,31 @@ partition table). The QEMU USB image creation in `targets/qemu.mk` now creates an MBR partition table + single ext4 partition to work around this. +**dd-written hybrid ISOs:** Kicksecure's dd'd ISO is an iso9660 filesystem +on the whole device plus a GPT partition table. Heads' `mount-usb.sh` +probes the whole device with an actual read-only `mount` (via +`first_mountable_usb_disk()` in `initrd/etc/functions.sh`) instead of +relying on blkid `TYPE="iso9660"` tagging. The probe runs only when +`--whole-disk` is passed; the default is partitions-only. The USB boot +path opts in via `mount_usb --whole-disk`. If the whole device mounts, +it is used directly, so such drives boot via the bootable-USB path. +If the whole device does not mount, or `kexec-select-boot` finds no boot +entries on it, `media-scan.sh` retries with the default partitions-only +picker (`mount-usb.sh` with no flag). Partitionless dd'd ISOs (Tails) and +MBR hybrids (Debian live) already work through the existing +partition/whole-device selection. + +Note: `mount_usb()` (defined in `initrd/etc/gui_functions.sh`) is an +interactive wrapper around the `mount-usb.sh` script. It unmounts any +prior `/media` mount, maps a picker abort (`mount-usb.sh` exit 5) to +exit 1, and re-prompts the user to insert a USB drive (then retries) on +mount failure. It forwards all its arguments to `mount-usb.sh`, so the +boot path calls `mount_usb --whole-disk` to opt into whole-disk probing +while retaining the interactive prompt and abort handling. + ## Test expectations -The ISO boot test (`initrd/tests/iso-test/iso-boot-test.sh`) verifies: +The ISO boot test (`tests/iso-test/iso-boot-test.sh`) verifies: 1. **Kernel display driver detection** — decompresses bzImage, searches for built-in driver symbols (`vesadrm_probe`, `vesafb_probe`, `simpledrm_probe`). @@ -251,7 +273,7 @@ kernel arguments. ## Distro Compatibility Notes -The test harness (`initrd/tests/iso-test/iso-boot-test.sh`) validates +The test harness (`tests/iso-test/iso-boot-test.sh`) validates USB boot **detection** — kernel display symbols, initramfs filesystem modules, isoboot keywords, and boot menu markers. It does **not** test whether the OS installs correctly with Heads' TPM+LUKS workflow. diff --git a/doc/logging.md b/doc/logging.md index bcf5dd0ca..5d7fb3249 100644 --- a/doc/logging.md +++ b/doc/logging.md @@ -274,6 +274,15 @@ DIE is for fatal errors from which Heads cannot recover. Execution stops after D DIE always goes to debug.log and is always shown on the console regardless of output mode. +## Exit codes + +Scripts terminate with a nonzero exit code on failure: + +* `DIE()` prints the fatal error and exits with code 1. +* `mount-usb.sh` exits with code 5 when the user aborts the USB disk + picker; `mount_usb()` in `initrd/etc/gui_functions.sh` maps that to + exit 1 (abort) so callers see a uniform abort/failure code. + ## INPUT INPUT is a direct replacement for the `echo "prompt"; read [flags] VAR` pattern. @@ -407,3 +416,21 @@ CONFIG_DEBUG_OUTPUT=y CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y CONFIG_QUIET_MODE=n ``` + +## Console Routing + +The kernel's `console=` parameter determines which device `/dev/console` +resolves to. With multiple `console=` parameters, the **last** one becomes +the primary console for `/dev/console` (see the kernel +`Documentation/admin-guide/serial-console.rst`). + +Implications for Heads log output: + +- Kernel `printk` output goes to ALL registered consoles. +- `STATUS` and `NOTE` write to `/dev/console` only -- they appear only on + the primary console. +- `DEBUG`, `WARN`, and `DIE` also write to `/dev/kmsg` in debug mode, + which the kernel broadcasts to all consoles -- so they appear on the + serial console even when the framebuffer is the primary console. +- whiptail dialogs render directly to `tty0` (the framebuffer console) + and never appear on the serial console. diff --git a/initrd/bin/kexec-iso-init.sh b/initrd/bin/kexec-iso-init.sh index 11007a8fc..e7635ae36 100755 --- a/initrd/bin/kexec-iso-init.sh +++ b/initrd/bin/kexec-iso-init.sh @@ -81,14 +81,6 @@ else trap _iso_cleanup EXIT fi -# Extract filesystem type (TYPE="...") from blkid output. -# blkid emits space-separated KEY="value" pairs; this pipeline -# splits on spaces and extracts the TYPE value. -_get_blkid_fstype() { - TRACE_FUNC - blkid "$1" 2>/dev/null | tr ' ' '\n' | sed -n 's/^TYPE="\(.*\)"$/\1/p' -} - # Source guard: when _HEADS_TEST=y, only load function definitions, skip main body # Guard placed at end-of-functions marker below; here we wrap the Step 1-2 # body that sits ahead of the function definitions. diff --git a/initrd/bin/kexec-select-boot.sh b/initrd/bin/kexec-select-boot.sh index 09111a974..6d27ae089 100755 --- a/initrd/bin/kexec-select-boot.sh +++ b/initrd/bin/kexec-select-boot.sh @@ -234,7 +234,9 @@ confirm_menu_option() { # so users can change their selection without restarting the boot flow. # The full cmdline combines the entry's parsed params with the global ADD # params (injected by kexec-iso-init.sh for ISO boot). - + # 'e' drops into vi to edit the boot entry (one-time runtime change). + # After an edit, 'd' (make default) is suppressed: the entry no longer + # matches the on-media boot config or the signed /boot state. # TODO : simplify to be able to use whiptail; too big for QubesOS # No GUI for now, sorry. # if [ "$gui_menu" = "y" ]; then @@ -247,23 +249,118 @@ confirm_menu_option() { # -- 'y' "Boot" 'd' "${default_text}" 'b' "Back to menu" \ # 2>/tmp/whiptail && option_confirm=$(cat /tmp/whiptail) || option_confirm="b" #else - STATUS " Confirm boot details for $name:" - STATUS " Kernel: $kernel" - STATUS " Initramfs: ${initrd:--}" - STATUS " Original kernel cmdline: ${params:--}" - [ -n "$CONFIG_BOOT_KERNEL_ADD" ] && STATUS " Board adds: $CONFIG_BOOT_KERNEL_ADD" - [ -n "$CONFIG_BOOT_KERNEL_REMOVE" ] && STATUS " Board removes: $CONFIG_BOOT_KERNEL_REMOVE" - [ -n "$add" ] && STATUS " ISO params: $add" - # Build final cmdline using shared function (matches kexec-boot.sh) - local _final_cmdline - _final_cmdline=$(_build_final_cmdline "$params" "$add" "$CONFIG_BOOT_KERNEL_REMOVE" "$CONFIG_BOOT_KERNEL_ADD") - STATUS " Final kernel cmdline: $_final_cmdline" - INPUT "Boot (Y), make default (d), back to menu (b) [Y/d/b]:" -n 1 option_confirm - [ -z "$option_confirm" ] && option_confirm="y" - return 0 + edited="n" + orig_option="$option" + orig_params="$params" + while true; do + STATUS " Confirm boot details for $name:" + STATUS " Kernel: $kernel" + STATUS " Initramfs: ${initrd:--}" + if [ "$edited" = "y" ]; then + STATUS " Original kernel cmdline: ${orig_params:--}" + else + STATUS " Original kernel cmdline: ${params:--}" + fi + [ -n "$CONFIG_BOOT_KERNEL_ADD" ] && STATUS " Board adds: $CONFIG_BOOT_KERNEL_ADD" + [ -n "$CONFIG_BOOT_KERNEL_REMOVE" ] && STATUS " Board removes: $CONFIG_BOOT_KERNEL_REMOVE" + [ -n "$add" ] && STATUS " ISO params: $add" + local _final_cmdline + _final_cmdline=$(_build_final_cmdline "$params" "$add" "$CONFIG_BOOT_KERNEL_REMOVE" "$CONFIG_BOOT_KERNEL_ADD") + STATUS " Final kernel cmdline: $_final_cmdline" + + if [ "$edited" = "y" ]; then + INPUT "Boot (Y), edit again (e), back to menu (b) [Y/e/b]:" -n 1 option_confirm + else + INPUT "Boot (Y), make default (d), edit (e), back to menu (b) [Y/d/e/b]:" -n 1 option_confirm + fi + [ -z "$option_confirm" ] && option_confirm="y" + + case "$option_confirm" in + y|Y) option_confirm="y"; return 0 ;; + d|D) + if [ "$edited" = "y" ]; then + WARN "Make default (d) is unavailable after editing: the entry no longer matches the on-media boot configuration." + continue + fi + option_confirm="d"; return 0 ;; + b|B) option_confirm="b"; return 0 ;; + e|E) edit_boot_entry || true; continue ;; + *) continue ;; + esac + done #fi } +# Edit the selected boot entry ($option) in vi, then re-parse and validate. +# One-time runtime modification: nothing is persisted to /boot or config.user. +# Returns 0 if the entry was modified and re-parsed (sets edited="y"). +# Returns 1 if the user discarded changes, left the entry empty, or entered +# an invalid line (original entry is left in place). +edit_boot_entry() { + TRACE_FUNC + local edit_file="/tmp/kexec/kexec_edit.txt" + local edited_line="" + + mkdir -p "$(dirname "$edit_file")" 2>/dev/null || true + + NOTE "Editing the boot entry with vi." + STATUS "Entry format: name|kexectype|kernel path|initrd path|append params" + INPUT "In vi: press i to start editing, Esc to stop editing, then type :wq and press Enter to save and exit, or :q! and press Enter to discard changes. Press Enter to open the editor..." ignored + printf '%s\n' "$option" > "$edit_file" + + # Attach vi to the real interactive terminal so it works on both the + # framebuffer and serial console (mirrors recovery()'s TTY redirection). + if [ -n "$HEADS_TTY" ]; then + vi "$edit_file" <"$HEADS_TTY" >"$HEADS_TTY" 2>&1 || true + else + vi "$edit_file" || true + fi + + if [ ! -s "$edit_file" ]; then + WARN "Edited boot entry is empty; keeping the original entry." + return 1 + fi + + # Entries are single-line; take the first line and trim surrounding space. + edited_line=$(head -1 "$edit_file") + edited_line=$(printf '%s' "$edited_line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + + if [ "$edited_line" = "$option" ]; then + DEBUG "edit_boot_entry: entry unchanged" + return 1 + fi + + option="$edited_line" + parse_option + if ! _validate_edited_entry; then + WARN "Edited boot entry is invalid. Restoring the original entry." + option="$orig_option" + parse_option + return 1 + fi + + edited="y" + WARN "Boot entry modified at runtime. This change applies to this boot only and is not saved." + return 0 +} + +# Validate an edited boot entry after parse_option has populated +# name/kexectype/kernel/initrd/params. Returns 0 when the entry is usable. +_validate_edited_entry() { + [ -n "$name" ] || return 1 + [ -n "$kexectype" ] || return 1 + case "$kexectype" in + elf|multiboot|xen) ;; + *) DEBUG "_validate_edited_entry: unknown kexectype '$kexectype'"; return 1 ;; + esac + [ -n "$kernel" ] || return 1 + if [ ! -r "$bootdir/${kernel#/}" ]; then + DEBUG "_validate_edited_entry: kernel not found: $bootdir/${kernel#/}" + return 1 + fi + return 0 +} + parse_option() { # Parse pipe-delimited boot entry into shell variables. # Entry format: entry_name|kexectype|field3|field4|field5 diff --git a/initrd/bin/media-scan.sh b/initrd/bin/media-scan.sh index a9b055595..49c123970 100755 --- a/initrd/bin/media-scan.sh +++ b/initrd/bin/media-scan.sh @@ -19,8 +19,9 @@ fi available_partitions="$(blkid | while read line; do echo $line | awk -F ":" {'print $1'}; done )" if [ "$1" == "usb" ]; then - # Mount the USB boot device - mount_usb || DIE "Unable to mount /media" + # Mount the USB boot device, probing whole disks first so a dd-written + # hybrid ISO (iso9660 on the whole device) is found before partitions. + mount_usb --whole-disk || DIE "Unable to mount /media" elif $(echo $available_partitions | grep -q "$1"); then if grep -q /media /proc/mounts; then umount /media \ @@ -33,6 +34,8 @@ fi # Get USB boot device USB_BOOT_DEV=$(grep "/media" /etc/mtab | cut -f 1 -d' ') +retried_whole_disk="n" + # Check for ISO first get_menu_option() { if [ -x /bin/whiptail ]; then @@ -77,36 +80,63 @@ get_menu_option() { fi } -# create ISO menu options - search recursively for ISO files -find /media -name "*.iso" -type f 2>/dev/null | sort -r > /tmp/iso_menu.txt || true -if [ `cat /tmp/iso_menu.txt | wc -l` -gt 0 ]; then - while true; do - option="" - option_index="" - option_confirm="" - while [ -z "$option" -a "$option_index" != "s" ] - do - get_menu_option +while true; do + # create ISO menu options - search recursively for ISO files + find /media -name "*.iso" -type f 2>/dev/null | sort -r > /tmp/iso_menu.txt || true + if [ `cat /tmp/iso_menu.txt | wc -l` -gt 0 ]; then + while true; do + option="" + option_index="" + option_confirm="" + while [ -z "$option" -a "$option_index" != "s" ] + do + get_menu_option + done + + MOUNTED_ISO="$option" + ISO="${option:7}" # remove /media/ to get device relative path + DO_WITH_DEBUG kexec-iso-init.sh "$MOUNTED_ISO" "$ISO" "$USB_BOOT_DEV" && break done + fi - MOUNTED_ISO="$option" - ISO="${option:7}" # remove /media/ to get device relative path - DO_WITH_DEBUG kexec-iso-init.sh "$MOUNTED_ISO" "$ISO" "$USB_BOOT_DEV" && break - done -fi - -# No *.iso files on media, try ordinary bootable USB - -if [ "$CONFIG_RESTRICTED_BOOT" = y ]; then - DIE "No ISO files found, bootable USB not allowed with Restricted Boot." -fi + # No *.iso files on media, try ordinary bootable USB -WARN "Could not find any ISO, trying bootable USB" -# Attempt to pull verified config from device -if [ -x /bin/whiptail ]; then - DO_WITH_DEBUG kexec-select-boot.sh -b /media -c "*.cfg" -u -g -s -else - DO_WITH_DEBUG kexec-select-boot.sh -b /media -c "*.cfg" -u -s -fi + if [ "$CONFIG_RESTRICTED_BOOT" = y ]; then + DIE "No ISO files found, bootable USB not allowed with Restricted Boot." + fi -DIE "Something failed in selecting boot" + WARN "Could not find any ISO, trying bootable USB" + # Attempt to pull verified config from device. + # Unlike the ISO-file path (kexec-iso-init.sh), do NOT pass -s here. + # -s is kexec-select-boot.sh's skip_confirm flag: with it, user_select() + # calls do_boot() directly and confirm_menu_option() never runs, so the + # full kexec command line is never shown. Omitting -s makes the dd'ed + # bootable-USB path show the same boot confirmation (and Edit [e]) as + # the ISO boot path. + if [ -x /bin/whiptail ]; then + # -s (skip_confirm) intentionally omitted: confirm before booting. + if DO_WITH_DEBUG kexec-select-boot.sh -b /media -c "*.cfg" -u -g; then + break + fi + else + # -s (skip_confirm) intentionally omitted: confirm before booting. + if DO_WITH_DEBUG kexec-select-boot.sh -b /media -c "*.cfg" -u; then + break + fi + fi + # The whole disk was mounted (e.g. a dd-written hybrid ISO) but + # yielded no boot entries. Retry once with the default + # partitions-only mount in case the bootable filesystem is on a + # partition instead. + if [ "$retried_whole_disk" = "n" ] && is_whole_disk "$USB_BOOT_DEV"; then + retried_whole_disk="y" + umount /media 2>/dev/null || true + if ! mount-usb.sh; then + WARN "No bootable filesystem found on USB media" + exit 1 + fi + USB_BOOT_DEV=$(grep "/media" /etc/mtab | cut -f 1 -d' ') + continue + fi + DIE "Something failed in selecting boot" +done diff --git a/initrd/bin/mount-usb.sh b/initrd/bin/mount-usb.sh index a22033108..5276a6cd1 100755 --- a/initrd/bin/mount-usb.sh +++ b/initrd/bin/mount-usb.sh @@ -16,6 +16,8 @@ parameters: --device: device to mount (default: first USB device found) --mountpoint: where to mount the device (default: /media) --pass: passphrase for LUKS device (default: none) + --whole-disk: probe whole USB disks (not partitions) for a bootable + filesystem, e.g. a dd-written hybrid ISO (default: off) --help: Show this help USAGE_END } @@ -24,6 +26,7 @@ MODE="ro" DEVICE="" MOUNTPOINT="/media" PASS="" +WHOLE_DISK="" #Only assign --mode, --device, --mountpoint and --pass parameters only if variables following them are not empty while [ $# -gt 0 ]; do @@ -56,6 +59,12 @@ while [ $# -gt 0 ]; do shift fi ;; + # --whole-disk: also probe the raw whole disk (not just partitions) + # for a bootable filesystem, e.g. a dd-written hybrid ISO (#2008). + --whole-disk) + WHOLE_DISK="y" + shift + ;; *) usage exit 1 @@ -78,7 +87,14 @@ else fi list_usb_storage >/tmp/usb_block_devices -if [ -z "$(cat /tmp/usb_block_devices)" ]; then +# Probe whole USB disks read-only for a bootable filesystem (e.g. a +# dd-written hybrid ISO). Non-destructive: each disk is mounted ro +# then unmounted; nothing is left mounted. +if [ -n "$WHOLE_DISK" ] && [ -z "$DEVICE" ]; then + USB_MOUNT_DEVICE="$(first_mountable_usb_disk "$MOUNTPOINT")" + [ -n "$USB_MOUNT_DEVICE" ] && DEBUG "Detected bootable filesystem on whole device $USB_MOUNT_DEVICE" +fi +if [ -z "$(cat /tmp/usb_block_devices)" ] && [ -z "$USB_MOUNT_DEVICE" ]; then if [ -x /bin/whiptail ]; then whiptail_warning --title 'USB Drive Missing' \ --msgbox "Insert your USB drive and press Enter to continue." 0 80 @@ -87,7 +103,14 @@ if [ -z "$(cat /tmp/usb_block_devices)" ]; then fi sleep 1 list_usb_storage >/tmp/usb_block_devices - if [ -z "$(cat /tmp/usb_block_devices)" ]; then +# Probe whole USB disks read-only for a bootable filesystem (e.g. a +# dd-written hybrid ISO). Non-destructive: each disk is mounted ro +# then unmounted; nothing is left mounted. + if [ -n "$WHOLE_DISK" ] && [ -z "$DEVICE" ]; then + USB_MOUNT_DEVICE="$(first_mountable_usb_disk "$MOUNTPOINT")" + [ -n "$USB_MOUNT_DEVICE" ] && DEBUG "Detected bootable filesystem on whole device $USB_MOUNT_DEVICE" + fi + if [ -z "$(cat /tmp/usb_block_devices)" ] && [ -z "$USB_MOUNT_DEVICE" ]; then if [ -x /bin/whiptail ]; then whiptail_error --title 'ERROR: USB Drive Missing' \ --msgbox "USB Drive Missing! Aborting mount attempt.\n\nPress Enter to continue." 0 80 @@ -98,7 +121,6 @@ if [ -z "$(cat /tmp/usb_block_devices)" ]; then fi fi -USB_MOUNT_DEVICE="" # Check if the user has specified a USB device if [ -n "$DEVICE" ]; then DEBUG "Checking if "$DEVICE" is a USB detected block device" @@ -109,8 +131,12 @@ if [ -n "$DEVICE" ]; then DIE "ERROR: Selected $DEVICE is not a USB block device" fi else + # USB_MOUNT_DEVICE was already set above when the whole raw device + # carries a bootable filesystem (e.g. a dd-written hybrid ISO), so + # the single-partition auto-select and the user pick below are + # skipped via the [ -z "$USB_MOUNT_DEVICE" ] guards. # Check for the common case: a single USB disk with one partition - if [ $(cat /tmp/usb_block_devices | wc -l) -eq 1 ]; then + if [ -z "$USB_MOUNT_DEVICE" ] && [ $(cat /tmp/usb_block_devices | wc -l) -eq 1 ]; then USB_MOUNT_DEVICE="$(cat /tmp/usb_block_devices)" fi # When a passphrase is provided and multiple devices are present, @@ -167,6 +193,7 @@ else fi if [ "$option_index" = "a" ]; then + # exit 5 = user aborted the USB disk picker; mount_usb() in gui_functions.sh maps this to exit 1 (abort). exit 5 fi USB_MOUNT_DEVICE=$(head -n $option_index /tmp/usb_disk_list | tail -1 | sed 's/\ .*$//') @@ -204,6 +231,7 @@ fi # Mount the USB device if [ "$MODE" = "rw" ]; then DEBUG "Mounting $USB_MOUNT_DEVICE as read-write" + umount "$MOUNTPOINT" 2>/dev/null || true mount -o rw "$USB_MOUNT_DEVICE" "$MOUNTPOINT" || DIE "ERROR: Failed to mount ${USB_MOUNT_DEVICE} as read-write" else DEBUG "Mounting $USB_MOUNT_DEVICE as read-only" diff --git a/initrd/bin/usb-autoboot.sh b/initrd/bin/usb-autoboot.sh index 851f6ebe1..f7e2352be 100755 --- a/initrd/bin/usb-autoboot.sh +++ b/initrd/bin/usb-autoboot.sh @@ -28,9 +28,18 @@ parse_boot_options() } # Look for any bootable USB medium. -list_usb_storage >/tmp/usb-autoboot-usb-storage +# Try whole disks first (a dd-written hybrid ISO like Kicksecure is an +# iso9660 filesystem on the whole device, hidden behind its partition +# table), then partitions. The mount below skips any device that is not a +# mountable filesystem. Fixes linuxboot/heads#2008. +list_usb_storage disks > /tmp/usb-autoboot-usb-storage +list_usb_storage >> /tmp/usb-autoboot-usb-storage +# The two listings can overlap (whole disk sdX vs its partition sdX1), +# so dedupe preserving first-seen order before probing. +awk '!seen[$0]++' /tmp/usb-autoboot-usb-storage > /tmp/usb-autoboot-usb-storage.dedup +mv /tmp/usb-autoboot-usb-storage.dedup /tmp/usb-autoboot-usb-storage while read -u 4 -r USB_BLOCK_DEVICE; do - mount "$USB_BLOCK_DEVICE" /media || continue + mount "$USB_BLOCK_DEVICE" /media 2>/dev/null || continue USB_DEFAULT_BOOT="$(parse_boot_options /media | head -1)" if [ -n "$USB_DEFAULT_BOOT" ]; then # Boot automatically, unless the user interrupts. diff --git a/initrd/etc/functions.sh b/initrd/etc/functions.sh index d6723f21d..2f5f6a15e 100644 --- a/initrd/etc/functions.sh +++ b/initrd/etc/functions.sh @@ -1783,6 +1783,29 @@ disk_info_sysfs() { printf "%s" "${disk_info%$'\n'}" } +# Try to mount each USB disk read-only to find one carrying a bootable +# filesystem on the whole device (e.g. a dd-written hybrid ISO). +# Non-destructive: each disk is mounted ro then immediately unmounted, +# so nothing is left mounted. Returns the first mountable device, or +# nothing (exit 1) if none. +first_mountable_usb_disk() { + local mountpoint="$1" dev + for dev in $(list_usb_storage disks 2>/dev/null); do + if mount -o ro "$dev" "$mountpoint" 2>/dev/null; then + umount "$mountpoint" 2>/dev/null + echo "$dev" + return 0 + fi + done + return 1 +} + +# Return 0 if $1 is a whole USB disk (not a partition), testing +# membership in the whole-disk list from list_usb_storage disks. +is_whole_disk() { + [ -n "$1" ] && list_usb_storage disks 2>/dev/null | grep -qxF "$1" +} + list_usb_storage() { TRACE_FUNC # List all USB storage devices, including partitions unless we received argument stating we want drives only @@ -3032,6 +3055,14 @@ initrd_fs_type_to_kmod() { esac } +# Extract the filesystem TYPE from blkid output. blkid emits a single line of +# space-separated KEY="value" pairs; this pipeline splits them and prints the +# TYPE value (empty if blkid did not report a TYPE). +_get_blkid_fstype() { + TRACE_FUNC + blkid "$1" 2>/dev/null | tr ' ' '\n' | sed -n 's/^TYPE="\(.*\)"$/\1/p' +} + # Check whether a kernel binary has a filesystem driver built-in # (CONFIG_EXFAT_FS=y style). Decompresses the kernel at each # compression magic offset and greps for built-in filesystem init diff --git a/initrd/etc/gui_functions.sh b/initrd/etc/gui_functions.sh index cf2ab6d42..bf406d563 100755 --- a/initrd/etc/gui_functions.sh +++ b/initrd/etc/gui_functions.sh @@ -20,11 +20,14 @@ mount_usb() { umount /media || DIE "Unable to unmount /media" fi # Mount the USB boot device - mount-usb.sh && USB_FAILED=0 || ([ $? -eq 5 ] && exit 1 || USB_FAILED=1) + # Forward "$@" (e.g. --whole-disk) to mount-usb.sh. It exits 5 when + # the user aborts the USB disk picker; map that to exit 1 (abort). + mount-usb.sh "$@" && USB_FAILED=0 || ([ $? -eq 5 ] && exit 1 || USB_FAILED=1) if [ $USB_FAILED -ne 0 ]; then whiptail_error --title 'USB Drive Missing' \ --msgbox "Insert your USB drive and press Enter to continue." 0 80 - mount-usb.sh && USB_FAILED=0 || ([ $? -eq 5 ] && exit 1 || USB_FAILED=1) + # mount-usb.sh exits 5 when the user aborts the USB disk picker; map that to exit 1 (abort). + mount-usb.sh "$@" && USB_FAILED=0 || ([ $? -eq 5 ] && exit 1 || USB_FAILED=1) if [ $USB_FAILED -ne 0 ]; then whiptail_error --title 'ERROR: Mounting /media Failed' \ --msgbox "Unable to mount USB device" 0 80 diff --git a/initrd/tests/iso-test/README.md b/tests/iso-test/README.md similarity index 68% rename from initrd/tests/iso-test/README.md rename to tests/iso-test/README.md index d7a781803..b35fafd21 100644 --- a/initrd/tests/iso-test/README.md +++ b/tests/iso-test/README.md @@ -26,11 +26,23 @@ whether the initramfs scripts actually implement `iso-scan/filename` or | ISO | Reason | |-----|--------| -| **openSUSE Tumbleweed DVD** | Compiled linuxrc binary as /init (no dracut-live, no | -| (2026-06-05) | findiso/fromiso/iso-scan support). Network boot fallback. | -| **Debian 13 DVD** | installer image (iso9660 only), not a hybrid/live ISO | +| **openSUSE Tumbleweed DVD** | Installer ISO: compiled linuxrc binary as /init (no | +| (2026-06-05) | dracut-live, no findiso/fromiso/iso-scan support). | +| | Network boot fallback. Whole device IS ISO9660-mountable (MBR). | +| **Debian 13 DVD** | Installer image (iso9660 only), not a hybrid/live ISO | | (2026-12-15) | designed for USB boot. d-i netinst/installer images | -| | are built for CD boot, not USB loopback. | +| | are built for CD boot, not USB loopback. Whole device IS | +| | ISO9660-mountable (MBR). | +| **NixOS** | Partitioned disk image: the whole device is NOT | +| | ISO9660-mountable, so the whole-device mount probe fails and | +| | a partition must be mounted. The genuine "partitioned image, | +| | no ISO9660 on the whole device" case. | + +Note: the openSUSE/Debian DVD entries require dd because their installer +initramfs has no file-based ISO boot support (the whole device still +mounts as ISO9660). NixOS is listed because when dd'd, the whole device +is not ISO9660-mountable -- only a partition carries the filesystem; it +otherwise boots fine from a USB file via `findiso`. ## Kernel probe symbol mapping by kernel version @@ -58,23 +70,34 @@ eras with a single decompression pass: ### Tested ISO Versions -| ISO file | Distro | Kernel | Compression | -|----------|--------|--------|-------------| -| `ubuntu-26.04-desktop-amd64.iso` | Ubuntu 26.04 | 7.0.0 | zstd | -| `debian-live-13.2.0-amd64-kde.iso` | Debian 13 Trixie | 6.12.57 | zstd | -| `debian-live-13.2.0-amd64-xfce.iso` | Debian 13 Trixie | 6.12.57 | zstd | -| `debian-13.2.0-amd64-DVD-1.iso` | Debian 13 installer | 6.12.57 | gzip | -| `Fedora-Workstation-Live-43-1.6.x86_64.iso` | Fedora 43 Workstation | 6.17.1 | xz/zstd | -| `Fedora-Silverblue-ostree-x86_64-43-1.6.iso` | Fedora 43 Silverblue | 6.17.1 | xz/zstd | -| `Kicksecure-LXQt-18.1.4.2.Intel_AMD64.iso` | Kicksecure 18.1 | 6.12.69 | zstd | -| `Qubes-R4.3.1-rc1-x86_64.iso` | Qubes OS R4.3 | 6.12/6.17 | xz | -| `nixos-graphical-25.11.*.iso` | NixOS 25.11 | 6.12/6.18 | zstd | -| `pureos-11-gnome-live-20260515_amd64.iso` | PureOS 11 | 6.12 | zstd | -| `tails-amd64-7.8.1.iso` | Tails 7.8 | 6.12.74 | xz | -| `openSUSE-Tumbleweed-KDE-Live-*.iso` | openSUSE TW KDE Live | 7.0.11 | gzip | -| `openSUSE-Tumbleweed-DVD-*.iso` | openSUSE TW DVD | 7.0.11 | gzip | -| `CorePlus-current.iso` | TinyCore 15 | 6.18 | gzip | -| `Samsung_SSD_990_PRO_8B2QJXD7.iso` | Samsung firmware | - | gzip | +| ISO file | Distro | Kernel | Compression | Layout | +|----------|--------|--------|-------------|--------| +| `ubuntu-26.04-desktop-amd64.iso` | Ubuntu 26.04 | 7.0.0 | zstd | hybrid (GPT) | +| `debian-live-13.2.0-amd64-kde.iso` | Debian 13 Trixie | 6.12.57 | zstd | hybrid (MBR) | +| `debian-live-13.2.0-amd64-xfce.iso` | Debian 13 Trixie | 6.12.57 | zstd | hybrid (MBR) | +| `debian-13.2.0-amd64-DVD-1.iso` | Debian 13 installer | 6.12.57 | gzip | hybrid (MBR) | +| `Fedora-Workstation-Live-43-1.6.x86_64.iso` | Fedora 43 Workstation | 6.17.1 | xz/zstd | hybrid (GPT) | +| `Fedora-Silverblue-ostree-x86_64-43-1.6.iso` | Fedora 43 Silverblue | 6.17.1 | xz/zstd | hybrid (GPT) | +| `Kicksecure-LXQt-18.1.4.2.Intel_AMD64.iso` | Kicksecure 18.1 | 6.12.69 | zstd | hybrid (GPT, 3 partitions) | +| `Qubes-R4.3.1-rc1-x86_64.iso` | Qubes OS R4.3 | 6.12/6.17 | xz | hybrid (GPT) | +| `nixos-graphical-25.11.*.iso` | NixOS 25.11 | 6.12/6.18 | zstd | partitioned image (no whole-device ISO9660) | +| `pureos-11-gnome-live-20260515_amd64.iso` | PureOS 11 | 6.12 | zstd | hybrid (MBR) | +| `tails-amd64-7.8.1.iso` | Tails 7.8 | 6.12.74 | xz | plain ISO9660 (no partition table) | +| `openSUSE-Tumbleweed-KDE-Live-*.iso` | openSUSE TW KDE Live | 7.0.11 | gzip | hybrid (GPT) | +| `openSUSE-Tumbleweed-DVD-*.iso` | openSUSE TW DVD | 7.0.11 | gzip | hybrid (MBR) | +| `CorePlus-current.iso` | TinyCore 15 | 6.18 | gzip | hybrid (MBR) | +| `Samsung_SSD_990_PRO_8B2QJXD7.iso` | Samsung firmware | - | gzip | hybrid (MBR) | + +**Layout legend** (from fdisk/fuseiso testing): + +- **hybrid (GPT/MBR)** -- whole-image ISO9660-mountable: the dd'd drive + carries an iso9660 filesystem on the whole device plus a partition + table, so `mount_usb --whole-disk` mounts the whole device directly. +- **plain ISO9660** -- no partition table; the whole device is the + iso9660 filesystem. +- **partitioned image** -- the whole device is NOT ISO9660-mountable; + only a partition carries the filesystem, so the default partitions-only + picker (`mount-usb.sh` with no flag) is required. ### Expected Results diff --git a/initrd/tests/iso-test/iso-boot-test.sh b/tests/iso-test/iso-boot-test.sh similarity index 91% rename from initrd/tests/iso-test/iso-boot-test.sh rename to tests/iso-test/iso-boot-test.sh index 1866344f5..f95345fdc 100755 --- a/initrd/tests/iso-test/iso-boot-test.sh +++ b/tests/iso-test/iso-boot-test.sh @@ -18,7 +18,7 @@ # Inside initramfs: run mount-usb.sh first, then test /media: # mount-usb.sh && /tests/iso-test/iso-boot-test.sh --iso-dir /media # On host (from repo root): -# ./initrd/tests/iso-test/iso-boot-test.sh --iso-dir ~/Downloads/ISOs +# ./tests/iso-test/iso-boot-test.sh --iso-dir ~/Downloads/ISOs PASS=0; FAIL=0; SKIP=0; WITH_ISOS="n"; ISOS="" TMPDIR="/tmp/iso_boot_test_$$" @@ -58,7 +58,7 @@ if [ -d "/etc" ] && [ -f "/etc/functions.sh" ]; then _PATCHED_SELECT="$KEXEC_SELECT" else # On host (development): paths relative to repo - REPO="$(cd "$(dirname "$0")/../../.." && pwd)" + REPO="$(cd "$(dirname "$0")/../.." && pwd)" FUNCTIONS="$REPO/initrd/etc/functions.sh" GUI_FUNCTIONS="$REPO/initrd/etc/gui_functions.sh" KEXEC_ISO="$REPO/initrd/bin/kexec-iso-init.sh" @@ -271,6 +271,49 @@ check_marker "fs=OK display=blank" "[~]" "$(boot_marker)" rm -f /tmp/kexec_initramfs_compat.txt /tmp/kexec_display_driver.txt unset initrd gui_menu echo "" + +# ================================================================ +# Step 7b (production): boot entry edit + validation +# ================================================================ +echo "=== Step 7b: Boot entry edit + validation ===" +echo "" + +# Dummy readable kernel so _validate_edited_entry path checks pass. +TMP_LIVE_DIR="$TMPDIR/live" +mkdir -p "$TMP_LIVE_DIR" +: > "$TMP_LIVE_DIR/vmlinuz" +bootdir="$TMPDIR" + +# Valid entry: parse_option populates globals, validation passes +option="test|elf|kernel /live/vmlinuz|initrd /live/initrd|append root=/dev/sda1" +parse_option +check "$([ "$name" = "test" ] && [ "$kernel" = "/live/vmlinuz" ] && echo pass || echo fail)" \ + "parse_option sets name/kernel from pipe-delimited entry" +check "$([ "$params" = "root=/dev/sda1" ] && echo pass || echo fail)" \ + "parse_option sets params from append field" +check "$(_validate_edited_entry && echo pass || echo fail)" \ + "_validate_edited_entry accepts valid entry" + +# kexectype outside the whitelist -> reject, then restore valid state +option="test|badtype|kernel /live/vmlinuz|initrd /live/initrd|append root=/dev/sda1" +parse_option +check "$(_validate_edited_entry && echo fail || echo pass)" \ + "_validate_edited_entry rejects kexectype outside whitelist" + +# Missing kernel file -> reject, then restore valid state +option="test|elf|kernel /live/missing|initrd /live/initrd|append root=/dev/sda1" +parse_option +check "$(_validate_edited_entry && echo fail || echo pass)" \ + "_validate_edited_entry rejects missing kernel file" + +# Round-trip: edited append value survives into the final kexec cmdline +option="test|elf|kernel /live/vmlinuz|initrd /live/initrd|append root=/dev/sda2 single" +parse_option +r=$(_build_final_cmdline "$params" "" "" "") +check "$(echo "$r" | grep -qF "root=/dev/sda2" && echo pass || echo fail)" \ + "edited append root= appears in final kernel cmdline" +unset bootdir option params name kernel kexectype initrd +echo "" echo "" # ================================================================