#!/bin/bash #=============================================================================== # mkimage-surgery.sh — R-21 slice B: replace a prepared Felhom ISO's UEFI boot loader with a # monolithic grub-mkimage image, so cheap AMI (AN3PLUS-class) firmware that can't relocate the ISO's # signed GRUB from USB (VALIDATION-n100-baremetal F1, `relocation 0x0`) boots. # # RUNS INSIDE the felhom-iso-assistant container; operates on /work/out.iso (the prepare-iso output) # and writes /work/final.iso + /work/grub-version.txt. NEVER touches the source ISO or the assistant's # answer/first-boot payload — only the EFI boot path. # # The recipe is the N100 run's PROVEN-LIVE workaround (do NOT re-derive it): build BOOTX64.EFI from # the ISO's OWN x86_64-efi GRUB modules (the box's working 2.12-9+pmx2 build), embedding the module # set the ISO's grub.cfg needs + an embedded config that `search --fs-uuid`es the ISO and # `configfile`s its real menu; swap it into the ISO9660 EFI/BOOT tree AND inside efi.img; re-master # with xorriso preserving BOTH the hybrid BIOS boot and the payload. The image is UNSIGNED (Secure # Boot must be OFF on the target) — that is the documented mkimage contract. #=============================================================================== set -euo pipefail OUT=/work/out.iso FINAL=/work/final.iso EX=/work/ex EMB=/work/embedded.cfg LDR=/work/BOOTX64.EFI say() { echo "mkimage-surgery: $*"; } # The osirrox extract tree is written by container-root; the host-side build cleanup (a non-root user) # cannot remove it. Remove it here (we ARE root in the container) on every exit path so no /tmp litter # survives the run. cleanup_ex() { if [[ -n "${EX:-}" && -e "$EX" ]]; then rm -rf "$EX" 2>/dev/null || true; fi; } trap cleanup_ex EXIT [[ -f "$OUT" ]] || { echo "mkimage-surgery: /work/out.iso missing" >&2; exit 2; } for t in xorriso grub-mkimage mcopy mdir; do command -v "$t" >/dev/null || { echo "mkimage-surgery: missing tool: $t" >&2; exit 2; } done grub-mkimage --version | head -1 > /work/grub-version.txt say "grub: $(cat /work/grub-version.txt)" # --- 1. extract the full prepared ISO tree (osirrox) — preserves the answer + first-boot payload. # osirrox reproduces the ISO's (read-only) file modes, so make the tree writable afterwards or # the loader swap (step 7) and the workspace cleanup can't overwrite/remove the files. --------- [[ -e "$EX" ]] && { chmod -R u+w "$EX" 2>/dev/null || true; rm -rf "$EX"; } mkdir -p "$EX" xorriso -osirrox on -indev "$OUT" -extract / "$EX" >/dev/null 2>&1 chmod -R u+w "$EX" say "extracted prepared ISO tree" # --- 2. GRUB build to assemble the loader from. The N100 fix used the box's OWN INSTALLED 2.12 GRUB # (a DIFFERENT, working build than the ISO's problem one — which is the whole point). The ISO # ships modules but NOT kernel.img, so grub-mkimage cannot use the ISO's module dir directly; # the box used its /usr/lib/grub/x86_64-efi. The container mirrors that: grub 2.12 == the PVE # 9.x ISO's 2.12-9+pmx2 generation. We take the module BINARIES from here and the module LIST # from the ISO's own grub.cfg (so we embed exactly what the ISO menu needs). -------------------- GDIR="" for d in /usr/lib/grub/x86_64-efi /usr/lib/grub/x86_64-efi-signed; do [[ -f "$d/kernel.img" ]] && { GDIR="$d"; break; } done [[ -n "$GDIR" ]] || { echo "mkimage-surgery: no usable GRUB x86_64-efi build (kernel.img) in the container" >&2; exit 3; } say "grub module source: $GDIR" GCFG="" for c in "$EX/boot/grub/grub.cfg" "$EX/boot/grub/x86_64-efi/grub.cfg"; do [[ -f "$c" ]] && { GCFG="$c"; break; } done # --- 3. module list: the base set the search/configfile-from-USB chain needs, PLUS every module the # ISO's grub.cfg insmod's. Keep only modules present in the GRUB build. This IS "every module # the grub.cfg needs". BASE="part_gpt part_msdos msdospart fat exfat iso9660 udf search search_fs_uuid search_fs_file search_label \ configfile normal boot linux linuxefi chain loadenv loopback echo test true cat ls help \ all_video efi_gop efi_uga video video_fb font gfxterm gfxterm_background gfxmenu png jpeg \ terminal serial gzio xzio lzopio minicmd reboot halt probe regexp sleep keystatus read" CFGMODS="" [[ -n "$GCFG" ]] && CFGMODS=$(grep -hoE 'insmod[[:space:]]+[a-zA-Z0-9_]+' "$GCFG" | awk '{print $2}' | sort -u) MODS="" for m in $BASE $CFGMODS; do [[ -f "$GDIR/$m.mod" ]] && MODS="$MODS $m" done MODS="$(echo "$MODS" | tr ' ' '\n' | awk 'NF' | sort -u | tr '\n' ' ')" say "embedding $(echo "$MODS" | wc -w) modules from the ISO's own x86_64-efi build" # --- 4. pin the volume modification-date so the ISO's GRUB fs-uuid is DETERMINISTIC and KNOWN before # we build the loader (GRUB's iso9660 fs_uuid is derived from the PVD modification timestamp). # Reuse the prepared ISO's own timestamp verbatim -> the embedded search matches the re-mastered # image (we pin the same value on re-master in step 8). MDATE="$(xorriso -indev "$OUT" -report_el_torito as_mkisofs 2>/dev/null \ | grep -oE "modification-date='[0-9]+'" | grep -oE '[0-9]+' | head -1)" [[ -n "$MDATE" && ${#MDATE} -ge 14 ]] || { echo "mkimage-surgery: could not read the ISO modification-date" >&2; exit 4; } ISO_UUID="$(echo "${MDATE:0:16}" | sed -E 's/^(.{4})(.{2})(.{2})(.{2})(.{2})(.{2})(.{2})$/\1-\2-\3-\4-\5-\6-\7/')" say "ISO fs-uuid (from modification-date $MDATE): $ISO_UUID" # --- 5. embedded config: find the ISO by fs-uuid, then chain its real menu (the recorded recipe) --- cat > "$EMB" <&2; exit 5; } say "built BOOTX64.EFI ($(stat -c%s "$LDR") bytes)" # --- 7. swap the loader into BOTH the ISO9660 EFI/BOOT tree AND inside the efi.img ESP. The ISO tree # uses Rock Ridge (LOWERCASE) names — `/efi/boot/bootx64.efi` — so overwrite the EXISTING files # in place (case-insensitive find), NEVER mkdir a spurious uppercase path. The efi.img ESP is # FAT (case-insensitive), the authoritative loader UEFI firmware actually runs from USB. -------- TREE_HITS=0 while IFS= read -r f; do cp "$LDR" "$f"; TREE_HITS=$((TREE_HITS+1)); done \ < <(find "$EX" -ipath '*/efi/boot/bootx64.efi') while IFS= read -r f; do cp "$LDR" "$f"; done \ < <(find "$EX" -ipath '*/efi/boot/grubx64.efi') [[ "$TREE_HITS" -ge 1 ]] || { echo "mkimage-surgery: no bootx64.efi found in the ISO9660 tree to replace" >&2; exit 6; } EFIIMG="$EX/efi.img" [[ -f "$EFIIMG" ]] || EFIIMG="$(find "$EX" -maxdepth 3 -iname 'efi*.img' | head -1)" [[ -f "$EFIIMG" ]] || { echo "mkimage-surgery: efi.img ESP not found in the ISO tree" >&2; exit 6; } # FAT is case-insensitive: ::/EFI/BOOT/BOOTX64.EFI resolves the real loader regardless of stored case. mcopy -i "$EFIIMG" -o "$LDR" ::/EFI/BOOT/BOOTX64.EFI if mdir -i "$EFIIMG" ::/EFI/BOOT 2>/dev/null | grep -qi grubx64; then mcopy -i "$EFIIMG" -o "$LDR" ::/EFI/BOOT/grubx64.efi fi say "swapped bootx64.efi in the ISO tree ($TREE_HITS) and inside $(basename "$EFIIMG")" # --- 8. re-master from the (modified) tree, reproducing the source ISO's boot geometry from its OWN # as_mkisofs report so we track PVE minor versions. We drop ONLY the Apple APM/HFS+ boot map # (-hfsplus / -apm-block-size): re-emitting it trips xorriso 1.5.6's "Overlapping MBR partition # entries" on THIS layout, and Mac boot is irrelevant for N100/PC hardware. We KEEP the hybrid # BIOS grub2-mbr + El Torito (BIOS eltorito.img + UEFI /efi.img) + the GPT EFI System Partition # (-efi-boot-part) that USB UEFI firmware boots from — the whole point of this fix. The volume # id + modification-date are pinned explicitly so the embedded fs-uuid stays valid. ------------- RPT="$(xorriso -indev "$OUT" -report_el_torito as_mkisofs 2>/dev/null)" VOLID="$(printf '%s\n' "$RPT" | sed -nE "s/^-V '(.*)'\$/\\1/p" | head -1)"; [[ -n "$VOLID" ]] || VOLID="PVE" # Drop, then re-add explicitly: the volume id + modification-date. Drop entirely: the Apple APM/HFS+ # map (-hfsplus / -apm-block-size) AND the isohybrid GPT-basdat marking (-part_like_isohybrid / # -isohybrid-gpt-basdat) — prepare-iso re-masters with these, and re-emitting them alongside # -efi-boot-part + the protective MBR trips xorriso 1.5.6's "Overlapping MBR partition entries". The # resulting image keeps the protective MBR + grub2-mbr (BIOS) + El Torito (BIOS+UEFI) + the GPT EFI # System Partition (verified). Repoint the grub2-mbr template at the in-container out.iso. FILTERED="$(printf '%s\n' "$RPT" \ | grep -vE "^-V '|^--modification-date=|^-apm-block-size |^-hfsplus\$|^-part_like_isohybrid\$|^-isohybrid-gpt-basdat\$" \ | sed -E "s#(--interval:[^']*:)'[^']*'#\\1'$OUT'#")" rm -f "$FINAL" # shellcheck disable=SC2086 eval xorriso -as mkisofs -V "'$VOLID'" --modification-date="'$MDATE'" \ $FILTERED -o "$FINAL" "$EX" >/work/xorriso.log 2>&1 \ || { echo "mkimage-surgery: xorriso re-master FAILED"; tail -25 /work/xorriso.log >&2; exit 7; } [[ -f "$FINAL" ]] || { echo "mkimage-surgery: no final.iso produced" >&2; exit 7; } # assert both boot images survived (BIOS eltorito.img + UEFI efi.img) — a silent loss would fail-safe # to an unbootable stick, so gate it here. ETIMG="$(xorriso -indev "$FINAL" -report_el_torito plain 2>/dev/null | grep -cE 'El Torito boot img')" [[ "$ETIMG" -ge 2 ]] || { echo "mkimage-surgery: re-master lost a boot image (El Torito entries=$ETIMG, want >=2)" >&2; exit 8; } say "re-mastered final.iso ($(stat -c%s "$FINAL") bytes; El Torito boot images=$ETIMG)" # --- 9. verify the re-mastered image kept the modification-date (so the embedded fs-uuid matches) --- FINAL_MDATE="$(xorriso -indev "$FINAL" -report_el_torito as_mkisofs 2>/dev/null \ | grep -oE "modification-date='[0-9]+'" | grep -oE '[0-9]+' | head -1)" if [[ "${FINAL_MDATE:0:14}" != "${MDATE:0:14}" ]]; then echo "mkimage-surgery: WARN final modification-date ($FINAL_MDATE) != source ($MDATE) — the search fs-uuid may not match; re-check" >&2 else say "fs-uuid preserved ($ISO_UUID)" fi say "done"