Files
felhom.eu/scripts/iso/mkimage-surgery.sh
T
admin 3172df1927 scripts v1.18.0 — R-21 slice B: firmware loader option --loader shim|mkimage (F1)
Closes N100 F1 (HIGH): cheap AMI (AN3PLUS 0.01-class) UEFI firmware can't
relocate the ISO's stock signed GRUB from USB (relocation 0x0). The run's live
grub-mkimage workaround is now a first-class pipeline mode.

- build-felhom-iso.sh: --loader shim|mkimage (default shim, byte-for-byte
  unchanged; profile-settable FELHOM_LOADER; --loader wins). Loud banner +
  manifest loader:/grub-mkimage: fields + -mkimage filename suffix.
- mkimage-surgery.sh (new): post-prepare-iso, in the assistant container. Builds
  a monolithic grub-mkimage loader from the ISO's own GRUB (module set from its
  grub.cfg; embedded search --fs-uuid -> configfile the real menu). Swaps it into
  the ISO9660 tree (real lowercase path) + the efi.img ESP; xorriso re-master
  preserves BIOS-hybrid + UEFI + GPT-ESP, drops only Apple HFS+/APM. Recipe from
  the N100 run evidence, not re-derived.
- Dockerfile.assistant: grub-common + grub-efi-amd64-bin + mtools + dosfstools.
  profiles/n100.profile (new, mkimage + SB-off note).
- Validated on nested VM 311 (RUNBOOK-B legs): leg1 shim boots+installs under
  OVMF SB-enforcing + SeaBIOS; leg2 mkimage boots+installs under SB-off; leg3
  (red-proof) mkimage under SB-enforcing FAILS Access Denied (unsigned -> SB must
  be OFF); leg4 surgery byte-identical payload. bash -n + shellcheck clean.
  Physical N100 closure folds into the rehearsal (n100-safety match-nothing ISO
  built + sha-recorded, unbooted). PXE stays a deferred R-21 note.
2026-07-17 14:28:10 +02:00

169 lines
10 KiB
Bash

#!/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" <<CFG
search --no-floppy --fs-uuid --set=root $ISO_UUID
if [ -z "\$root" ]; then search --no-floppy --file --set=root /boot/grub/grub.cfg; fi
set prefix=(\$root)/boot/grub
configfile (\$root)/boot/grub/grub.cfg
CFG
# --- 6. build the monolithic BOOTX64.EFI from the ISO's OWN modules (-d $GDIR) --------------------
# shellcheck disable=SC2086
grub-mkimage -O x86_64-efi -d "$GDIR" -p /boot/grub -c "$EMB" -o "$LDR" $MODS
[[ -s "$LDR" ]] || { echo "mkimage-surgery: grub-mkimage produced no image" >&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"