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