archy/.claude/skills/iso-debug/references/boot-chain-reference.md
Dorian 6cfb8082c5 fix: xorriso append_partition for real USB boot + grub-mkstandalone
Root cause of USB boot failure: our xorriso used -e boot/grub/efi.img
to embed the EFI image inside the ISO. This works for CD-ROM and QEMU
but NOT for USB on real UEFI hardware.

Fix: use the Will Haley / Debian live-build approach:
- -append_partition 2 (GPT type EFI) appends efi.img AFTER ISO data
- -e --interval:appended_partition_2:all:: references the appended partition
- --mbr-force-bootable forces MBR active flag
- grub-mkstandalone with embedded bootstrap config (searches for grub.cfg)
- grub.cfg placed in both /boot/grub/ AND /EFI/BOOT/ on ISO
- grub.cfg uses search --label ARCHIPELAGO to find the ISO root

This is the exact approach used by StartOS, TAILS, and every production
custom Debian live ISO that boots from USB.

Also: iso-debug, iso-branding skills + reference docs, dev-start.sh
option 0 for branding dev, improved dev-branding.sh and test-iso-qemu.sh.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 11:34:29 +00:00

368 lines
13 KiB
Markdown

# Custom Debian ISO Boot Chain — Technical Reference
Expert reference for building and debugging custom bootable Debian-based ISOs.
Covers hybrid MBR/GPT, live-boot, debootstrap, GRUB, ISOLINUX, Plymouth, and xorriso.
---
## 1. Hybrid MBR/GPT for USB Boot
### What is isohdpfx.bin?
The first 432 bytes of a hybrid-bootable ISO. Contains the Master Boot Record code that BIOS firmware executes when booting from USB. Different sources produce different MBR code:
| Source | First bytes | Compatibility |
|--------|-------------|---------------|
| Debian Live ISO (`dd if=debian-live.iso bs=1 count=432`) | `45 52` | Best — works on all tested hardware |
| `/usr/lib/ISOLINUX/isohdpfx.bin` | `33 ed` | Generic — fails on some UEFI hardware |
| Manually built with `isohybrid` | Varies | Unpredictable |
**Rule**: Always extract MBR from a known-working ISO. Never rely on the generic ISOLINUX one.
### xorriso flags for hybrid boot
```bash
xorriso -as mkisofs -o output.iso \
-isohybrid-mbr isohdpfx.bin \ # Embeds MBR for BIOS USB boot
-c isolinux/boot.cat \ # El Torito boot catalog
-b isolinux/isolinux.bin \ # BIOS bootloader
-no-emul-boot -boot-load-size 4 -boot-info-table \
-eltorito-alt-boot \ # Second boot entry (EFI)
-e boot/grub/efi.img \ # EFI boot image
-no-emul-boot \
-isohybrid-gpt-basdat \ # Adds GPT partition for EFI
-partition_offset 16 \ # Space for GPT table — REQUIRED for UEFI
/path/to/iso/contents
```
**Critical flags**:
- `-isohybrid-gpt-basdat`: Without this, UEFI firmware won't see the EFI partition
- `-partition_offset 16`: Reserves 16 sectors for GPT. Without it, some UEFI firmware ignores the USB entirely
- `-isohybrid-mbr`: Without this, the ISO won't boot from USB at all (only CD-ROM)
### Balena Etcher
Writes the ISO byte-for-byte to USB — no reformatting, no special partition creation. If the ISO works with `dd`, it works with Etcher. If BIOS doesn't see the USB, the MBR code is wrong, not Etcher.
### Verifying hybrid structure
```bash
xxd -l 4 image.iso # MBR code (should be 45 52 for Debian Live)
xxd -s 510 -l 2 image.iso # Boot signature (must be 55 aa)
xxd -s 512 -l 8 image.iso # GPT signature at LBA 1 (should be "EFI PART")
file image.iso # Should say "DOS/MBR boot sector" and "bootable"
```
---
## 2. live-boot Package
### What it does
Provides initramfs hooks that mount a squashfs file as the root filesystem using overlayfs. This is how every Debian/Ubuntu live ISO works.
Boot flow: kernel → initramfs → live-boot scripts → find squashfs → mount overlayfs → pivot_root → systemd
### Package structure
- `live-boot` (~29KB): Main package, boot scripts
- `live-boot-initramfs-tools` (~6KB): Initramfs hooks that get baked into initrd.img
**Critical**: `scripts/live` is a **FILE**, not a directory. Verification must use `[ -e ]` not `[ -d ]`.
### Kernel parameters
| Parameter | Required | Effect |
|-----------|----------|--------|
| `boot=live` | YES | Activates live-boot's initramfs hooks |
| `components` | YES | Scans live/ for additional squashfs modules |
| `toram` | No | Copies squashfs to RAM (faster, allows USB removal) |
| `persistence` | No | Enables writable overlay on a partition labeled "persistence" |
| `quiet` | No | Suppresses boot messages |
| `splash` | No | Enables Plymouth splash screen |
| `console=ttyS0,115200` | No | Serial console for QEMU debugging |
### Where live-boot mounts things
- `/run/live/medium` — The boot media (USB/CDROM) mount point
- `/run/live/rootfs/filesystem.squashfs` — The mounted squashfs
- `/run/live/overlay` — The tmpfs overlay for writes
### Verifying live-boot in initramfs
```bash
TMPDIR=$(mktemp -d)
unmkinitramfs /path/to/initrd.img $TMPDIR
# Check for live-boot scripts
file $TMPDIR/scripts/live # Should be "ASCII text"
# OR (some initramfs have main/ prefix)
file $TMPDIR/main/scripts/live
```
### Common failures
1. **live-boot not in initrd**: Installed in rootfs but initramfs not regenerated after
2. **Missing kernel params**: `boot=live` not in GRUB/ISOLINUX config
3. **Broken initramfs**: Built without /proc /sys /dev mounted in chroot
4. **Wrong verification**: `[ -d scripts/live ]` fails because it's a file
---
## 3. debootstrap for Installer Environments
### Variants
- `--variant=minbase`: Absolute minimum (~150MB). Only essential + apt. Good for installer squashfs.
- Default (no variant): Full base system (~300MB). More packages, fewer missing deps.
### --include limitations
debootstrap's minbase resolver is simplified and **cannot resolve complex dependency chains**. Packages like `live-boot` that depend on `initramfs-tools` which depends on many other packages will silently fail or be skipped.
**Fix**: Install complex packages via `chroot apt-get` after debootstrap completes:
```bash
debootstrap --variant=minbase --include=basic,packages bookworm /installer http://deb.debian.org/debian
# Then:
mount --bind /proc /installer/proc
mount --bind /sys /installer/sys
mount --bind /dev /installer/dev
chroot /installer apt-get update
chroot /installer apt-get install -y live-boot live-boot-initramfs-tools
umount /installer/dev /installer/sys /installer/proc
```
### Initramfs generation inside containers
`update-initramfs` REQUIRES `/proc`, `/sys`, `/dev` to be mounted in the chroot. Without them:
- Module detection fails (can't read /proc/modules)
- Device nodes missing (can't detect hardware)
- The resulting initramfs boots but can't load kernel modules
### Container-in-container considerations
When running debootstrap inside a Podman/Docker container on a CI runner:
- `--privileged` flag needed for chroot to work
- The container runtime may kill the container after debootstrap exits if using `set -e`
- proc/sys/dev mounts inside the debootstrapped chroot work fine with `--privileged`
---
## 4. GRUB Theming
### theme.txt format
```
desktop-color: "#0a0a0a" # Fallback background color
desktop-image: "background.png" # Background image (any PNG, GRUB scales)
title-text: "" # Empty = hide title
+ boot_menu {
left = 25%
top = 40%
width = 50%
height = 30%
item_color = "#aaaaaa" # Normal menu item color
selected_item_color = "#fb923c" # Selected item color
item_height = 36
item_spacing = 8
scrollbar = false
}
+ label {
left = 25%
top = 20%
width = 50%
text = "Some Text"
color = "#f7931a"
align = "center"
}
```
**IMPORTANT**: Do NOT specify `font = "Name Size"` in theme elements unless you know the exact internal font name. If GRUB can't find the font, the ENTIRE theme fails to load and you get the ugly default.
### Font handling
```bash
# Generate .pf2 font file
grub-mkfont -s 16 -o dejavu_16.pf2 /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf
# In grub.cfg, load fonts BEFORE setting theme:
loadfont /boot/grub/font.pf2
loadfont /boot/grub/themes/archipelago/dejavu_16.pf2
set theme=/boot/grub/themes/archipelago/theme.txt
```
### Background images
- Any PNG works, GRUB scales to screen resolution
- Smaller images (1024x768) load faster
- Large images (3000x2000+) add seconds to boot and may fail on limited GRUB heap
### grub-mkimage — essential modules for ISO boot
```bash
grub-mkimage -O x86_64-efi -o BOOTX64.EFI -p /boot/grub \
part_gpt part_msdos fat iso9660 udf \ # Filesystem access
normal boot linux search search_fs_uuid search_fs_file search_label \
configfile echo cat ls test true \ # Basic commands
loopback \ # Loop device support
gfxterm gfxmenu font png \ # Graphical display
all_video video video_bochs video_cirrus \ # Video drivers
efi_gop efi_uga # EFI display protocols
```
Missing `all_video`/`efi_gop` = black screen on real hardware (works in QEMU).
### EFI boot image creation
```bash
dd if=/dev/zero of=efi.img bs=1M count=4
mkfs.vfat efi.img
mmd -i efi.img ::/EFI ::/EFI/BOOT
mcopy -i efi.img BOOTX64.EFI ::/EFI/BOOT/BOOTX64.EFI
```
---
## 5. Plymouth Boot Splash
### Theme types
- **script**: Most flexible. Lua-like scripting with sprites, animations, callbacks.
- **two-step**: Simple logo + spinner. Less customizable but easier.
- **fade-in**: Logo fades in. Minimal.
### Script theme structure
```
/usr/share/plymouth/themes/mytheme/
mytheme.plymouth # Theme metadata
mytheme.script # Animation script
logo.png # Logo image (PNG with alpha)
```
### mytheme.plymouth
```ini
[Plymouth Theme]
Name=MyTheme
Description=Custom boot splash
ModuleName=script
[script]
ImageDir=/usr/share/plymouth/themes/mytheme
ScriptFile=/usr/share/plymouth/themes/mytheme/mytheme.script
```
### Script language key functions
```javascript
Window.SetBackgroundTopColor(r, g, b); // 0.0-1.0 floats
Window.SetBackgroundBottomColor(r, g, b);
image = Image("logo.png");
sprite = Sprite(image);
sprite.SetX(x); sprite.SetY(y); sprite.SetOpacity(0.0-1.0);
Plymouth.SetRefreshFunction(fn); // Called every frame
Plymouth.SetBootProgressFunction(fn); // fn(duration, progress)
Plymouth.SetDisplayPasswordFunction(fn); // fn(prompt, bullets)
Plymouth.SetQuitFunction(fn);
screen_w = Window.GetWidth();
screen_h = Window.GetHeight();
```
### Setting default theme
```bash
plymouth-set-default-theme mytheme
# OR manually:
ln -sf /usr/share/plymouth/themes/mytheme/mytheme.plymouth /etc/alternatives/default.plymouth
```
### Kernel params
- `splash` in GRUB_CMDLINE_LINUX_DEFAULT enables Plymouth
- `quiet` suppresses text that would overlay Plymouth
---
## 6. ISOLINUX/SYSLINUX
### Required files
| File | Source | Purpose |
|------|--------|---------|
| `isolinux.bin` | `/usr/lib/ISOLINUX/isolinux.bin` | BIOS bootloader |
| `ldlinux.c32` | `/usr/lib/syslinux/modules/bios/ldlinux.c32` | Core library (REQUIRED) |
| `menu.c32` | `/usr/lib/syslinux/modules/bios/menu.c32` | Text menu UI |
| `libutil.c32` | `/usr/lib/syslinux/modules/bios/libutil.c32` | Utility library |
| `boot.cat` | Auto-generated by xorriso | El Torito boot catalog |
| `isohdpfx.bin` | Extracted from working ISO | Hybrid MBR code |
### Configuration (isolinux.cfg)
```
UI menu.c32
PROMPT 0
TIMEOUT 50 # 5 seconds (units of 1/10 second)
DEFAULT install
MENU TITLE MY INSTALLER
MENU COLOR border 30;44 #40ffffff #00000000 std
MENU COLOR title 1;36;44 #ff00b7ff #00000000 std
MENU COLOR sel 7;37;40 #ffffffff #ff333333 std
MENU COLOR unsel 37;44 #ffaaaaaa #00000000 std
LABEL install
MENU LABEL Install System
KERNEL /live/vmlinuz
APPEND initrd=/live/initrd.img boot=live components quiet
MENU DEFAULT
```
### menu.c32 vs vesamenu.c32
- `menu.c32`: Text-mode menu. More compatible, no background image.
- `vesamenu.c32`: VESA graphical menu. Supports background PNG, but some hardware/VMs don't support VESA.
---
## 7. Testing Without Real Hardware
### QEMU UEFI boot
```bash
qemu-system-x86_64 \
-machine q35 \
-drive if=pflash,format=raw,readonly=on,file=/path/to/OVMF_CODE.fd \
-m 4G -smp 2 \
-boot d -cdrom image.iso \
-drive if=virtio,format=qcow2,file=test-disk.qcow2 \
-vga virtio -display default
```
### QEMU BIOS boot (sees ISOLINUX)
```bash
qemu-system-x86_64 \
-machine pc \
-m 4G -smp 2 \
-boot d -cdrom image.iso \
-drive if=virtio,format=qcow2,file=test-disk.qcow2 \
-vga virtio -display default
```
### Serial console capture
Add to QEMU: `-serial file:/tmp/serial.log`
Add to kernel params: `console=ttyS0,115200 console=tty0`
### ISO structure verification (no boot required)
```bash
MNT=$(mktemp -d)
sudo mount -o loop,ro image.iso $MNT
# Check all critical files
for f in live/vmlinuz live/initrd.img live/filesystem.squashfs \
isolinux/isolinux.bin EFI/BOOT/BOOTX64.EFI boot/grub/grub.cfg; do
[ -e $MNT/$f ] && echo "OK: $f" || echo "MISSING: $f"
done
# Check initramfs for live-boot
INITRD=$(mktemp -d)
unmkinitramfs $MNT/live/initrd.img $INITRD
[ -e $INITRD/scripts/live ] && echo "live-boot: OK" || echo "live-boot: MISSING"
# Check kernel params
grep "boot=live" $MNT/boot/grub/grub.cfg && echo "params: OK"
sudo umount $MNT
```
---
## 8. Security Considerations for Custom ISOs
### Supply chain
- Pin the Debian mirror URL (don't use redirectors in production)
- Verify package signatures (debootstrap does this by default)
- Pin kernel and GRUB package versions for reproducibility
### Installer security
- Auto-install.sh runs as root — validate all inputs before path construction
- LUKS key generation must use CSPRNG (`/dev/urandom`, never `/dev/random` which blocks)
- Drop the LUKS key file after writing to crypttab (or store in root-only location with 0400)
### Boot security
- Secure Boot requires signed GRUB EFI binary (shim-signed package)
- Without Secure Boot, the unsigned BOOTX64.EFI works but users must disable Secure Boot in BIOS
- The MBR code (isohdpfx.bin) is not signed — Secure Boot only validates EFI path