1#!/bin/sh 2# 3# boot-test.sh - Automated boot loader regression tests 4# 5# Builds a minimal bootable tree, assembles disk images for all supported boot 6# configurations, then runs each in QEMU with a timeout looking for "SUCCESS". 7# 8# All tests run as an unprivileged user. No root required, with one exception: 9# netboot-bios/netboot-efi need a real tap(4) device + dnsmasq (so DHCP can 10# carry a root-path) instead of QEMU's slirp networking, and both creating a tap 11# and giving it an address require root. That setup runs once (via sudo, 12# prompting interactively) and is left running -- later runs detect the existing 13# setup and skip sudo entirely. Assumes buildworld and buildkernel have already 14# been done for the target architecture. 15# 16# Usage: 17# cd /usr/src/stand && sh ../tools/boot/boot-test.sh [options] 18# 19# Options: 20# -a ARCH Architecture to test; repeat -a to test several 21# (default: host arch) 22# Supported: amd64, aarch64, armv7, riscv64, powerpc, powerpc64, 23# powerpc64le, loongarch64 24# (loongarch64 is disabled/excluded from -A until the LoongArch 25# port lands in-tree; usable directly via -a once it has) 26# -A Test every supported architecture 27# -b Skip build/install phase (reuse existing tree) 28# -B Skip build/install and image creation (reuse existing images) 29# -l Tell us the log directory and exit 30# -j JOBS Max parallel QEMU instances (default: unlimited) 31# -o DIR Output directory for images and logs 32# -t REGEX Only run tests matching REGEX 33# -T SECONDS QEMU timeout (default: per-arch, 60; 180 for powerpc64) 34# --netboot-teardown 35# Destroy the tap(4)/dnsmasq netboot setup (run with sudo) 36 37set -e 38 39die() { 40 echo "FATAL: $*" >&2 41 exit 1 42} 43 44# FreeBSD port package (origin) that provides a command or file; "" = base 45# system. 46pkg_for() { 47 case "$1" in 48 jq) echo textproc/jq ;; 49 expect) echo lang/expect ;; 50 qemu-system-*) echo emulators/qemu ;; 51 *ipxe*) echo sysutils/ipxe ;; 52 *syslinux*|*memdisk*) echo sysutils/syslinux ;; 53 *edk2*) echo emulators/qemu ;; # edk2-*.fd ship with qemu 54 python3) echo lang/python3 ;; 55 *) echo "" ;; # makefs/mkimg etc. = base 56 esac 57} 58 59# Hard requirement: a command that must be in PATH, else abort naming the pkg. 60need_cmd() { 61 which "$1" >/dev/null 2>&1 && return 0 62 p=$(pkg_for "$1") 63 [ -n "${p}" ] && die "$1 not found; install with: pkg install ${p##*/}" 64 die "$1 not found (expected in the base system)" 65} 66 67# Optional requirement: a file for a feature; warn + skip (return 1) if absent. 68have_file() { 69 [ -e "$1" ] && return 0 70 p=$(pkg_for "$1") 71 echo " WARNING: $1 missing${p:+; pkg install ${p##*/}} -- skipping" >&2 72 return 1 73} 74 75# -------------------------------------------------------------------------- 76# Architecture configuration 77# 78# Per-arch parameters live in boot-test.json, as an arch object merged over a 79# "defaults" object. Arrays are expanded to a space separated list. Missing 80# values default to an empty string. ARCH_CAPS is a cached copy of the 81# capabilities array for the architecture. TARGET and TARGET_ARCH are cached due 82# to heavy use. Other parameters are fetched as needed as their use is 83# infrequent. 84# -------------------------------------------------------------------------- 85 86# Test whether the current arch declares a capability, e.g. "has efi". 87# "no_release" is special: it means the arch has never had a FreeBSD 88# release, so there's no ISO to pull a minimal userland from -- see 89# install_minimal_userland_world(). 90has() { 91 case " ${ARCH_CAPS} " in 92 *" $1 "*) return 0 ;; 93 esac 94 return 1 95} 96 97# Fetch the named parameter for this $ARCH. 98param() { 99 jq -r --arg a "${ARCH}" --arg k "$1" ' 100 (.defaults + .arch[$a])[$k] as $v 101 | if ($v | type) == "array" then $v | join(" ") 102 elif $v == null then "" 103 else $v end 104 ' "${CONF}" 105} 106 107# Validate ${ARCH} and cache the parameters that are heavily used in global 108# variables. 109load_arch_config() { 110 jq -e --arg a "${ARCH}" '.arch | has($a)' "${CONF}" >/dev/null 2>&1 \ 111 || die "Unknown architecture: ${ARCH}" 112 ARCH_CAPS=$(param caps) 113 TARGET=$(param target) 114 TARGET_ARCH=$(param target_arch) 115} 116 117# Derive every per-arch value for ${1} into the ARCH_*/TARGET/OUTDIR/... globals. 118# Callers isolate architectures via subshells (build) or job backgrounding (run) 119# -- both snapshot these globals -- so they never collide across arches, and we 120# never have to pass the whole bundle around. 121setup_arch_env() { 122 ARCH=$1 123 load_arch_config 124 TIMEOUT=${TIMEOUT_OVERRIDE:-$(param timeout)} 125 MK="make TARGET=${TARGET} TARGET_ARCH=${TARGET_ARCH}" 126 ARCH_OBJDIR=$(${MK} -v .OBJDIR) 127 [ -n "${ARCH_OBJDIR}" ] || die "Cannot determine OBJDIR for ${ARCH}" 128 OUTDIR=${OUTDIR_OVERRIDE:-${ARCH_OBJDIR}/boot-test} 129 IMGDIR=${OUTDIR}/images 130 LOGDIR=${OUTDIR}/logs 131 DESTDIR=${OUTDIR}/tree 132 TESTLIST=${OUTDIR}/test-list.txt 133 mkdir -p ${IMGDIR} ${LOGDIR} 134} 135 136# --- Configuration --- 137 138SKIP_BUILD=false 139SKIP_IMAGES=false 140TEST_FILTER="" 141MAX_JOBS=0 142OUTDIR_OVERRIDE="" # from -o; only valid with a single arch 143TIMEOUT_OVERRIDE="" # from -T; else each arch's param timeout 144ARCHES="" # from -a (repeatable) / -A; defaults to $(uname -p) 145ALL=false 146 147# State shared by tap(4)/dnsmasq netboot networking (see netboot_network_setup / 148# netboot_helper below). Key off user's ID to allow multiple people to 149# run the script at the same time. 150NETBOOT_STATE_DIR=${TMPDIR:-/tmp}/boot-test-net.${SUDO_UID:-$(id -u)} 151 152# --netboot-helper/--netboot-teardown are internal entry points used to run 153# privileged setup/teardown via sudo (see netboot_network_setup); they bypass 154# normal option parsing and are dispatched once every function is defined, in 155# the Main section at the bottom of this script. 156NETBOOT_MODE="" 157case "$1" in 158 --netboot-helper) NETBOOT_MODE=helper; NETBOOT_HELPER_PLAN=$2 ;; 159 --netboot-teardown) NETBOOT_MODE=teardown ;; 160esac 161 162do_report_dirs=false 163if [ -z "${NETBOOT_MODE}" ]; then 164 while getopts "a:AbBlj:o:t:T:" opt; do 165 case "$opt" in 166 a) ARCHES="${ARCHES} $OPTARG" ;; 167 A) ALL=true ;; 168 b) SKIP_BUILD=true ;; 169 B) SKIP_BUILD=true; SKIP_IMAGES=true ;; 170 j) MAX_JOBS="$OPTARG" ;; 171 l) do_report_dirs=true ;; 172 o) OUTDIR_OVERRIDE="$OPTARG" ;; 173 t) TEST_FILTER="$OPTARG" ;; 174 T) TIMEOUT_OVERRIDE="$OPTARG" ;; 175 ?) echo "Usage: $0 [-a arch]... | -A] [-b] [-B] [-t regex] [-T secs] [-j jobs] [-o dir]" >&2 176 exit 1 ;; 177 esac 178 done 179 180 # Resolve the config file next to this script before we cd elsewhere. 181 CONF="$(cd "$(dirname "$0")" && pwd)/boot-test.json" 182 [ -f "${CONF}" ] || die "Config file not found: ${CONF}" 183 184 SRCTOP=$(make -v SRCTOP) || die "Run from stand/ directory in a FreeBSD source tree" 185 cd ${SRCTOP}/stand 186 187 # Build the architecture list from json. Partially supported architectures 188 # are omitted from -A, but accessible with a direct -a. 189 if ${ALL}; then 190 for a in $(jq -r '.arch | keys_unsorted[]' "${CONF}"); do 191 if [ $(jq ".arch.${a}.disabled" "${CONF}") != "true" ]; then 192 ARCHES="$ARCHES $a" 193 fi 194 done 195 fi 196 [ -n "${ARCHES}" ] || ARCHES=$(uname -p) 197 198 # -o names one output directory, so it only makes sense for a single arch. 199 if [ -n "${OUTDIR_OVERRIDE}" ] && [ $(echo ${ARCHES} | wc -w) -gt 1 ]; then 200 die "-o cannot be combined with multiple architectures" 201 fi 202fi 203 204# The smallest FAT32 filesystem is 33292 KB 205espsize=33292 206 207# GPT attribute bit that marks the partition to boot, from 208# sys/sys/disk/gpt.h (GPT_ENT_ATTR_BOOTME). Used for the A/B test. 209GPT_ATTR_BOOTME=59 210 211# Linux kernel version for linuxboot tests 212LINUX_VERSION=6.18.2 213 214# -------------------------------------------------------------------------- 215# QEMU command builders 216# 217# qemu_base wraps the constant bits -- binary, memory, machine, per-arch extra 218# flags, and the -nographic/serial tail -- around the device arguments each 219# specific builder passes in (disks, CDs, firmware, bios). 220# -------------------------------------------------------------------------- 221 222qemu_base() { 223 echo "$(param qemu_bin) -m 1g $(param qemu_machine) $(param qemu_extra) $* -nographic -monitor none -serial stdio" 224} 225 226# -drive for the EFI firmware pflash; empty when the arch has no separate 227# firmware (e.g. riscv64's u-boot payload). 228qemu_efi_firmware() { 229 [ -z "$(param efi_firmware)" ] && return 0 230 echo "-drive file=$(param efi_firmware),format=raw,if=pflash,readonly=on" 231} 232 233# Custom OpenBIOS cached beside the ISOs, if present (QEMU's bundled one is 234# missing fixes we need for now); empty otherwise. 235qemu_ofw_bios() { 236 [ -f "${ISODIR}/openbios-ppc" ] || return 0 237 echo "-bios ${ISODIR}/openbios-ppc" 238} 239 240qemu_bios() { qemu_base "-drive file=$1,format=raw"; } 241qemu_efi() { qemu_base "$(qemu_efi_firmware) -drive file=$1,format=raw"; } 242qemu_bios_cdrom() { qemu_base "-cdrom $1"; } 243qemu_efi_cdrom() { qemu_base "$(qemu_efi_firmware) -cdrom $1"; } 244 245# OFW disk on the default (macio IDE) bus: OpenBIOS aliases it "hd" and 246# auto-probes hd:,\\:tbxi for the Apple_Bootstrap (boot1.hfs) partition. 247# virtio disks are not reachable as "hd", so OF finds nothing and drops to "0 >". 248qemu_ofw() { qemu_base "$(qemu_ofw_bios) -drive file=$1,format=raw"; } 249# -boot d boots the (macio IDE) CD-ROM, which OpenBIOS probes cd:,\\:tbxi. 250qemu_ofw_cdrom() { qemu_base "$(qemu_ofw_bios) -boot d -cdrom $1"; } 251 252# pseries PReP disk boot: virtio-blk (vtbd0), matching freebsd-ci; SLOF finds 253# the PReP boot partition on it and runs boot1.elf. 254qemu_prep() { qemu_base "-drive if=none,file=$1,format=raw,id=hd0 -device virtio-blk,drive=hd0"; } 255# pseries CD boot: SLOF boots the El Torito CHRP image; -boot d selects the CD. 256qemu_prep_cdrom() { qemu_base "-cdrom $1 -boot d"; } 257 258# Direct linuxboot: hand the Linux kernel and initrd to QEMU on the command 259# line (-kernel/-initrd) rather than off an ESP. Used by platforms with no 260# EFI/ESP (e.g. powerpc64le/pseries); the FreeBSD root disk is attached the 261# same way as every other test. 262qemu_linuxboot() { 263 extra="-kernel $2 -initrd $3" 264 [ -n "$(param linux_console)" ] && extra="${extra} -append $(param linux_console)" 265 qemu_base "${extra} -drive file=$1,format=raw" 266} 267 268# Netboot over $1 -- vmnet(4): dnsmasq owns DHCP/TFTP/root-path, which we use so 269# we use tftp, not NFS, for all the files. QEMU's netdev type is still "tap" (it 270# treats vmnet(4) and tap(4) identically). $2 = bios|efi (efi adds the pflash 271# firmware; bios relies on the NIC's PXE option ROM). 272qemu_netboot() { 273 netif=$1 274 fw="" 275 [ "$2" = efi ] && fw="$(qemu_efi_firmware)" 276 qemu_base "${fw} -netdev tap,id=net0,ifname=${netif},script=no,downscript=no -device $(param netboot_nic),netdev=net0 -boot n" 277} 278 279# RAM-disk netboot (x86 EFI). Boots iPXE from -hda (edk2's own PXE is disabled 280# via fw_cfg); iPXE DHCPs, fetches the bootfile (an iPXE script) over TFTP, and 281# chains loader.efi with memdisk=<url>. The loader downloads that image and 282# boots it entirely from RAM -- no NFS and no DHCP root-path needed. Mirrors 283# ~/memdisk/do-memddisk-efi. ${OUTDIR}/netboot-vars.fd is a writable edk2 vars 284# copy made by assemble_netboot. 285# 286# We'll need to to http boots in the future, and that will likely require 287# we don't use the ipxe USB path we use here. We use that because Tianocore 288# expects http/https booting when the obvious '-boot n' sort of things 289# are used. 290# 291# -device virtio-rng-pci: EDK II's network stack has required an RNG 292# device since CVE-2023-45237. 293qemu_netboot_ramdisk() { 294 netif=$1 295 echo "$(param qemu_bin) -M q35 -cpu max -m 2g \ 296 -drive if=pflash,format=raw,readonly=on,file=$(param efi_firmware) \ 297 -drive if=pflash,format=raw,file=${OUTDIR}/netboot-vars.fd \ 298 -hda ${OUTDIR}/netboot-ipxe.img \ 299 -device virtio-net,netdev=net0 \ 300 -netdev tap,id=net0,ifname=${netif},script=no,downscript=no \ 301 -device virtio-rng-pci \ 302 -fw_cfg name=opt/org.tianocore/IPv4PXESupport,string=no \ 303 -fw_cfg name=opt/org.tianocore/IPv6PXESupport,string=no \ 304 -nographic -monitor none -serial stdio" 305} 306 307# -------------------------------------------------------------------------- 308# Phase 0: Extract minimal userland from release ISO 309# -------------------------------------------------------------------------- 310 311# Binaries needed for a minimal bootable userland. 312# Libraries are inferred from these via ldd on the host equivalents. 313USERLAND_BINS="sbin/fastboot sbin/halt sbin/init bin/sh sbin/sysctl" 314 315ISODIR=${HOME}/iso 316 317find_iso() { 318 local isotgt=${TARGET} 319 [ ${TARGET} != ${TARGET_ARCH} ] && isotgt="${isotgt}-${TARGET_ARCH}" 320 local isoname="${ISODIR}/FreeBSD-$(param freebsd_version)-RELEASE-${isotgt}-disc1.iso.xz" 321 [ -f "${isoname}" ] && echo "${isoname}" && return 322 die "No ISO found for ${ARCH}: ${isoname} not found" 323} 324 325# Paths (binaries + their libraries + rtld) to pull into DESTDIR for a minimal 326# userland. Library *names* are discovered via ldd on the HOST's own copies of 327# USERLAND_BINS: those paths (e.g. /lib/libc.so.7) are the same across 328# architectures, only the file contents differ, so this works even when the 329# files themselves are about to come from a foreign-arch source. 330userland_extract_list() { 331 local host_bins="" 332 for b in ${USERLAND_BINS}; do 333 host_bins="${host_bins} /${b}" 334 done 335 local lib_paths=$(ldd ${host_bins} 2>/dev/null \ 336 | awk 'NF == 4 { print $3 }' | sort -u) 337 338 local list="" 339 for b in ${USERLAND_BINS}; do 340 list="${list} ./${b}" 341 done 342 for l in ${lib_paths}; do 343 list="${list} .${l}" 344 done 345 list="${list} ./libexec/ld-elf.so.1" 346 347 # Some architectures (e.g., armv7) need libgcc_s.so.1 even though the host 348 # binaries don't. Always try to extract it. We'll ignore it if not there. 349 list="${list} ./lib/libgcc_s.so.1" 350 echo "${list}" 351} 352 353# Extract the minimal userland from a release ISO -- the normal path for any 354# arch that has actually shipped a FreeBSD release. 355install_minimal_userland_iso() { 356 # Split the declaration from the assignment: `local iso=$(find_iso)` would 357 # swallow find_iso's exit status (local returns 0), so its die() -- which 358 # runs in the $() subshell -- would not stop this script. 359 local iso 360 iso=$(find_iso) || exit 1 361 echo " Extracting minimal userland from release ISO ${iso}..." 362 tar -C ${DESTDIR} -xf ${iso} $(userland_extract_list) \ 363 >> ${LOGDIR}/installuserland.log 2>&1 || true 364} 365 366# Cross-build the minimal userland via installworld instead of an ISO -- for 367# architectures that have never had a FreeBSD release (no_release cap), so no 368# ISO exists or ever will for the current version. Assumes buildworld has 369# already been done for TARGET/TARGET_ARCH, same precondition build_tree 370# already has for buildkernel. 371install_minimal_userland_world() { 372 echo " Cross-installing minimal userland via installworld (no release for ${ARCH})..." 373 local worlddir=${OUTDIR}/world 374 rm -rf ${worlddir} 375 mkdir -p ${worlddir} 376 (cd ${SRCTOP} && ${MK} installworld \ 377 DESTDIR=${worlddir} \ 378 MK_MAN=no \ 379 MK_TESTS=no \ 380 MK_DEBUG_FILES=no \ 381 MK_INSTALL_AS_USER=yes) > ${LOGDIR}/installworld.log 2>&1 \ 382 || die "installworld failed (see ${LOGDIR}/installworld.log)" 383 384 local item src 385 for item in $(userland_extract_list); do 386 src=${worlddir}/${item#.} 387 [ -f "${src}" ] || continue 388 mkdir -p ${DESTDIR}/$(dirname ${item#.}) 389 cp -p ${src} ${DESTDIR}/${item#.} 390 done 391 rm -rf ${worlddir} 392} 393 394install_minimal_userland() { 395 if has no_release; then 396 install_minimal_userland_world 397 else 398 install_minimal_userland_iso 399 fi 400} 401 402# -------------------------------------------------------------------------- 403# Phase 1: Build the boot tree 404# -------------------------------------------------------------------------- 405 406build_tree() { 407 echo "=== Phase 1: Building boot tree ===" 408 409 rm -rf ${DESTDIR} 410 mkdir -p ${DESTDIR}/boot/defaults 411 mkdir -p ${DESTDIR}/boot/kernel 412 mkdir -p ${DESTDIR}/boot/uboot 413 mkdir -p ${DESTDIR}/sbin ${DESTDIR}/bin \ 414 ${DESTDIR}/lib ${DESTDIR}/libexec \ 415 ${DESTDIR}/etc ${DESTDIR}/dev 416 417 # Install kernel 418 # I'd prefer this to be MINIMAL, but GENERIC is needed until I work out what 419 # different devices we boot from... 420 (cd ${SRCTOP} && ${MK} installkernel \ 421 KERNCONF=$(param kernconf) \ 422 MODULES_OVERRIDE="ufs zfs acl_nfs4 crypto zlib cd9660" \ 423 DESTDIR=${DESTDIR} \ 424 MK_KERNEL_SYMBOLS=no \ 425 MK_INSTALL_AS_USER=yes) > ${LOGDIR}/installkernel.log 2>&1 \ 426 || die "Kernel install failed (see ${LOGDIR}/installkernel.log)" 427 428 # Install boot loaders 429 ${MK} buildenv \ 430 DESTDIR=${DESTDIR} \ 431 MK_MAN=no \ 432 MK_INSTALL_AS_USER=yes \ 433 MK_DEBUG_FILES=no \ 434 BUILDENV_SHELL="make all install" \ 435 >> ${LOGDIR}/installloader.log 2>&1 \ 436 || die "Boot loader install failed (see ${LOGDIR}/installloader.log)" 437 438 # Install minimal userland (works for both native and cross builds) 439 install_minimal_userland 440 441 # Remove default loader symlinks -- we add them back per-image via 442 # mtree overlays to test each loader variant individually. 443 # /boot/loader is the BIOS stage-3 (amd64 only). 444 # /boot/loader.efi is what boot1.efi chainloads (all EFI platforms). 445 # OFW/PReP are the exception: their /boot/loader is the one real loader 446 # (no lua/4th/simp variants are built), chainloaded by boot1.hfs (mac99) 447 # or boot1.elf (pseries PReP), so keep it in the tree. 448 has ofw || has prep || rm -f ${DESTDIR}/boot/loader 449 rm -f ${DESTDIR}/boot/loader.efi 450 451 # Serial console configuration. boot.config is consumed only by the 452 # BIOS boot blocks; its -h/-D/-S flags are meaningless on EFI/OFW, so 453 # only write it where BIOS booting is supported. 454 if has bios; then 455 echo -h -D -S115200 > ${DESTDIR}/boot.config 456 fi 457 # Unified loader.conf: always load ufs, zfs, and cd9660 458 cat > ${DESTDIR}/boot/loader.conf <<EOF 459boot_serial=YES 460comconsole_speed=115200 461autoboot_delay=1 462ufs_load="YES" 463zfs_load="YES" 464cd9660_load="YES" 465EOF 466 local hints=$(param hints) 467 if [ -n "${hints}" ] && [ -f "${SRCTOP}/${hints}" ]; then 468 cp "${SRCTOP}/${hints}" ${DESTDIR}/boot/device.hints 469 fi 470 471 # Test /etc/rc - prints success and halts 472 cat > ${DESTDIR}/etc/rc <<'RCEOF' 473#!/bin/sh 474 475sysctl machdep.bootmethod 476echo "RC COMMAND RUNNING -- SUCCESS!!!!!" 477halt -p 478RCEOF 479 chmod +x ${DESTDIR}/etc/rc 480 481 # Create fstab used by UFS mtree overlays 482 cat > ${OUTDIR}/fstab.ufs <<EOF 483/dev/ufs/root / ufs rw 1 1 484EOF 485 # Create fstab used by CD mtree overlays 486 cat > ${OUTDIR}/fstab.cd <<EOF 487/dev/iso9660/FBSDTEST / cd9660 ro 0 0 488EOF 489 490 echo "Boot tree built in ${DESTDIR}" 491} 492 493# -------------------------------------------------------------------------- 494# Phase 2: Create base filesystem images 495# 496# Uses mtree overlays to vary the /boot/loader and /etc/fstab 497# without copying the tree. The base tree has no /boot/loader or 498# /etc/fstab; each image adds what it needs via an mtree spec file 499# passed as a second source to makefs. 500# -------------------------------------------------------------------------- 501 502# Create a UFS image with a specific loader variant via mtree overlay 503make_one_ufs() { 504 variant=$1 505 img=${IMGDIR}/bootable-ufs-${variant}.img 506 mt=$(mktemp ${OUTDIR}/ufs-mtree.XXXXXX) 507 508 echo " Creating UFS image with loader_${variant}..." 509 echo "./etc/fstab type=file mode=0644 contents=${OUTDIR}/fstab.ufs" > ${mt} 510 # BIOS: /boot/loader -> loader_<variant> 511 if [ -n "$(param bios_loaders)" ]; then 512 echo "./boot/loader type=file mode=0644 contents=${DESTDIR}/boot/loader_${variant}" >> ${mt} 513 fi 514 # OFW: boot1.hfs chainloads the single /boot/loader already in the 515 # tree (no per-variant overlay needed). 516 # EFI: /boot/loader.efi -> loader_<variant>.efi (needed for boot1.efi chainload) 517 if has efi; then 518 echo "./boot/loader.efi type=file mode=0755 contents=${DESTDIR}/boot/loader_${variant}.efi" >> ${mt} 519 fi 520 makefs -t ffs -B $(param byte_order) -M 10m -o label=root -o version=2 \ 521 ${img} ${mt} ${DESTDIR} >> ${LOGDIR}/imagebuild.log 2>&1 522 rm -f ${mt} 523} 524 525# Create a ZFS image with a specific loader variant via mtree overlay 526make_one_zfs() { 527 variant=$1 528 img=${IMGDIR}/bootable-zfs-${variant}.img 529 mt=$(mktemp ${OUTDIR}/zfs-mtree.XXXXXX) 530 531 echo " Creating ZFS image with loader_${variant}..." 532 > ${mt} 533 # BIOS: /boot/loader -> loader_<variant> 534 if [ -n "$(param bios_loaders)" ]; then 535 echo "./boot/loader type=link link=loader_${variant}" >> ${mt} 536 fi 537 # OFW: boot1.hfs chainloads the single /boot/loader already in the 538 # tree (no per-variant overlay needed). 539 # EFI: /boot/loader.efi -> loader_<variant>.efi (needed for boot1.efi chainload) 540 if has efi; then 541 echo "./boot/loader.efi type=file mode=0755 contents=${DESTDIR}/boot/loader_${variant}.efi" >> ${mt} 542 fi 543 makefs -t zfs -s 100m \ 544 -o poolname=ztestroot -o bootfs=ztestroot -o rootpath=/ \ 545 ${img} ${mt} ${DESTDIR} >> ${LOGDIR}/imagebuild.log 2>&1 546 rm -f ${mt} 547} 548 549# -------------------------------------------------------------------------- 550# A/B (dual root) images 551# 552# gptboot.efi is the only FreeBSD EFI component that reads the GPT bootme 553# attribute, so it is the only way to test that the loader it chainloads 554# honours the partition that was selected for it, rather than picking a root 555# of its own. 556# 557# The failure being tested for is "boots fine, wrong root", not "fails to 558# boot": a loader that ignores the selected partition still reaches /etc/rc 559# and still looks healthy. So the two roots have to differ in what they 560# print, and only the bootme one may print the string run_one_test greps for. 561# Two identical roots (what mkimg writes if handed the same image twice) 562# would make this test permanently green. 563# -------------------------------------------------------------------------- 564 565# Create one of the two A/B root images. ${1} is the slice letter: each slice 566# gets its own UFS label, an fstab pointing at that label, and an /etc/rc that 567# names the slice it booted from. Only slice B -- the one set bootme in 568# assemble_efi_gpt_ab -- prints SUCCESS; booting A prints WRONG SLICE instead 569# and the test fails on timeout. 570make_one_ufs_ab() { 571 slice=$1 572 img=${IMGDIR}/bootable-ufs-ab-${slice}.img 573 mt=$(mktemp ${OUTDIR}/ufs-ab-mtree.XXXXXX) 574 fstab=${OUTDIR}/fstab.ufs.ab-${slice} 575 rc=${OUTDIR}/rc.ab-${slice} 576 577 echo " Creating UFS image for A/B slice ${slice}..." 578 echo "/dev/ufs/root${slice} / ufs rw 1 1" > ${fstab} 579 580 # Both roots report which one got mounted, so a failing log says what 581 # happened; only B counts as a pass. vfs.root.mountfrom is the direct 582 # readout of the loader's choice. 583 if [ "${slice}" = "B" ]; then 584 verdict='echo "RC COMMAND RUNNING -- SUCCESS!!!!!"' 585 else 586 verdict='echo "WRONG SLICE -- booted A, expected B"' 587 fi 588 cat > ${rc} <<RCEOF 589#!/bin/sh 590 591sysctl machdep.bootmethod 592kenv vfs.root.mountfrom 593${verdict} 594halt -p 595RCEOF 596 597 echo "./etc/fstab type=file mode=0644 contents=${fstab}" > ${mt} 598 echo "./etc/rc type=file mode=0755 contents=${rc}" >> ${mt} 599 echo "./boot/loader.efi type=file mode=0755 contents=${DESTDIR}/boot/loader_lua.efi" >> ${mt} 600 makefs -t ffs -B $(param byte_order) -M 10m -o label=root${slice} -o version=2 \ 601 ${img} ${mt} ${DESTDIR} >> ${LOGDIR}/imagebuild.log 2>&1 602 rm -f ${mt} 603} 604 605# Create an ESP holding gptboot.efi rather than a loader variant. gptboot.efi 606# is a boot1 flavour: it reads the GPT, picks the bootme partition, and 607# chainloads /boot/loader.efi from it. 608make_gptboot_esp() { 609 esp=${IMGDIR}/gptboot.esp 610 mt=$(mktemp ${OUTDIR}/esp-mtree.XXXXXX) 611 612 echo " Creating ESP with gptboot.efi..." 613 cat > ${mt} <<EOF 614./efi type=dir uname=root gname=wheel mode=0755 615./efi/boot type=dir uname=root gname=wheel mode=0755 616./efi/boot/$(param efi_bootname).efi type=file uname=root gname=wheel mode=0755 contents=${DESTDIR}/boot/gptboot.efi 617EOF 618 makefs -t msdos \ 619 -o fat_type=32 \ 620 -o sectors_per_cluster=1 \ 621 -o volume_label=EFISYS \ 622 -s ${espsize}k \ 623 ${esp} ${mt} >> ${LOGDIR}/imagebuild.log 2>&1 624 rm -f ${mt} 625} 626 627# Create an ESP with the given EFI loader using an mtree spec 628make_one_esp() { 629 loader_name=$1 630 esp=${IMGDIR}/${loader_name}.esp 631 mt=$(mktemp ${OUTDIR}/esp-mtree.XXXXXX) 632 633 echo " Creating ESP with ${loader_name}.efi..." 634 cat > ${mt} <<EOF 635./efi type=dir uname=root gname=wheel mode=0755 636./efi/boot type=dir uname=root gname=wheel mode=0755 637./efi/boot/$(param efi_bootname).efi type=file uname=root gname=wheel mode=0755 contents=${DESTDIR}/boot/${loader_name}.efi 638EOF 639 makefs -t msdos \ 640 -o fat_type=32 \ 641 -o sectors_per_cluster=1 \ 642 -o volume_label=EFISYS \ 643 -s ${espsize}k \ 644 ${esp} ${mt} >> ${LOGDIR}/imagebuild.log 2>&1 645 rm -f ${mt} 646} 647 648# Create a small ESP for CD hybrid boot using an mtree spec 649make_cd_esp() { 650 file=$1 651 loader=$2 652 mt=$(mktemp ${OUTDIR}/cd-esp-mtree.XXXXXX) 653 654 cat > ${mt} <<EOF 655./efi type=dir uname=root gname=wheel mode=0755 656./efi/boot type=dir uname=root gname=wheel mode=0755 657./efi/boot/$(param efi_bootname).efi type=file uname=root gname=wheel mode=0755 contents=${loader} 658EOF 659 makefs -t msdos \ 660 -o fat_type=12 \ 661 -o sectors_per_cluster=1 \ 662 -o volume_label=EFISYS \ 663 -s 2048k \ 664 ${file} ${mt} >> ${LOGDIR}/imagebuild.log 2>&1 665 rm -f ${mt} 666} 667 668# Find the pre-built Linux kernel EFI binary for linuxboot 669find_linux_kernel() { 670 target_arch=$(${MK} -v TARGET_ARCH) 671 linuxboot_dir=${ARCH_OBJDIR}/../../linuxboot/data/output 672 673 # amd64 has .efi suffix, others don't 674 for f in \ 675 ${linuxboot_dir}/${target_arch}.linux.v${LINUX_VERSION}.efi \ 676 ${linuxboot_dir}/${target_arch}.linux.v${LINUX_VERSION} \ 677 ${linuxboot_dir}/${target_arch}.v${LINUX_VERSION}.efi \ 678 ${linuxboot_dir}/${target_arch}.v${LINUX_VERSION}; do 679 if [ -f "$f" ]; then 680 echo "$f" 681 return 682 fi 683 done 684 return 1 685} 686 687# Build a linuxboot initrd using tar --format newc with an mtree spec. 688# No root/sudo required -- device nodes are written directly into the 689# cpio archive via mtree type=char entries. 690make_linuxboot_initrd() { 691 initrd=${IMGDIR}/linuxboot-initrd.cpio.gz 692 mt=$(mktemp ${OUTDIR}/initrd-mtree.XXXXXX) 693 694 echo " Creating linuxboot initrd..." 695 696 # Build mtree spec for the initrd contents 697 cat > ${mt} <<EOF 698./init type=file mode=0755 contents=${DESTDIR}/boot/loader.kboot 699./dev type=dir mode=0755 700./dev/console type=char mode=0600 device=freebsd,5,0 701./dev/tty type=char mode=0600 device=freebsd,5,1 702./dev/ttyS0 type=char mode=0600 device=freebsd,4,64 703./boot type=dir mode=0755 704./boot/defaults type=dir mode=0755 705./boot/defaults/loader.conf type=file mode=0644 contents=${DESTDIR}/boot/defaults/loader.conf 706./boot/lua type=dir mode=0755 707EOF 708 709 # Add all lua files 710 for f in ${DESTDIR}/boot/lua/*.lua; do 711 [ -f "$f" ] || continue 712 bn=$(basename $f) 713 echo "./boot/lua/${bn} type=file mode=0644 contents=$f" >> ${mt} 714 done 715 716 # Add loader.help.kboot if present 717 if [ -f "${DESTDIR}/boot/loader.help.kboot" ]; then 718 echo "./boot/loader.help.kboot type=file mode=0644 contents=${DESTDIR}/boot/loader.help.kboot" >> ${mt} 719 fi 720 721 # Create the kboot-specific loader.conf 722 kboot_conf=$(mktemp ${OUTDIR}/kboot-loader-conf.XXXXXX) 723 cat > ${kboot_conf} <<EOF 724# Kboot configuration -- FreeBSD ${ARCH} 725boot_serial="YES" 726EOF 727 if [ "${ARCH}" = "amd64" ]; then 728 cat >> ${kboot_conf} <<EOF 729hw.uart.console="io:1016,br:115200" 730EOF 731 fi 732 echo "./boot/loader.conf type=file mode=0644 contents=${kboot_conf}" >> ${mt} 733 734 # Create the initrd as a gzip-compressed newc cpio archive 735 tar --format newc -cf - @${mt} 2>> ${LOGDIR}/imagebuild.log | \ 736 gzip > ${initrd} 737 738 rm -f ${mt} ${kboot_conf} 739} 740 741# Build a linuxboot ESP containing the Linux kernel, initrd, and startup.nsh 742make_linuxboot_esp() { 743 linux_kernel=$1 744 esp=${IMGDIR}/linuxboot.esp 745 mt=$(mktemp ${OUTDIR}/linuxboot-esp-mtree.XXXXXX) 746 747 echo " Creating linuxboot ESP..." 748 749 # Generate startup.nsh 750 startup=${OUTDIR}/startup.nsh 751 cat > ${startup} <<EOF 752\\linux.efi $(param linux_console) initrd=\\initrd 753EOF 754 755 cat > ${mt} <<EOF 756./startup.nsh type=file mode=0644 contents=${startup} 757./linux.efi type=file mode=0755 contents=${linux_kernel} 758./initrd type=file mode=0644 contents=${IMGDIR}/linuxboot-initrd.cpio.gz 759EOF 760 makefs -t msdos \ 761 -o fat_type=32 \ 762 -o sectors_per_cluster=1 \ 763 -o volume_label=EFISYS \ 764 -s 100m \ 765 ${esp} ${mt} >> ${LOGDIR}/imagebuild.log 2>&1 766 rm -f ${mt} 767} 768 769make_base_images() { 770 echo "=== Phase 2: Creating base filesystem images ===" 771 772 # UFS images - one per BIOS loader variant, plus one for EFI (uses lua) 773 if [ -n "$(param bios_loaders)" ]; then 774 for v in $(param bios_loaders); do 775 make_one_ufs $v 776 done 777 else 778 # EFI-only arches still need one UFS image 779 make_one_ufs lua 780 fi 781 782 # ZFS images (if supported) 783 if has zfs; then 784 if [ -n "$(param bios_loaders)" ]; then 785 for v in $(param bios_loaders); do 786 make_one_zfs $v 787 done 788 else 789 make_one_zfs lua 790 fi 791 fi 792 793 # ESP images (if EFI is supported) 794 if has efi; then 795 for l in $(param efi_loaders); do 796 make_one_esp $l 797 done 798 # A/B: gptboot.efi on the ESP, plus two tellable-apart roots. 799 if [ -f "${DESTDIR}/boot/gptboot.efi" ]; then 800 make_gptboot_esp 801 make_one_ufs_ab A 802 make_one_ufs_ab B 803 fi 804 fi 805 806 # Linuxboot initrd + ESP (if supported and Linux kernel is available) 807 if has linuxboot; then 808 linux_kernel=$(find_linux_kernel) || true 809 if [ -n "${linux_kernel}" ]; then 810 make_linuxboot_initrd 811 # EFI arches chainload the kernel+initrd off an ESP; platforms 812 # without EFI hand them to QEMU directly (-kernel/-initrd), so 813 # they need no ESP. 814 has efi && make_linuxboot_esp ${linux_kernel} 815 else 816 echo " WARNING: Linux kernel not found for linuxboot, skipping" 817 echo " Expected in: ${ARCH_OBJDIR}/../../linuxboot/data/output/" 818 fi 819 fi 820 821 echo "Base images created in ${IMGDIR}" 822} 823 824# -------------------------------------------------------------------------- 825# Phase 3: Assemble disk images and register tests 826# -------------------------------------------------------------------------- 827 828# Test registration - uses temp files since /bin/sh doesn't have arrays. 829# ${TESTLIST} and ${OUTDIR} are set per-arch by setup_arch_env; the list is 830# truncated in build_all before each arch's images are (re)assembled. 831register_test() { 832 name=$1 833 shift 834 echo "$*" > ${OUTDIR}/test-cmd-${name}.sh 835 echo "${name}" >> ${TESTLIST} 836} 837 838# Like register_test, but for tests that need a real tap(4) interface 839# (assigned later by netboot_network_setup, once every arch is built) instead 840# of QEMU's slirp net. The command is written out now with a placeholder tap 841# name; netboot_network_setup patches it in once the tap is assigned. 842# ${NETBOOT_PLAN} accumulates across every arch (unlike ${TESTLIST}, it is not 843# truncated per-arch), so it must already exist by the time build_all runs -- 844# see Main. $5 optionally names a different command builder than the default 845# qemu_netboot (e.g. qemu_netboot_ramdisk) -- it's always called as 846# "builder __NETBOOT_TAP__ ${fw}", so a non-default builder that doesn't need 847# ${fw} just ignores its second arg. 848register_netboot_test() { 849 name=$1 850 tftpdir=$2 851 bootfile=$3 852 fw=$4 853 builder=${5:-qemu_netboot} 854 cmdfile=${OUTDIR}/test-cmd-${name}.sh 855 echo "$(${builder} __NETBOOT_TAP__ ${fw})" > ${cmdfile} 856 echo "${name}" >> ${TESTLIST} 857 echo "${cmdfile} ${tftpdir} ${bootfile}" >> ${NETBOOT_PLAN} 858} 859 860assemble_efi_gpt() { 861 echo " Assembling EFI+GPT images..." 862 for loader in $(param efi_loaders); do 863 esp=${IMGDIR}/${loader}.esp 864 fstypes="ufs" 865 has zfs && fstypes="ufs zfs" 866 for fs in ${fstypes}; do 867 name="efi-gpt-${fs}-${loader}" 868 img=${IMGDIR}/${name}.img 869 870 # For EFI, the stage-3 in the filesystem doesn't matter, use lua variant 871 case ${fs} in 872 ufs) fsimg=${IMGDIR}/bootable-ufs-lua.img; ptype="freebsd-ufs" ;; 873 zfs) fsimg=${IMGDIR}/bootable-zfs-lua.img; ptype="freebsd-zfs" ;; 874 esac 875 876 mkimg -s gpt \ 877 -p efi:=${esp} \ 878 -p ${ptype}:=${fsimg} \ 879 -o ${img} >> ${LOGDIR}/imagebuild.log 2>&1 880 881 register_test ${name} $(qemu_efi ${img}) 882 done 883 done 884} 885 886# Build the A/B disk and register its test. Layout mirrors an A/B upgrade 887# scheme: one ESP holding gptboot.efi, then two interchangeable root slices. 888# 889# bootme is set on p3 (slice B, the second root) on purpose. With no bootme 890# anywhere gptboot.efi falls back to the first UFS partition, which is slice 891# A, so marking p2 instead would pass even if the attribute were ignored 892# entirely. Marking the second root means only a loader that honours the 893# selected partition can reach slice B and print SUCCESS. 894assemble_efi_gpt_ab() { 895 echo " Assembling EFI+GPT A/B image..." 896 name="efi-gpt-ufs-ab" 897 img=${IMGDIR}/${name}.img 898 899 mkimg -s gpt \ 900 -p efi:=${IMGDIR}/gptboot.esp \ 901 -p freebsd-ufs:=${IMGDIR}/bootable-ufs-ab-A.img \ 902 -p freebsd-ufs:=${IMGDIR}/bootable-ufs-ab-B.img \ 903 -o ${img} >> ${LOGDIR}/imagebuild.log 2>&1 904 905 ${GPTATTR} ${img} 3 ${GPT_ATTR_BOOTME} >> ${LOGDIR}/imagebuild.log 2>&1 906 907 register_test ${name} $(qemu_efi ${img}) 908} 909 910assemble_efi_mbr() { 911 echo " Assembling EFI+MBR images..." 912 for loader in $(param efi_loaders); do 913 esp=${IMGDIR}/${loader}.esp 914 name="efi-mbr-ufs-${loader}" 915 img=${IMGDIR}/${name}.img 916 ufs=${IMGDIR}/bootable-ufs-lua.img 917 918 mkimg -s bsd -p freebsd-ufs:=${ufs} -o ${img}.s2 >> ${LOGDIR}/imagebuild.log 2>&1 919 mkimg -a 1 -s mbr -p efi:=${esp} -p freebsd:=${img}.s2 -o ${img} >> ${LOGDIR}/imagebuild.log 2>&1 920 rm -f ${img}.s2 921 922 register_test ${name} $(qemu_efi ${img}) 923 done 924} 925 926assemble_bios_gpt() { 927 echo " Assembling BIOS+GPT images..." 928 for variant in $(param bios_loaders); do 929 # UFS 930 name="bios-gpt-ufs-loader_${variant}" 931 img=${IMGDIR}/${name}.img 932 ufs=${IMGDIR}/bootable-ufs-${variant}.img 933 934 mkimg -s gpt -b ${DESTDIR}/boot/pmbr \ 935 -p freebsd-boot:=${DESTDIR}/boot/gptboot \ 936 -p freebsd-ufs:=${ufs} \ 937 -o ${img} >> ${LOGDIR}/imagebuild.log 2>&1 938 939 register_test ${name} $(qemu_bios ${img}) 940 941 # ZFS 942 if has zfs; then 943 name="bios-gpt-zfs-loader_${variant}" 944 img=${IMGDIR}/${name}.img 945 zfs=${IMGDIR}/bootable-zfs-${variant}.img 946 947 mkimg -s gpt -b ${DESTDIR}/boot/pmbr \ 948 -p freebsd-boot:=${DESTDIR}/boot/gptzfsboot \ 949 -p freebsd-zfs:=${zfs} \ 950 -o ${img} >> ${LOGDIR}/imagebuild.log 2>&1 951 952 register_test ${name} $(qemu_bios ${img}) 953 fi 954 done 955} 956 957assemble_bios_mbr() { 958 echo " Assembling BIOS+MBR images..." 959 for variant in $(param bios_loaders); do 960 name="bios-mbr-ufs-loader_${variant}" 961 img=${IMGDIR}/${name}.img 962 ufs=${IMGDIR}/bootable-ufs-${variant}.img 963 964 mkimg -s bsd -b ${DESTDIR}/boot/boot \ 965 -p freebsd-ufs:=${ufs} -o ${img}.s1 >> ${LOGDIR}/imagebuild.log 2>&1 966 # Note: boot0sio has a longish timeout, and does work but 967 # takes longer than 30s so we use mbr. 968 mkimg -a 1 -s mbr -b ${DESTDIR}/boot/mbr \ 969 -p freebsd:=${img}.s1 -o ${img} >> ${LOGDIR}/imagebuild.log 2>&1 970 rm -f ${img}.s1 971 972 register_test ${name} $(qemu_bios ${img}) 973 done 974} 975 976# Hybrid GPT images: ESP + freebsd-boot + filesystem, tested with both BIOS and EFI 977assemble_both_gpt() { 978 echo " Assembling hybrid BIOS+EFI GPT images..." 979 for l in $(param bios_loaders); do 980 loader="loader_${l}" 981 esp=${IMGDIR}/${loader}.esp 982 fstypes="ufs" 983 has zfs && fstypes="ufs zfs" 984 for fs in ${fstypes}; do 985 name_base="both-gpt-${fs}-${loader}" 986 img=${IMGDIR}/${name_base}.img 987 988 case ${fs} in 989 ufs) 990 fsimg=${IMGDIR}/bootable-ufs-lua.img 991 ptype="freebsd-ufs" 992 bootblk=${DESTDIR}/boot/gptboot 993 ;; 994 zfs) 995 fsimg=${IMGDIR}/bootable-zfs-lua.img 996 ptype="freebsd-zfs" 997 bootblk=${DESTDIR}/boot/gptzfsboot 998 ;; 999 esac 1000 1001 mkimg -b ${DESTDIR}/boot/pmbr -s gpt \ 1002 -p efi:=${esp} \ 1003 -p freebsd-boot:=${bootblk} \ 1004 -p ${ptype}:=${fsimg} \ 1005 -o ${img} >> ${LOGDIR}/imagebuild.log 2>&1 1006 1007 register_test "${name_base}-bios" $(qemu_bios ${img}) 1008 register_test "${name_base}-efi" $(qemu_efi ${img}) 1009 done 1010 done 1011} 1012 1013# Hybrid MBR images: ESP + freebsd(boot+ufs), tested with both BIOS and EFI 1014assemble_both_mbr() { 1015 echo " Assembling hybrid BIOS+EFI MBR images..." 1016 for l in $(param bios_loaders); do 1017 loader="loader_${l}" 1018 esp=${IMGDIR}/${loader}.esp 1019 name_base="both-mbr-ufs-${loader}" 1020 img=${IMGDIR}/${name_base}.img 1021 ufs=${IMGDIR}/bootable-ufs-lua.img 1022 1023 mkimg -s bsd -b ${DESTDIR}/boot/boot \ 1024 -p freebsd-ufs:=${ufs} -o ${img}.s2 >> ${LOGDIR}/imagebuild.log 2>&1 1025 mkimg -a 2 -s mbr -b ${DESTDIR}/boot/mbr \ 1026 -p efi:=${esp} \ 1027 -p freebsd:=${img}.s2 -o ${img} >> ${LOGDIR}/imagebuild.log 2>&1 1028 rm -f ${img}.s2 1029 1030 register_test "${name_base}-bios" $(qemu_bios ${img}) 1031 register_test "${name_base}-efi" $(qemu_efi ${img}) 1032 done 1033} 1034 1035assemble_cd() { 1036 echo " Assembling CD images..." 1037 1038 # mtree to add loader and fstab to image 1039 mt1=$(mktemp ${OUTDIR}/cd-mtree.XXXXXX) 1040 variant=lua 1041 cat > ${mt1} <<EOF 1042./boot/loader type=file mode=0644 contents=${DESTDIR}/boot/loader_${variant} 1043./etc/fstab type=file mode=0644 contents=${OUTDIR}/fstab.cd 1044EOF 1045 1046 # cdboot - BIOS CD boot via El Torito 1047 name="bios-cd-cdboot" 1048 img=${IMGDIR}/${name}.iso 1049 makefs -t cd9660 \ 1050 -o bootimage=i386\;${DESTDIR}/boot/cdboot \ 1051 -o no-emul-boot \ 1052 -o rockridge \ 1053 -o label=FBSDTEST \ 1054 ${img} ${mt1} ${DESTDIR} >> ${LOGDIR}/imagebuild.log 2>&1 1055 register_test ${name} $(qemu_bios_cdrom ${img}) 1056 1057 # isoboot - hybrid BIOS+EFI CD (tests isoboot for BIOS, loader.efi for EFI) 1058 name="hybrid-cd-isoboot" 1059 img=${IMGDIR}/${name}.iso 1060 espfile=$(mktemp ${OUTDIR}/efiboot.XXXXXX) 1061 make_cd_esp ${espfile} ${DESTDIR}/boot/loader_lua.efi 1062 1063 makefs -t cd9660 \ 1064 -o bootimage=i386\;${DESTDIR}/boot/cdboot \ 1065 -o no-emul-boot \ 1066 -o bootimage=i386\;${espfile} \ 1067 -o no-emul-boot \ 1068 -o platformid=efi \ 1069 -o rockridge \ 1070 -o label=FBSDTEST \ 1071 ${img} ${mt1} ${DESTDIR} >> ${LOGDIR}/imagebuild.log 2>&1 1072 1073 # Overlay hybrid GPT for isoboot 1074 imgsize=$(stat -f %z "${img}") 1075 1076 # Find the EFI partition in the ISO to reference it 1077 espstart="" 1078 espsize_cd="" 1079 for entry in $(etdump --format shell ${img}); do 1080 eval ${entry} 1081 if [ "${et_platform}" = "efi" ]; then 1082 espstart=$(expr ${et_lba} \* 2048) 1083 espsize_cd=$(expr ${et_sectors} \* 512) 1084 break 1085 fi 1086 done 1087 1088 if [ -n "${espstart}" ]; then 1089 hybrid=$(mktemp ${OUTDIR}/hybrid.XXXXXX) 1090 mkimg -s gpt \ 1091 --capacity ${imgsize} \ 1092 -b ${DESTDIR}/boot/pmbr \ 1093 -p freebsd-boot:=${DESTDIR}/boot/isoboot \ 1094 -p efi::${espsize_cd}:${espstart} \ 1095 -o ${hybrid} >> ${LOGDIR}/imagebuild.log 2>&1 1096 dd if=${hybrid} of=${img} bs=32k count=1 conv=notrunc >> ${LOGDIR}/imagebuild.log 2>&1 1097 rm -f ${hybrid} 1098 fi 1099 1100 rm -f ${espfile} 1101 1102 # Test the hybrid ISO with both BIOS and EFI 1103 register_test "${name}-bios" $(qemu_bios_cdrom ${img}) 1104 register_test "${name}-efi" $(qemu_efi_cdrom ${img}) 1105 1106 rm -f ${mt1} 1107} 1108 1109assemble_ofw() { 1110 echo " Assembling Open Firmware images..." 1111 # APM partitioned disk with boot1.hfs 1112 fstypes="ufs" 1113 has zfs && fstypes="ufs zfs" 1114 for fs in ${fstypes}; do 1115 name="ofw-apm-${fs}" 1116 img=${IMGDIR}/${name}.img 1117 1118 case ${fs} in 1119 ufs) fsimg=${IMGDIR}/bootable-ufs-lua.img; ptype="freebsd-ufs" ;; 1120 zfs) fsimg=${IMGDIR}/bootable-zfs-lua.img; ptype="freebsd-zfs" ;; 1121 esac 1122 1123 mkimg -a 1 -s apm \ 1124 -p freebsd-boot:=${DESTDIR}/boot/boot1.hfs \ 1125 -p ${ptype}:=${fsimg} \ 1126 -o ${img} >> ${LOGDIR}/imagebuild.log 2>&1 1127 1128 register_test ${name} $(qemu_ofw ${img}) 1129 done 1130} 1131 1132# Open Firmware bootable CD. Mirrors release/powerpc/mkisoimages.sh: build the 1133# Apple/OF "macppc" boot image by dd'ing /boot/loader into the hfs-boot block 1134# at its "Loader START" offset, then hand that to makefs as the El Torito 1135# no-emul boot image. OpenBIOS finds it via cd:,\\:tbxi. 1136assemble_ofw_cd() { 1137 echo " Assembling Open Firmware CD image..." 1138 name="ofw-cd" 1139 img=${IMGDIR}/${name}.iso 1140 1141 # cd9660 root fstab overlay 1142 mt=$(mktemp ${OUTDIR}/ofwcd-mtree.XXXXXX) 1143 echo "./etc/fstab type=file mode=0644 contents=${OUTDIR}/fstab.cd" > ${mt} 1144 1145 # Apple/OF boot block with the loader embedded at "Loader START". 1146 bootblock=$(mktemp ${OUTDIR}/hfs-boot.XXXXXX) 1147 uudecode -p ${SRCTOP}/release/powerpc/hfs-boot.bz2.uu | bunzip2 > ${bootblock} 1148 offset=$(hd ${bootblock} | grep 'Loader START' | cut -f 1 -d ' ') 1149 offset=$((0x${offset} / 512)) 1150 dd if=${DESTDIR}/boot/loader of=${bootblock} seek=${offset} conv=notrunc \ 1151 >> ${LOGDIR}/imagebuild.log 2>&1 1152 1153 makefs -t cd9660 \ 1154 -o bootimage=macppc\;${bootblock} \ 1155 -o no-emul-boot \ 1156 -o rockridge \ 1157 -o label=FBSDTEST \ 1158 ${img} ${mt} ${DESTDIR} >> ${LOGDIR}/imagebuild.log 2>&1 1159 1160 rm -f ${bootblock} ${mt} 1161 register_test ${name} $(qemu_ofw_cdrom ${img}) 1162} 1163 1164# pseries PReP disk boot: MBR with a PReP boot partition (boot1.elf) and the 1165# UFS root directly on an MBR partition -- no BSD label, matching freebsd-ci 1166# (whose BSD-slice container does not cross-build from amd64). SLOF runs 1167# boot1.elf from the PReP partition, which loads /boot/loader from the UFS. 1168assemble_prep() { 1169 echo " Assembling pseries PReP (MBR) images..." 1170 name="prep-mbr-ufs" 1171 img=${IMGDIR}/${name}.img 1172 ufs=${IMGDIR}/bootable-ufs-lua.img 1173 1174 mkimg -a 1 -s mbr \ 1175 -p prepboot:=${DESTDIR}/boot/boot1.elf \ 1176 -p freebsd:=${ufs} \ 1177 -o ${img} >> ${LOGDIR}/imagebuild.log 2>&1 1178 1179 register_test ${name} $(qemu_prep ${img}) 1180} 1181 1182# pseries CD boot. SLOF reads \ppc\bootinfo.txt (CHRP boot script) and runs 1183# the OF loader from the CD. Mirrors the chrp-boot half of 1184# release/powerpc/mkisoimages.sh (the macppc/Apple half is mac99-only). 1185assemble_pseries_cd() { 1186 echo " Assembling pseries CD image..." 1187 name="prep-cd" 1188 img=${IMGDIR}/${name}.iso 1189 1190 mt=$(mktemp ${OUTDIR}/pseriescd-mtree.XXXXXX) 1191 bootinfo=$(mktemp ${OUTDIR}/bootinfo.XXXXXX) 1192 cat > ${bootinfo} <<EOF 1193<chrp-boot> 1194<description>FreeBSD Install</description> 1195<os-name>FreeBSD</os-name> 1196<boot-script>boot &device;:,\ppc\chrp\loader</boot-script> 1197</chrp-boot> 1198EOF 1199 cat > ${mt} <<EOF 1200./etc/fstab type=file mode=0644 contents=${OUTDIR}/fstab.cd 1201./ppc type=dir mode=0755 1202./ppc/bootinfo.txt type=file mode=0644 contents=${bootinfo} 1203./ppc/chrp type=dir mode=0755 1204./ppc/chrp/loader type=file mode=0644 contents=${DESTDIR}/boot/loader 1205EOF 1206 makefs -t cd9660 \ 1207 -o chrp-boot \ 1208 -o rockridge \ 1209 -o label=FBSDTEST \ 1210 ${img} ${mt} ${DESTDIR} >> ${LOGDIR}/imagebuild.log 2>&1 1211 rm -f ${mt} ${bootinfo} 1212 register_test ${name} $(qemu_prep_cdrom ${img}) 1213} 1214 1215assemble_linuxboot() { 1216 echo " Assembling linuxboot images..." 1217 esp=${IMGDIR}/linuxboot.esp 1218 fstypes="ufs" 1219 has zfs && fstypes="ufs zfs" 1220 for fs in ${fstypes}; do 1221 name="linuxboot-gpt-${fs}" 1222 img=${IMGDIR}/${name}.img 1223 1224 case ${fs} in 1225 ufs) fsimg=${IMGDIR}/bootable-ufs-lua.img; ptype="freebsd-ufs" ;; 1226 zfs) fsimg=${IMGDIR}/bootable-zfs-lua.img; ptype="freebsd-zfs" ;; 1227 esac 1228 1229 mkimg -s gpt \ 1230 -p efi:=${esp} \ 1231 -p ${ptype}:=${fsimg} \ 1232 -o ${img} >> ${LOGDIR}/imagebuild.log 2>&1 1233 1234 register_test ${name} $(qemu_efi ${img}) 1235 done 1236} 1237 1238# Linuxboot for platforms without an ESP: the kernel+initrd go on the QEMU 1239# command line, and the disk is just the FreeBSD root filesystem in a GPT 1240# (no ESP partition). loader.kboot mounts root from that disk. 1241assemble_linuxboot_direct() { 1242 echo " Assembling linuxboot (direct kernel/initrd) images..." 1243 linux_kernel=$(find_linux_kernel) || return 0 1244 initrd=${IMGDIR}/linuxboot-initrd.cpio.gz 1245 fstypes="ufs" 1246 has zfs && fstypes="ufs zfs" 1247 for fs in ${fstypes}; do 1248 name="linuxboot-${fs}" 1249 img=${IMGDIR}/${name}.img 1250 1251 case ${fs} in 1252 ufs) fsimg=${IMGDIR}/bootable-ufs-lua.img; ptype="freebsd-ufs" ;; 1253 zfs) fsimg=${IMGDIR}/bootable-zfs-lua.img; ptype="freebsd-zfs" ;; 1254 esac 1255 1256 mkimg -s gpt \ 1257 -p ${ptype}:=${fsimg} \ 1258 -o ${img} >> ${LOGDIR}/imagebuild.log 2>&1 1259 1260 register_test ${name} $(qemu_linuxboot ${img} ${linux_kernel} ${initrd}) 1261 done 1262} 1263 1264# Netboot: stage a TFTP root and register PXE/EFI network-boot tests. The 1265# loader fetches the kernel + an md_image RAM root over TFTP and mounts 1266# /dev/md0. The bootable UFS image is reused as the RAM root, so the kernel 1267# must have "options MD_ROOT" (amd64/aarch64 do; riscv64/armv7 GENERIC do 1268# not -- see the plan). netboot-bios/netboot-efi go over a real tap(4) + 1269# dnsmasq (register_netboot_test / netboot_network_setup) so DHCP can carry a 1270# root-path. 1271assemble_netboot() { 1272 echo " Assembling netboot images..." 1273 tftp=${OUTDIR}/tftp 1274 rm -rf ${tftp} 1275 mkdir -p ${tftp} 1276 cp -a ${DESTDIR}/boot ${tftp}/boot 1277 cp ${IMGDIR}/bootable-ufs-lua.img ${tftp}/boot/mdroot.img 1278 1279 # Serve the kernel and RAM root as .xz: xzfs_fsops (stand/libsa/xzfs.c) 1280 # transparently retries "<name>.xz" whenever "<name>" isn't found, 1281 # exactly like gzipfs already does for kernel.gz, so this cuts the bytes 1282 # that have to cross TFTP with no loader.conf change -- mdroot_name below 1283 # still names the plain, unsuffixed "mdroot.img", and pxeboot/loader.efi 1284 # (fetched directly by firmware, not through libsa) are untouched. Both 1285 # the amd64 BIOS loader (i386/loader/Makefile: LOADER_XZ_SUPPORT) and 1286 # every EFI loader (efi/loader/conf.c: unconditional) have xzfs compiled 1287 # in, so netboot-bios and netboot-efi -- which share this tftp tree -- 1288 # both exercise the compressed path. 1289 # 1290 # -6 (xz's own default), not -9: -9's 64MB LZMA2 dictionary needs one 1291 # contiguous vmalloc() of that size before xz_dec_run() can decode 1292 # anything, and the BIOS loader's *entire* heap is a fixed 64MB 1293 # (HEAP_MIN in i386/libi386/biosmem.c) regardless of VM memory -- so a 1294 # 64MB dictionary request against a 64MB heap that already holds other 1295 # loader state can never succeed (XZ_MEM_ERROR, every read, forever). 1296 # -6's 8MB dictionary leaves ample headroom and loses ~nothing on data 1297 # this size. 1298 xz -6 -f ${tftp}/boot/kernel/kernel 1299 xz -6 -f ${tftp}/boot/mdroot.img 1300 1301 # Boot the kernel + RAM root off the network instead of a disk (appended to 1302 # the arch-correct loader.conf build_tree already wrote). 1303 cat >> ${tftp}/boot/loader.conf <<EOF 1304mdroot_load="YES" 1305mdroot_type="md_image" 1306mdroot_name="/boot/mdroot.img" 1307vfs.root.mountfrom="ufs:/dev/md0" 1308EOF 1309 1310 # BIOS PXE: the NIC option ROM chainloads pxeboot (the DHCP bootfile). 1311 # Registered via register_netboot_test, not register_test: these need a 1312 # real tap(4) + dnsmasq root-path (see netboot_network_setup) rather than 1313 # slirp, which can't send one. 1314 # 1315 # netboot-bios legitimately runs past the default timeout: it's TFTP 1316 # (not xzfs decode) that's the bottleneck here -- EFI's memdisk=<url> 1317 # path decodes 150MB .xz images in about a minute, so the loader's xz 1318 # decoder isn't the bottleneck; this is plain TFTP bandwidth for the 1319 # kernel+mdroot fetch. Measured wins on real hardware: whole-suite boot 1320 # time dropped from ~25 min to ~18 min with xzfs in place. TFTP-level 1321 # fixes (e.g. HTTP instead, once BIOS can do that) are the only further 1322 # lever; don't chase this again without one. 1323 if has bios; then 1324 cp ${DESTDIR}/boot/pxeboot ${tftp}/pxeboot 1325 register_netboot_test netboot-bios ${tftp} pxeboot bios 1326 fi 1327 # EFI: stage loader.efi for the iPXE-chainload test below. This can't use 1328 # the NIC's own PXE ROM the way netboot-bios does -- our installed OVMF 1329 # build (qemu's bundled edk2-x86_64-code.fd) has no NetworkPkg/PXE driver 1330 # at all (confirmed: zero PXE/HTTPBoot strings in the firmware, and 1331 # BdsDxe drops straight to "EFI Internal Shell" with no network boot 1332 # option ever offered, independent of vars pflash/bootindex/-boot n). So 1333 # EFI netboot instead brings its own iPXE (see the ramdisk block below) 1334 # to do DHCP/TFTP; netboot-efi's own chain script (also below) omits 1335 # memdisk= so loader.efi does its own BOOTP call and gets a real 1336 # DHCP root-path, unlike the RAM-disk tests. 1337 if has efi; then 1338 cp ${DESTDIR}/boot/loader_lua.efi ${tftp}/loader.efi 1339 fi 1340 1341 # RAM-disk netboot: iPXE (from -hda) chains loader.efi with memdisk=<url>, 1342 # which the loader boots entirely from RAM -- sidestepping the NFS/root-path 1343 # problem above. iPXE EFI is x86-only in the install, so this is gated on 1344 # netboot_ipxe (amd64 today). See qemu_netboot_ramdisk / do-memddisk-efi. 1345 # netboot-efi (real root-path, no memdisk=) piggybacks on the same iPXE 1346 # image/vars, since it needs the same OVMF-has-no-PXE workaround. 1347 if [ -n "$(param netboot_ipxe)" ] && have_file "$(param netboot_ipxe)" && \ 1348 have_file "$(param netboot_efi_vars)"; then 1349 # Writable copies: QEMU opens -hda and the edk2 vars pflash read-write, 1350 # but the installed originals are root-owned. 1351 cp -f $(param netboot_ipxe) ${OUTDIR}/netboot-ipxe.img 1352 cp -f $(param netboot_efi_vars) ${OUTDIR}/netboot-vars.fd 1353 1354 if has efi; then 1355 cat > ${tftp}/boot-efi.ipxe <<EOF 1356#!ipxe 1357chain tftp://\${next-server}/loader.efi 1358EOF 1359 register_netboot_test netboot-efi ${tftp} /boot-efi.ipxe "" qemu_netboot_ramdisk 1360 1361 # Same idea as netboot-efi, but rootdev= forces currdev to an 1362 # http:// URL so the root fetch exercises the loader's own EFI 1363 # HTTP devspec (efi/libefi/efihttp.c) instead of tftp/NFS. 1364 cat > ${tftp}/boot-http.ipxe <<EOF 1365#!ipxe 1366chain tftp://\${next-server}/loader.efi rootdev=http://\${next-server}/ 1367EOF 1368 register_netboot_test netboot-http-efi ${tftp} /boot-http.ipxe "" qemu_netboot_ramdisk 1369 fi 1370 1371 cat > ${tftp}/boot.ipxe <<EOF 1372#!ipxe 1373chain tftp://\${next-server}/loader.efi memdisk=tftp://\${next-server}/boot/mdroot.img 1374EOF 1375 register_netboot_test netboot-ramdisk ${tftp} /boot.ipxe "" qemu_netboot_ramdisk 1376 1377 # Same mechanism, but the image is compressed, exercising each codec 1378 # in stand/efi/loader/decompress.c. Compress a throwaway copy rather 1379 # than ${IMGDIR}/bootable-ufs-lua.img itself: gzip/bzip2/xz remove 1380 # their input on success, and this image is shared with other tests. 1381 # Compressing a real file (not a pipe) lets zstd embed the frame 1382 # content size, so zstd_init() can size the output buffer exactly 1383 # instead of guessing 4x and growing/copying as it decompresses. 1384 for spec in gzip:gz bzip2:bz2 xz:xz zstd:zst; do 1385 tool=${spec%%:*} 1386 ext=${spec#*:} 1387 img=${tftp}/boot/mdroot-${tool}.img 1388 cp ${IMGDIR}/bootable-ufs-lua.img ${img} 1389 case ${tool} in 1390 zstd) zstd -f --rm ${img} >> ${LOGDIR}/imagebuild.log 2>&1 ;; 1391 *) ${tool} -f ${img} >> ${LOGDIR}/imagebuild.log 2>&1 ;; 1392 esac 1393 cat > ${tftp}/boot-${tool}.ipxe <<EOF 1394#!ipxe 1395chain tftp://\${next-server}/loader.efi memdisk=tftp://\${next-server}/boot/mdroot-${tool}.img.${ext} 1396EOF 1397 register_netboot_test netboot-ramdisk-${tool} ${tftp} /boot-${tool}.ipxe "" qemu_netboot_ramdisk 1398 done 1399 fi 1400 1401 # BIOS RAM-disk netboot: iPXE loads syslinux memdisk with a *bootable* disk 1402 # image as its initrd. memdisk boots the image's MBR and installs a MEMDISK 1403 # memory disk; the BIOS loader detects it (biosmemdisk.c -> hint.md.0.*), so 1404 # the kernel gets md0 and roots via the UFS label. The BIOS loader has no 1405 # memdisk= arg, hence this different mechanism. 1406 if has bios && [ -n "$(param netboot_memdisk)" ] && \ 1407 have_file "$(param netboot_memdisk)"; then 1408 cp $(param netboot_memdisk) ${tftp}/memdisk 1409 cp ${IMGDIR}/bios-mbr-ufs-loader_lua.img ${tftp}/bootdisk.img 1410 cat > ${tftp}/boot-bios.ipxe <<EOF 1411#!ipxe 1412kernel tftp://\${next-server}/memdisk 1413initrd tftp://\${next-server}/bootdisk.img 1414boot 1415EOF 1416 register_netboot_test netboot-bios-memdisk ${tftp} /boot-bios.ipxe bios 1417 fi 1418} 1419 1420# -------------------------------------------------------------------------- 1421# Netboot host networking: vmnet(4) + dnsmasq 1422# 1423# QEMU's slirp/user-mode net cannot send DHCP root-path (option 17); without 1424# one, the loader's netproto defaults to NFS for every file fetch after the 1425# initial TFTP-delivered bootfile (net_parse_rootpath()/NETPROTO_DEFAULT), which 1426# is why netboot-bios/ netboot-efi otherwise fail here (no NFS server). 1427# 1428# This uses vmnet(4), not tap(4), even though both are clones of the same 1429# if_tuntap(4) driver and QEMU treats them identically (-netdev tap,ifname=). 1430# tap(4)'s close handler unconditionally runs if_down()+if_purgeaddrs() on last 1431# close -- so the interface would lose its address and go down every time QEMU 1432# exits, and re-adding it needs root. vmnet(4) explicitly skips that, so the 1433# address assigned once survives every subsequent QEMU open/close. 1434# 1435# Creating the interface and giving it an address both require root regardless 1436# of net.link.tap.user_open (that sysctl only gates opening the cloning device 1437# itself). So the root setup below runs once, chowns the resulting /dev/vmnetN 1438# nodes to the invoking user (so QEMU, unprivileged, can open them directly -- 1439# see net/tap-bsd.c: it opens /dev/<ifname> directly when that device already 1440# exists, only falling back to the /dev/tap cloning device otherwise), and 1441# leaves everything running. Later invocations detect the existing state via a 1442# content hash and skip sudo entirely: "once per boot", not "once per run". 1443# -------------------------------------------------------------------------- 1444 1445# Deterministically slice a /30 out of $(param netboot_subnet_base) (a /16, 1446# e.g. 198.18.0.0) for plan line ${1} (0-based). Prints "gw client". 1447netboot_subnet_for() { 1448 idx=$1 1449 base=$(param netboot_subnet_base) 1450 o1o2=$(echo ${base} | cut -d. -f1-2) 1451 o3=$(($(echo ${base} | cut -d. -f3) + idx / 64)) 1452 o4=$(((idx % 64) * 4)) 1453 echo "${o1o2}.${o3}.$((o4 + 1)) ${o1o2}.${o3}.$((o4 + 2))" 1454} 1455 1456# Runs after every arch is built (so ${NETBOOT_PLAN} is complete) and before 1457# run_all_tests. Assigns each registered netboot test its own vmnet + /30, 1458# escalates via sudo only if the host doesn't already match that plan, then 1459# patches the __NETBOOT_TAP__ placeholder in each test's command file. 1460netboot_network_setup() { 1461 [ -s "${NETBOOT_PLAN}" ] || return 0 1462 echo "=== Netboot: configuring vmnet(4) + dnsmasq ===" 1463 mkdir -p "${NETBOOT_STATE_DIR}" 1464 1465 resolved=${NETBOOT_STATE_DIR}/plan.new 1466 : > "${resolved}" 1467 idx=0 1468 while read cmdfile tftpdir bootfile; do 1469 vmnet="vmnet$((100 + idx))" # offset away from any operator-managed vmnets 1470 set -- $(netboot_subnet_for ${idx}) 1471 gw=$1 1472 client=$2 1473 echo "${vmnet} ${gw} ${client} 30 ${tftpdir} ${bootfile} ${cmdfile}" >> "${resolved}" 1474 idx=$((idx + 1)) 1475 done < "${NETBOOT_PLAN}" 1476 1477 newhash=$(md5 -q "${resolved}") 1478 oldhash="" 1479 [ -f "${NETBOOT_STATE_DIR}/state.hash" ] && oldhash=$(cat "${NETBOOT_STATE_DIR}/state.hash") 1480 1481 # dnsmasq runs as "nobody" (it drops root after binding), so kill -0 1482 # from this unprivileged process would always fail with EPERM even when 1483 # it's alive; check process existence via ps instead, which doesn't 1484 # require signal permission. 1485 converged=false 1486 if [ "${newhash}" = "${oldhash}" ] && [ -s "${NETBOOT_STATE_DIR}/dnsmasq.pid" ] \ 1487 && ps -p "$(cat ${NETBOOT_STATE_DIR}/dnsmasq.pid)" > /dev/null 2>&1; then 1488 converged=true 1489 while read vmnet gw client prefix tftpdir bootfile cmdfile; do 1490 ifconfig "${vmnet}" > /dev/null 2>&1 || { converged=false; break; } 1491 done < "${resolved}" 1492 fi 1493 1494 # Each vmnet also gets its own python3 http.server (see netboot_helper), 1495 # so http:// fetches (netboot-http-efi) have something to hit at 1496 # ${next-server} alongside dnsmasq's tftp-root. Confirm the pid count 1497 # still matches the plan and every one of them is still alive. 1498 if ${converged}; then 1499 if [ -s "${NETBOOT_STATE_DIR}/httpd.pids" ] && \ 1500 [ "$(wc -l < "${NETBOOT_STATE_DIR}/httpd.pids")" -eq "$(wc -l < "${resolved}")" ]; then 1501 while read pid; do 1502 ps -p "${pid}" > /dev/null 2>&1 || { converged=false; break; } 1503 done < "${NETBOOT_STATE_DIR}/httpd.pids" 1504 else 1505 converged=false 1506 fi 1507 fi 1508 1509 if ${converged}; then 1510 echo " Existing vmnet(4)/dnsmasq setup already matches -- no sudo needed." 1511 else 1512 echo " Host networking missing or stale; requesting sudo once to (re)create it..." 1513 cp "${resolved}" "${NETBOOT_STATE_DIR}/plan" 1514 sudo "$0" --netboot-helper "${NETBOOT_STATE_DIR}/plan" \ 1515 || die "netboot vmnet(4)/dnsmasq setup (sudo) failed" 1516 fi 1517 1518 while read vmnet gw client prefix tftpdir bootfile cmdfile; do 1519 sed -i '' "s/__NETBOOT_TAP__/${vmnet}/" "${cmdfile}" 1520 done < "${resolved}" 1521} 1522 1523# Root-side setup, only reached via `sudo "$0" --netboot-helper <planfile>` 1524# (see netboot_network_setup). ${1} has the same "vmnet gw client prefix 1525# tftpdir bootfile cmdfile" lines as netboot_network_setup's resolved plan. 1526netboot_helper() { 1527 planfile=$1 1528 [ -r "${planfile}" ] || die "netboot helper: cannot read plan ${planfile}" 1529 need_cmd python3 1530 invoker=${SUDO_UID:-$(id -u)} 1531 mkdir -p "${NETBOOT_STATE_DIR}" 1532 1533 # Converge from scratch rather than trusting the caller's hash check: 1534 # tear down anything left over from a previous (possibly interrupted) run. 1535 if [ -s "${NETBOOT_STATE_DIR}/dnsmasq.pid" ]; then 1536 kill "$(cat ${NETBOOT_STATE_DIR}/dnsmasq.pid)" 2>/dev/null || true 1537 rm -f "${NETBOOT_STATE_DIR}/dnsmasq.pid" 1538 fi 1539 if [ -s "${NETBOOT_STATE_DIR}/httpd.pids" ]; then 1540 while read pid; do 1541 kill "${pid}" 2>/dev/null || true 1542 done < "${NETBOOT_STATE_DIR}/httpd.pids" 1543 fi 1544 : > "${NETBOOT_STATE_DIR}/httpd.pids" 1545 for vmnet in $(ifconfig -g boot-test 2>/dev/null); do 1546 ifconfig "${vmnet}" destroy 1547 done 1548 1549 conf=${NETBOOT_STATE_DIR}/dnsmasq.conf 1550 cat > "${conf}" <<EOF 1551port=0 1552bind-interfaces 1553enable-tftp 1554log-dhcp 1555pid-file=${NETBOOT_STATE_DIR}/dnsmasq.pid 1556dhcp-leasefile=${NETBOOT_STATE_DIR}/dnsmasq.leases 1557 1558# QEMU's PXE ROM is iPXE; it self-identifies as vendor-class 1559# "PXEClient:Arch:00000:UNDI:002001" (stand/libsa/bootp.c's own request just 1560# says "PXEClient", no ":Arch:..." suffix, so this substring match is 1561# iPXE-only). iPXE's autoboot() gives DHCP root-path priority over 1562# chainloading the DHCP filename -- if root-path is set it tries to 1563# sanboot(8) it instead, which fails outright for a tftp:// URI ("Could not 1564# open SAN device"). So root-path below is withheld from iPXE's own 1565# negotiation and only given once stand/libsa/bootp.c does its own separate 1566# BOOTP call after pxeboot/loader.efi has already been chainloaded. 1567dhcp-vendorclass=set:ipxerom,PXEClient:Arch 1568EOF 1569 1570 while read vmnet gw client prefix tftpdir bootfile cmdfile; do 1571 ifconfig "${vmnet}" create group boot-test 1572 ifconfig "${vmnet}" inet "${gw}/${prefix}" up 1573 chown "${invoker}" "/dev/${vmnet}" 1574 1575 # One http.server per subnet, bound only to that subnet's own gw 1576 # address (not 0.0.0.0), so concurrently-running tests never collide 1577 # on port 80. Serves the exact same tree as dnsmasq's tftp-root above 1578 # -- http:// and tftp:// fetches of the same path see identical bytes. 1579 # Harmless for tests that never issue an http:// fetch; only 1580 # netboot-http-efi actually relies on it today. 1581 python3 -m http.server 80 --bind "${gw}" --directory "${tftpdir}" \ 1582 > "${NETBOOT_STATE_DIR}/httpd-${vmnet}.log" 2>&1 & 1583 echo $! >> "${NETBOOT_STATE_DIR}/httpd.pids" 1584 1585 # dnsmasq's dhcp-boot only sets DHCP option 67 (bootfile-name), never 1586 # the classic fixed-length BOOTP "file" field -- confirmed by comparing 1587 # against QEMU's own slirp DHCP server, which sets that field directly 1588 # and gets a plain "Filename: pxeboot" chainload with no further 1589 # ceremony. Without it, and since iPXE requested option 43 (PXE vendor 1590 # info) in its Parameter-Request list, iPXE assumes it must run the full 1591 # PXE boot-server-discovery dance instead of trusting option 67 -- seen 1592 # on the wire as a TFTP RRQ with an empty filename, then "No 1593 # configuration methods succeeded" / "PXEBS ... Connection timed 1594 # out". Telling it not to via PXE discovery-control (option 43 1595 # sub-option 6, value 8 = "use bootfile name from the DHCP packet, don't 1596 # discover") fixes it directly, without dnsmasq's heavier --pxe-service 1597 # boot-menu machinery (which defaults discovery-control to 3, still 1598 # triggering discovery). 1599 cat >> "${conf}" <<EOF 1600interface=${vmnet} 1601dhcp-range=set:${vmnet},${client},${client},255.255.255.252,1h 1602dhcp-boot=tag:${vmnet},${bootfile} 1603dhcp-option=tag:${vmnet},tag:ipxerom,encap:43,6,8 1604dhcp-option=tag:${vmnet},tag:!ipxerom,17,"tftp://${gw}/" 1605tftp-root=${tftpdir},${vmnet} 1606EOF 1607 done < "${planfile}" 1608 1609 dnsmasq --conf-file="${conf}" 1610 1611 # dnsmasq daemonizes itself; wait for the pidfile before trusting it. 1612 tries=0 1613 while [ ! -s "${NETBOOT_STATE_DIR}/dnsmasq.pid" ] && [ ${tries} -lt 5 ]; do 1614 sleep 1 1615 tries=$((tries + 1)) 1616 done 1617 [ -s "${NETBOOT_STATE_DIR}/dnsmasq.pid" ] || die "dnsmasq did not start" 1618 1619 md5 -q "${planfile}" > "${NETBOOT_STATE_DIR}/state.hash" 1620 chown "${invoker}" "${NETBOOT_STATE_DIR}" "${conf}" "${planfile}" \ 1621 "${NETBOOT_STATE_DIR}/dnsmasq.pid" "${NETBOOT_STATE_DIR}/state.hash" \ 1622 "${NETBOOT_STATE_DIR}"/httpd.pids "${NETBOOT_STATE_DIR}"/httpd-*.log 1623} 1624 1625# Manual cleanup: `sudo sh boot-test.sh --netboot-teardown`. Not run 1626# automatically -- leaving the setup running is what makes it "once per 1627# boot" instead of "once per run" (see netboot_network_setup). 1628netboot_teardown() { 1629 if [ -s "${NETBOOT_STATE_DIR}/dnsmasq.pid" ]; then 1630 kill "$(cat ${NETBOOT_STATE_DIR}/dnsmasq.pid)" 2>/dev/null || true 1631 fi 1632 if [ -s "${NETBOOT_STATE_DIR}/httpd.pids" ]; then 1633 while read pid; do 1634 kill "${pid}" 2>/dev/null || true 1635 done < "${NETBOOT_STATE_DIR}/httpd.pids" 1636 fi 1637 for vmnet in $(ifconfig -g boot-test 2>/dev/null); do 1638 ifconfig "${vmnet}" destroy 1639 done 1640 rm -rf "${NETBOOT_STATE_DIR}" 1641 echo "Netboot vmnet(4)/dnsmasq setup torn down." 1642} 1643 1644assemble_all_images() { 1645 echo "=== Phase 3: Assembling disk images ===" 1646 1647 if has efi; then 1648 if [ -r "$(param efi_firmware)" ]; then 1649 assemble_efi_gpt 1650 if [ -f "${IMGDIR}/gptboot.esp" ]; then 1651 assemble_efi_gpt_ab 1652 fi 1653 if has mbr; then 1654 assemble_efi_mbr 1655 fi 1656 elif [ -n "$(param efi_firmware)" ]; then 1657 echo "WARNING: EFI firmware not found at $(param efi_firmware), skipping EFI tests" 1658 else 1659 # riscv64 uses u-boot with EFI payload 1660 assemble_efi_gpt 1661 fi 1662 fi 1663 1664 if has bios; then 1665 assemble_bios_gpt 1666 if has mbr; then 1667 assemble_bios_mbr 1668 fi 1669 fi 1670 1671 if has bios && has efi && [ -r "$(param efi_firmware)" ]; then 1672 assemble_both_gpt 1673 if has mbr; then 1674 assemble_both_mbr 1675 fi 1676 fi 1677 1678 if has cd; then 1679 if has prep; then 1680 assemble_pseries_cd # pseries SLOF CHRP CD 1681 elif has ofw; then 1682 assemble_ofw_cd # mac99 Apple/OF CD 1683 else 1684 assemble_cd # x86 El Torito 1685 fi 1686 fi 1687 1688 if has prep; then 1689 assemble_prep 1690 fi 1691 1692 # Used for mac99 emulation, though kernel issues prevent testing 1693 if has ofw; then 1694 assemble_ofw 1695 fi 1696 1697 if has linuxboot; then 1698 if [ -f "${IMGDIR}/linuxboot.esp" ]; then 1699 assemble_linuxboot 1700 elif [ -f "${IMGDIR}/linuxboot-initrd.cpio.gz" ]; then 1701 assemble_linuxboot_direct 1702 fi 1703 fi 1704 1705 if has netboot; then 1706 assemble_netboot 1707 fi 1708} 1709 1710# -------------------------------------------------------------------------- 1711# Phase 4: Run tests in parallel 1712# -------------------------------------------------------------------------- 1713 1714run_one_test() { 1715 name=$1 1716 log="${LOGDIR}/${name}.log" 1717 cmd=$(cat ${OUTDIR}/test-cmd-${name}.sh) 1718 1719 expect -c " 1720 set timeout ${TIMEOUT} 1721 log_file -noappend \"${log}\" 1722 spawn {*}${cmd} 1723 expect { 1724 \"SUCCESS\" { exit 0 } 1725 timeout { exit 1 } 1726 eof { exit 2 } 1727 } 1728 " >/dev/null 2>&1 1729 return $? 1730} 1731 1732wait_for_slot() { 1733 # Wait until fewer than MAX_JOBS are running 1734 while true; do 1735 running=0 1736 for p in ${active_pids}; do 1737 if kill -0 $p 2>/dev/null; then 1738 running=$((running + 1)) 1739 fi 1740 done 1741 [ ${running} -lt ${MAX_JOBS} ] && break 1742 sleep 1 1743 done 1744} 1745 1746# Launch every arch's tests together and wait once, so a run of N arches costs 1747# roughly one timeout rather than N. Each `run_one_test &` snapshots the 1748# per-arch OUTDIR/LOGDIR/TIMEOUT that setup_arch_env just set, so backgrounded 1749# jobs keep their own arch context even as we move on to the next arch. 1750run_all_tests() { 1751 echo "=== Phase 4: Running tests ===" 1752 1753 total=0 1754 skipped=0 1755 active_pids="" 1756 results_map=$(mktemp -t boot-test-results) 1757 1758 for arch in ${ARCHES}; do 1759 setup_arch_env "${arch}" 1760 [ -s "${TESTLIST}" ] || continue 1761 1762 while read name; do 1763 # Apply test filter if given 1764 if [ -n "${TEST_FILTER}" ]; then 1765 echo "${name}" | grep -qE "${TEST_FILTER}" || { 1766 skipped=$((skipped + 1)) 1767 continue 1768 } 1769 fi 1770 1771 total=$((total + 1)) 1772 1773 # Job throttling (global across all arches) 1774 if [ ${MAX_JOBS} -gt 0 ]; then 1775 wait_for_slot 1776 fi 1777 1778 run_one_test "${name}" & 1779 pid=$! 1780 active_pids="${active_pids} ${pid}" 1781 echo "${pid} ${arch} ${name}" >> ${results_map} 1782 done < ${TESTLIST} 1783 done 1784 1785 [ ${total} -gt 0 ] || die "No tests registered. Run without -B first." 1786 echo " ${total} tests launched (${skipped} skipped by filter), waiting..." 1787 echo "" 1788 1789 # Collect results - disable errexit since wait returns the child's exit status 1790 set +e 1791 pass=0 1792 fail=0 1793 timeout_count=0 1794 while read pid arch name; do 1795 wait ${pid} 1796 rc=$? 1797 case ${rc} in 1798 0) 1799 result="PASS" 1800 pass=$((pass + 1)) 1801 ;; 1802 1) 1803 result="TIMEOUT" 1804 timeout_count=$((timeout_count + 1)) 1805 ;; 1806 *) 1807 result="FAILED" 1808 fail=$((fail + 1)) 1809 ;; 1810 esac 1811 printf " %-12s %-40s %s\n" "${arch}" "${name}" "${result}" 1812 done < ${results_map} 1813 1814 echo "" 1815 echo "=== Results: ${pass} passed, ${fail} failed, ${timeout_count} timed out (of ${total}) ===" 1816 1817 rm -f ${results_map} 1818 [ ${fail} -eq 0 ] && [ ${timeout_count} -eq 0 ] 1819} 1820 1821# Build the tree and images for each arch, one arch at a time. Each arch runs 1822# in a subshell so its setup_arch_env globals stay isolated and a build failure 1823# (set -e) is caught here instead of aborting the whole run. 1824build_all() { 1825 for arch in ${ARCHES}; do 1826 echo "############################################################" 1827 echo "# ${arch}: building" 1828 echo "############################################################" 1829 if ! ( 1830 setup_arch_env "${arch}" 1831 need_cmd "$(param qemu_bin)" 1832 ${SKIP_BUILD} || build_tree 1833 if ! ${SKIP_IMAGES}; then 1834 [ -d "${DESTDIR}" ] || \ 1835 die "No boot tree at ${DESTDIR}. Run without -b first." 1836 : > ${TESTLIST} # fresh test list before (re)assembling 1837 make_base_images 1838 assemble_all_images 1839 fi 1840 ); then 1841 echo " ${arch}: BUILD FAILED" 1842 fi 1843 done 1844} 1845 1846# -------------------------------------------------------------------------- 1847# Main 1848# -------------------------------------------------------------------------- 1849 1850# --netboot-helper/--netboot-teardown are internal/manual entry points that 1851# skip the whole build+test flow below; see netboot_network_setup. 1852case "${NETBOOT_MODE}" in 1853 helper) netboot_helper "${NETBOOT_HELPER_PLAN}"; exit $? ;; 1854 teardown) netboot_teardown; exit $? ;; 1855esac 1856 1857# Preflight: universal tools (per-arch qemu binaries are checked in build_all). 1858for prog in jq expect makefs mkimg cc; do 1859 need_cmd "${prog}" 1860done 1861 1862# gptattr sets the GPT bootme attribute for the A/B test; see 1863# assemble_efi_gpt_ab. It is a host tool and independent of the target 1864# architecture, so build it once here rather than per-arch. 1865GPTATTR=$(mktemp -t boot-test-gptattr) 1866if ! cc -o ${GPTATTR} ${SRCTOP}/tools/boot/gptattr.c -lz 2>/dev/null; then 1867 die "Failed to build ${SRCTOP}/tools/boot/gptattr.c" 1868fi 1869 1870echo "FreeBSD boot loader test suite: ${ARCHES}" 1871echo "" 1872 1873# Accumulates netboot test plan lines across every arch (unlike ${TESTLIST}, 1874# which build_all truncates per-arch) -- see register_netboot_test. 1875NETBOOT_PLAN=$(mktemp -t boot-test-netboot-plan) 1876 1877if $do_report_dirs; then 1878 for arch in ${ARCHES}; do 1879 setup_arch_env "${arch}" 1880 echo "${arch} settings:" 1881 echo " TIMEOUT=${TIMEOUT}" 1882 echo " ARCH_OBJDIR=${ARCH_OBJDIR}" 1883 echo " OUTDIR=${OUTDIR}" 1884 echo " LOGDIR=${LOGDIR}" 1885 echo " DESTDIR=${DESTDIR}" 1886 echo " TESTLIST=${TESTLIST}" 1887 done 1888 exit 0 1889fi 1890 1891build_all 1892netboot_network_setup 1893if run_all_tests; then 1894 rc=0 1895else 1896 rc=$? 1897fi 1898rm -f "${NETBOOT_PLAN}" 1899exit ${rc} 1900