From nobody Sat Sep 26 06:44:56 2026 X-Original-To: dev-commits-src-all@mlmmj.nyi.freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2610:1c1:1:606c::19:1]) by mlmmj.nyi.freebsd.org (Postfix) with ESMTP id 4hsJ3F308bz6tQ0S for ; Sat, 26 Sep 2026 06:44:57 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from mxrelay.nyi.freebsd.org (mxrelay.nyi.freebsd.org [IPv6:2610:1c1:1:606c::19:3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange x25519 server-signature RSA-PSS (4096 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mxrelay.nyi.freebsd.org", Issuer "YR2" (not verified)) by mx1.freebsd.org (Postfix) with ESMTPS id 4hsJ3F14qHz3NZG for ; Sat, 26 Sep 2026 06:44:56 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1790405097; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=3exZjIoGSI1Luqgt4UEr+TtA2nlDsMXj5ZtVGmVTNIw=; b=oBm9WoOgpRYalT5ycSmXd/WLhICgpmaCKkfG23+TzSOhEvOaLRSSjQZA8rMJhH7tD+yoWK PD2L1yXSfUYjKe5UlEG/UW9Hyskdl0X6KGWXaoCNfSbq2hp3di+X0+MpVxleCGZc83dFyb k1I+AMNgcsyA7qzfP/sWZgZJpZECKhUyMhmmIthgyywaalnc+UcLZSyTpH4ure3Ta0MvDc tA2ljoagpFd4v1/C7EYEWx1+X7mNQH3P0DFsFP2cofO6sbw3bY612NIyKx/ju6Em2MlpSY +8l5BEM50+8zVU3B801x2tc+4sx2sqR6C1so2U6ObjCupsuObFcCCHEmQW5OfQ== ARC-Seal: i=1; a=rsa-sha256; d=freebsd.org; s=dkim; cv=none; t=1790405097; b=DUoBT2GrUlgisPS+3nH6dNxusIcHt8X3ulXABSY3QbinxK4SSeHsGA8UWDbw+hu7CLqWLO 8723tsWFq3/E2/0Yv+vVWfuMOhv9GXVkeHD2CfR31H67lxEVYk9yimSwFqeXhTs8KkcyHX unl4+RF3DPHlK0WvxuOAL9gmRVZ2Y1IFfyyAtEcK+pplzkw3syJG5YMyf0cbSwUvQP3Nrp /mPu0pdmh+gIXDLCxKQM/7Oe/J/vI+iT90oCYq5OFxwzPS0z233bxondlqGoH8PALhM+jM hcpXon5/QW47qIc46eJRmTw9UCgbX/o6VPDGpZlT1uOvM+C4MGcVR5Tbn26HYg== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1790405097; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=3exZjIoGSI1Luqgt4UEr+TtA2nlDsMXj5ZtVGmVTNIw=; b=nZERgEAyb4h6eYmNIm/Eu3yhd1hcgRm7jwm83/DHHvaITcJdzmXR76dzFCyvZKxaW5r0Yt b9hIftu47k9tBUXKwHrGOlx4u+VLBdMyT6rHlfjzdWHZRSOTbAcU5Cnr/OJDWUeaj2+Lxj RT6eK9XcCcWhN+QuyYAY3McRb3DlckRl1GgGv/k+RveHZVXJgWu4mtLw95wb4ON138ISTw 6XW18iS9Fke8YL52+anaCfYzmdurT1NSZhYCxclfm7LxXm6jbkr7a5kVJMAP2jK4GUhshs VMObHY/Ui6cKiWFPJecGnuC/fWSTBwt4M6YPSah/9BA2rgZ01TFPaUaXsz/cHw== ARC-Authentication-Results: i=1; mx1.freebsd.org; none Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) by mxrelay.nyi.freebsd.org (Postfix) with ESMTP id 4hsJ3D5KwFzmPB for ; Sat, 26 Sep 2026 06:44:56 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from git (uid 1279) (envelope-from git@FreeBSD.org) id 366b2 by gitrepo.freebsd.org (DragonFly Mail Agent v0.13+ on gitrepo.freebsd.org); Sat, 26 Sep 2026 06:44:56 +0000 To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org From: Warner Losh Subject: git: d8acc2a9a431 - main - boot-test.sh: Add gptboot.efi tests List-Id: Commit messages for all branches of the src repository List-Archive: https://lists.freebsd.org/archives/dev-commits-src-all List-Help: List-Post: List-Subscribe: List-Unsubscribe: X-BeenThere: dev-commits-src-all@freebsd.org Sender: owner-dev-commits-src-all@FreeBSD.org List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Git-Committer: imp X-Git-Repository: src X-Git-Refname: refs/heads/main X-Git-Reftype: branch X-Git-Commit: d8acc2a9a431b4d57ef79b2ca3113ecf33b95d8d Auto-Submitted: auto-generated Date: Sat, 26 Sep 2026 06:44:56 +0000 Message-Id: <6ab769e8.366b2.a25b9d4@gitrepo.freebsd.org> The branch main has been updated by imp: URL: https://cgit.FreeBSD.org/src/commit/?id=d8acc2a9a431b4d57ef79b2ca3113ecf33b95d8d commit d8acc2a9a431b4d57ef79b2ca3113ecf33b95d8d Author: Warner Losh AuthorDate: 2026-09-24 15:02:23 +0000 Commit: Warner Losh CommitDate: 2026-09-26 06:44:21 +0000 boot-test.sh: Add gptboot.efi tests Make sure that we can chainboot with gptboot.efi. Many projects use this as their migration tool from gptboot to ping-pong partitions to boot from. This tests that functionality which I recently broke. Assisted-by: Claude Code (Fable 5, Opus 5) Sponsored by: Netflix --- tools/boot/boot-test.sh | 125 ++++++++++++++++++++++++++++++++++++++++++++- tools/boot/gptattr.c | 133 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 257 insertions(+), 1 deletion(-) diff --git a/tools/boot/boot-test.sh b/tools/boot/boot-test.sh index a2149e5da80a..909c87fa1288 100755 --- a/tools/boot/boot-test.sh +++ b/tools/boot/boot-test.sh @@ -199,6 +199,10 @@ fi # The smallest FAT32 filesystem is 33292 KB espsize=33292 +# GPT attribute bit that marks the partition to boot, from +# sys/sys/disk/gpt.h (GPT_ENT_ATTR_BOOTME). Used for the A/B test. +GPT_ATTR_BOOTME=59 + # Linux kernel version for linuxboot tests LINUX_VERSION=6.18.2 @@ -495,6 +499,84 @@ make_one_zfs() { rm -f ${mt} } +# -------------------------------------------------------------------------- +# A/B (dual root) images +# +# gptboot.efi is the only FreeBSD EFI component that reads the GPT bootme +# attribute, so it is the only way to test that the loader it chainloads +# honours the partition that was selected for it, rather than picking a root +# of its own. +# +# The failure being tested for is "boots fine, wrong root", not "fails to +# boot": a loader that ignores the selected partition still reaches /etc/rc +# and still looks healthy. So the two roots have to differ in what they +# print, and only the bootme one may print the string run_one_test greps for. +# Two identical roots (what mkimg writes if handed the same image twice) +# would make this test permanently green. +# -------------------------------------------------------------------------- + +# Create one of the two A/B root images. ${1} is the slice letter: each slice +# gets its own UFS label, an fstab pointing at that label, and an /etc/rc that +# names the slice it booted from. Only slice B -- the one set bootme in +# assemble_efi_gpt_ab -- prints SUCCESS; booting A prints WRONG SLICE instead +# and the test fails on timeout. +make_one_ufs_ab() { + slice=$1 + img=${IMGDIR}/bootable-ufs-ab-${slice}.img + mt=$(mktemp ${OUTDIR}/ufs-ab-mtree.XXXXXX) + fstab=${OUTDIR}/fstab.ufs.ab-${slice} + rc=${OUTDIR}/rc.ab-${slice} + + echo " Creating UFS image for A/B slice ${slice}..." + echo "/dev/ufs/root${slice} / ufs rw 1 1" > ${fstab} + + # Both roots report which one got mounted, so a failing log says what + # happened; only B counts as a pass. vfs.root.mountfrom is the direct + # readout of the loader's choice. + if [ "${slice}" = "B" ]; then + verdict='echo "RC COMMAND RUNNING -- SUCCESS!!!!!"' + else + verdict='echo "WRONG SLICE -- booted A, expected B"' + fi + cat > ${rc} < ${mt} + echo "./etc/rc type=file mode=0755 contents=${rc}" >> ${mt} + echo "./boot/loader.efi type=file mode=0755 contents=${DESTDIR}/boot/loader_lua.efi" >> ${mt} + makefs -t ffs -B $(param byte_order) -M 10m -o label=root${slice} -o version=2 \ + ${img} ${mt} ${DESTDIR} >> ${LOGDIR}/imagebuild.log 2>&1 + rm -f ${mt} +} + +# Create an ESP holding gptboot.efi rather than a loader variant. gptboot.efi +# is a boot1 flavour: it reads the GPT, picks the bootme partition, and +# chainloads /boot/loader.efi from it. +make_gptboot_esp() { + esp=${IMGDIR}/gptboot.esp + mt=$(mktemp ${OUTDIR}/esp-mtree.XXXXXX) + + echo " Creating ESP with gptboot.efi..." + cat > ${mt} <> ${LOGDIR}/imagebuild.log 2>&1 + rm -f ${mt} +} + # Create an ESP with the given EFI loader using an mtree spec make_one_esp() { loader_name=$1 @@ -666,6 +748,12 @@ make_base_images() { for l in $(param efi_loaders); do make_one_esp $l done + # A/B: gptboot.efi on the ESP, plus two tellable-apart roots. + if [ -f "${DESTDIR}/boot/gptboot.efi" ]; then + make_gptboot_esp + make_one_ufs_ab A + make_one_ufs_ab B + fi fi # Linuxboot initrd + ESP (if supported and Linux kernel is available) @@ -748,6 +836,30 @@ assemble_efi_gpt() { done } +# Build the A/B disk and register its test. Layout mirrors an A/B upgrade +# scheme: one ESP holding gptboot.efi, then two interchangeable root slices. +# +# bootme is set on p3 (slice B, the second root) on purpose. With no bootme +# anywhere gptboot.efi falls back to the first UFS partition, which is slice +# A, so marking p2 instead would pass even if the attribute were ignored +# entirely. Marking the second root means only a loader that honours the +# selected partition can reach slice B and print SUCCESS. +assemble_efi_gpt_ab() { + echo " Assembling EFI+GPT A/B image..." + name="efi-gpt-ufs-ab" + img=${IMGDIR}/${name}.img + + mkimg -s gpt \ + -p efi:=${IMGDIR}/gptboot.esp \ + -p freebsd-ufs:=${IMGDIR}/bootable-ufs-ab-A.img \ + -p freebsd-ufs:=${IMGDIR}/bootable-ufs-ab-B.img \ + -o ${img} >> ${LOGDIR}/imagebuild.log 2>&1 + + ${GPTATTR} ${img} 3 ${GPT_ATTR_BOOTME} >> ${LOGDIR}/imagebuild.log 2>&1 + + register_test ${name} $(qemu_efi ${img}) +} + assemble_efi_mbr() { echo " Assembling EFI+MBR images..." for loader in $(param efi_loaders); do @@ -1488,6 +1600,9 @@ assemble_all_images() { if has efi; then if [ -r "$(param efi_firmware)" ]; then assemble_efi_gpt + if [ -f "${IMGDIR}/gptboot.esp" ]; then + assemble_efi_gpt_ab + fi if has mbr; then assemble_efi_mbr fi @@ -1693,10 +1808,18 @@ case "${NETBOOT_MODE}" in esac # Preflight: universal tools (per-arch qemu binaries are checked in build_all). -for prog in jq expect makefs mkimg; do +for prog in jq expect makefs mkimg cc; do need_cmd "${prog}" done +# gptattr sets the GPT bootme attribute for the A/B test; see +# assemble_efi_gpt_ab. It is a host tool and independent of the target +# architecture, so build it once here rather than per-arch. +GPTATTR=$(mktemp -t boot-test-gptattr) +if ! cc -o ${GPTATTR} ${SRCTOP}/tools/boot/gptattr.c -lz 2>/dev/null; then + die "Failed to build ${SRCTOP}/tools/boot/gptattr.c" +fi + echo "FreeBSD boot loader test suite: ${ARCHES}" echo "" diff --git a/tools/boot/gptattr.c b/tools/boot/gptattr.c new file mode 100644 index 000000000000..547ebde3c047 --- /dev/null +++ b/tools/boot/gptattr.c @@ -0,0 +1,133 @@ +/* + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +/* + * Set a GPT attribute on a partition of a disk image file. + * + * gpart(8) does the same thing, but only on a real device and only as root. + * boot-test.sh builds every image as an unprivileged user, so it needs to + * edit the on-disk GPT itself. Both the primary and the secondary entry + * table are updated: the loader only reads the primary, but the kernel + * checks both, and a mismatch would be reported as a corrupt GPT during the + * boot under test. + * + * Usage: gptattr + * where is 1-based, matching gpart(8) partition numbering. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define SECSZ 512 + +static uint8_t *image; +static size_t imagesz; + +/* + * Apply the attribute to one entry table and refresh the CRCs of the header + * at hdroff that describes it. + */ +static void +patch_table(uint64_t hdroff, u_int index, uint64_t attr) +{ + struct gpt_hdr *hdr; + struct gpt_ent *ent; + uint64_t tbloff; + uint32_t entries, entsz, hdrsz; + + if (hdroff + SECSZ > imagesz) + errx(1, "GPT header at offset %ju is past end of image", + (uintmax_t)hdroff); + + hdr = (struct gpt_hdr *)(image + hdroff); + if (memcmp(hdr->hdr_sig, GPT_HDR_SIG, sizeof(hdr->hdr_sig)) != 0) + errx(1, "no GPT header at offset %ju", (uintmax_t)hdroff); + + hdrsz = le32toh(hdr->hdr_size); + entries = le32toh(hdr->hdr_entries); + entsz = le32toh(hdr->hdr_entsz); + tbloff = le64toh(hdr->hdr_lba_table) * SECSZ; + + if (index < 1 || index > entries) + errx(1, "partition index %u out of range (1..%u)", index, + entries); + if (tbloff + (uint64_t)entries * entsz > imagesz) + errx(1, "GPT entry table is past end of image"); + + ent = (struct gpt_ent *)(image + tbloff + (uint64_t)(index - 1) * entsz); + le64enc(&ent->ent_attr, le64toh(ent->ent_attr) | attr); + + /* + * The table CRC covers the entries, and the header CRC covers the + * header including that table CRC, so recompute them in that order. + * hdr_crc_self is zeroed while it is being computed over itself. + */ + le32enc(&hdr->hdr_crc_table, crc32(0, image + tbloff, + entries * entsz)); + hdr->hdr_crc_self = 0; + le32enc(&hdr->hdr_crc_self, crc32(0, (const uint8_t *)hdr, hdrsz)); +} + +int +main(int argc, char *argv[]) +{ + FILE *fp; + char *end; + uint64_t attr; + u_long bit, index; + + if (argc != 4) { + fprintf(stderr, + "usage: %s \n", argv[0]); + return (1); + } + + errno = 0; + index = strtoul(argv[2], &end, 0); + if (errno != 0 || *end != '\0' || index == 0) + errx(1, "bad partition index: %s", argv[2]); + bit = strtoul(argv[3], &end, 0); + if (errno != 0 || *end != '\0' || bit > 63) + errx(1, "bad attribute bit: %s", argv[3]); + attr = (uint64_t)1 << bit; + + if ((fp = fopen(argv[1], "r+")) == NULL) + err(1, "%s", argv[1]); + if (fseek(fp, 0, SEEK_END) != 0) + err(1, "%s", argv[1]); + imagesz = (size_t)ftell(fp); + rewind(fp); + if (imagesz < 2 * SECSZ) + errx(1, "%s is too small to hold a GPT", argv[1]); + if ((image = malloc(imagesz)) == NULL) + err(1, "malloc"); + if (fread(image, 1, imagesz, fp) != imagesz) + err(1, "%s: read", argv[1]); + + /* Primary header is always at LBA 1; it names the secondary. */ + patch_table(SECSZ, index, attr); + patch_table(le64toh(((struct gpt_hdr *)(image + SECSZ))->hdr_lba_alt) * + SECSZ, index, attr); + + rewind(fp); + if (fwrite(image, 1, imagesz, fp) != imagesz) + err(1, "%s: write", argv[1]); + if (fclose(fp) != 0) + err(1, "%s: close", argv[1]); + free(image); + + return (0); +}