Skip to main content

max / alloy

iso: give the medium a real partition table -isohybrid-gpt-basdat is a no-op in this shape. It describes an El Torito image opened with -eltorito-alt-boot, and there is no primary entry for it to be an alternative to, so xorriso emitted no table and reported no error. The ISO's first 4096 bytes were zero: no MBR, no GPT, no discoverable ESP, which left booting a stick to how lenient the firmware felt. Every QEMU test attached the image with -cdrom, and optical emulation boots El Torito by design and never looks for a partition table, so nothing caught it. Append the ESP as a real partition instead and describe it with a GPT, which is the removable-media path UEFI actually specifies. El Torito is kept, pointed at those same bytes, so the optical path is unchanged. Then read the structures back, because the bug shipped behind a flag that exited 0. The checks cover the protective MBR, the GPT header, and the ESP entry itself; the last one matters because the others all pass on a GPT that describes no ESP at all. Verified against the pre-fix ISO, which fails them.
Author: Max Johnson <me@maxj.phd> · 2026-07-22 15:45 UTC
Signed with PGP, not checked
Commit: eed2a7bbf87b9e64abcc546911923e3fe0a03723
Parent: 59aec95
1 file changed, +69 insertions, -4 deletions
@@ -262,11 +262,15 @@
262 262 if command -v mcopy >/dev/null; then
263 263 mcopy -i "$WORK/efiboot.img" -s "$WORK/iso/EFI/BOOT/"* ::/EFI/BOOT/ 2>/dev/null || true
264 264 fi
265 - cp "$WORK/efiboot.img" "$WORK/iso/efiboot.img"
265 + # Deliberately not copied into $WORK/iso. It is appended to the medium as a
266 + # real partition below, and El Torito is pointed at that partition rather
267 + # than at a file in the tree, so a copy inside the ISO filesystem would be
268 + # a second copy of these same bytes that nothing reads.
266 269
267 270 # ---------------------------------------------------------------------
268 271 # 5. Assemble. UEFI only, matching the images bootc produces: they have no
269 - # legacy BIOS path, so a hybrid MBR would advertise a boot that fails.
272 + # legacy BIOS path, so the MBR must be protective and must not advertise
273 + # a BIOS boot that would fail.
270 274 # ---------------------------------------------------------------------
271 275 say "xorriso"
272 276 # -rock is not decoration here. An OCI layout stores every blob under a
@@ -275,17 +279,78 @@
275 279 # install fails looking like a corrupt image rather than a naming problem.
276 280 # The squashfs and the boot files have short names and never needed it,
277 281 # which is why it was absent until the install source became a directory.
282 + #
283 + # The partition table is written by -append_partition + -appended_part_as_gpt,
284 + # not by -isohybrid-gpt-basdat, which is what this call used to pass. That
285 + # option is a no-op in this shape: it describes an El Torito image that was
286 + # opened with -eltorito-alt-boot and there is no primary entry for it to be
287 + # an alternative to, so xorriso emitted no table and reported no error. The
288 + # result was an ISO whose first 4096 bytes were zero, i.e. no MBR, no GPT
289 + # and no discoverable ESP, which left booting a USB stick up to how lenient
290 + # the firmware felt. It survived because every QEMU test attached the ISO
291 + # with -cdrom, and optical emulation boots El Torito by design and never
292 + # looks for a partition table.
293 + #
294 + # Appending the ESP instead gives the firmware the removable-media path it
295 + # actually specifies: a protective 0xEE MBR, a GPT, and the ESP as a typed
296 + # partition. `-e --interval:appended_partition_2:all::` keeps El Torito as
297 + # well, pointed at those same bytes, so the optical path is unchanged.
298 + #
299 + # fdisk reports three partitions, not two. The third is the 300 KiB of
300 + # padding xorriso appends by default to work around kernels that read past
301 + # the end of the medium; -appended_part_as_gpt describes every region of
302 + # the image, padding included. It is expected, not a stray partition.
278 303 xorriso -as mkisofs \
279 304 -iso-level 3 \
280 305 -rock \
281 306 -volid "$VOLID" \
282 307 -appid "Alloy Installer" \
308 + -append_partition 2 C12A7328-F81F-11D2-BA4B-00A0C93EC93B "$WORK/efiboot.img" \
309 + -appended_part_as_gpt \
283 310 -eltorito-alt-boot \
284 - -e efiboot.img \
311 + -e --interval:appended_partition_2:all:: \
285 312 -no-emul-boot \
286 - -isohybrid-gpt-basdat \
287 313 -output "$OUTPUT/install.iso" \
288 314 "$WORK/iso"
289 315
316 + # ---------------------------------------------------------------------
317 + # 6. Read the boot structures back.
318 + #
319 + # The bug above shipped because the build trusted a flag that exited 0.
320 + # These checks read the bytes the firmware reads, so a table that silently
321 + # fails to appear fails the build instead of the stick.
322 + # ---------------------------------------------------------------------
323 + say "checking the partition table"
324 + at() { dd if="$OUTPUT/install.iso" bs=1 skip="$1" count="$2" 2>/dev/null | od -An -tx1 | tr -d ' \n'; }
325 +
326 + [ "$(at 510 2)" = "55aa" ] \
327 + || { echo "no MBR signature at offset 510; the ISO has no partition table" >&2; exit 1; }
328 + [ "$(at 450 1)" = "ee" ] \
329 + || { echo "MBR partition 1 is not type 0xEE; the MBR is not protective" >&2; exit 1; }
330 + [ "$(at 446 1)" = "00" ] \
331 + || { echo "MBR partition 1 is marked bootable; this image has no BIOS path" >&2; exit 1; }
332 + [ "$(at 462 16)" = "$(printf '00%.0s' $(seq 16))" ] \
333 + || { echo "MBR partition 2 is populated; only a protective entry belongs here" >&2; exit 1; }
334 + [ "$(dd if="$OUTPUT/install.iso" bs=512 skip=1 count=1 2>/dev/null | head -c 8)" = "EFI PART" ] \
335 + || { echo "no GPT header at LBA 1" >&2; exit 1; }
336 +
337 + # Every check above passes on a GPT that describes no ESP at all, which is the
338 + # one thing this shape exists to add, so read the entry array and find it. The
339 + # type GUID is stored mixed-endian: the first three fields little-endian, the
340 + # last two as written, so C12A7328-F81F-11D2-BA4B-00A0C93EC93B lands on disk as
341 + # the bytes below. A 16-byte substring match is not entry-aligned, but a GUID
342 + # occurring by chance anywhere else in the array is not a thing that happens.
343 + le() { local h=$1 out="" i; for ((i = ${#h} / 2 - 1; i >= 0; i--)); do out="$out${h:$((i * 2)):2}"; done; printf '%d\n' "0x$out"; }
344 + ENTRY_LBA=$(le "$(at $((512 + 72)) 8)")
345 + ENTRY_COUNT=$(le "$(at $((512 + 80)) 4)")
346 + ENTRY_SIZE=$(le "$(at $((512 + 84)) 4)")
347 + ARRAY=$(dd if="$OUTPUT/install.iso" bs=512 skip="$ENTRY_LBA" \
348 + count=$(( (ENTRY_COUNT * ENTRY_SIZE + 511) / 512 )) 2>/dev/null | od -An -tx1 | tr -d ' \n')
349 + case "$ARRAY" in
350 + *28732ac11ff8d211ba4b00a0c93ec93b*) ;;
351 + *) echo "the GPT describes no EFI system partition" >&2; exit 1 ;;
352 + esac
353 + say "protective MBR, GPT at LBA 1, ESP appended as partition 2"
354 +
290 355 chmod 0644 "$OUTPUT/install.iso"
291 356 say "built $(du -h "$OUTPUT/install.iso" | cut -f1) at $OUTPUT/install.iso"