Page MenuHomeFreeBSD

freebsd-update: automatically update EFI and BIOS bootloaders on install
Needs ReviewPublic

Authored by ronald.pagani.jr_dev.us.com on Wed, Aug 19, 3:40 PM.
Tags
None
Referenced Files
F168254781: D58990.id184363.diff
Thu, Aug 27, 5:12 AM
F168211080: D58990.id.diff
Wed, Aug 26, 10:17 PM
F168123212: D58990.id184363.diff
Wed, Aug 26, 11:58 AM
Unknown Object (File)
Wed, Aug 26, 12:37 AM
Unknown Object (File)
Tue, Aug 25, 10:20 PM
Unknown Object (File)
Tue, Aug 25, 4:25 PM
Unknown Object (File)
Tue, Aug 25, 3:16 PM
Unknown Object (File)
Mon, Aug 24, 6:27 PM

Details

Summary

freebsd-update only updates files under the root filesystem. The EFI
System Partition (ESP) is a separate FAT32 partition that the UEFI
firmware reads directly. After a major version upgrade, the old
loader.efi on the ESP may be incompatible with the new /boot/lua
scripts โ€” since 14.1 removed the lua_path==nil compatibility shim in
core.lua, this results in a hard boot failure. Running zpool upgrade
with a stale ESP loader makes the system completely unbootable.

This patch adds efi_bootloader_update.sh, installed to /usr/libexec/,
and hooks it into the install_run phase of freebsd-update. After
install_files completes, the script uses boot-scoped ESP discovery:
BootCurrent NVRAM identifies the active boot disk; root filesystem
disk(s) are determined via zpool status (ZFS) or mount --libxo json
(UFS). The union of both sets is used as candidates, ensuring mirror
members are all updated while unrelated ESPs (other OSes, other
FreeBSD installations) are never touched. A split-media guard restricts
candidates to the BootCurrent disk when it does not overlap with the
root filesystem disks.

For each candidate ESP the script:

  • Mounts it (or reuses an existing mount)
  • Updates /EFI/FreeBSD/loader.efi, creating the directory if absent
  • Updates /EFI/BOOT/BOOTx64.efi (or arch equivalent) only if it fingerprints as a FreeBSD loader โ€” multi-OS safe
  • Creates an NVRAM boot entry if none exists (non-fatal if unavailable)
  • Unmounts the ESP

For BIOS freebsd-boot partitions (scoped to root filesystem disks):

  • Writes gptzfsboot or gptboot as appropriate

MBR partition schemes are supported. Graceful fallback on platforms
without EFI Runtime Services.

Opt-out: set UpdateBootloader=no in /etc/freebsd-update.conf.
Env override: EFI_NVRAM_UPDATE=0 skips NVRAM writes.

PR: 279829
See also: D45890 (Warner Losh's complementary loader version-check patch)

Test Plan

sh tests/run_tests.sh
286/286 passing on FreeBSD 14.0-RELEASE-p11 and 15.1-RELEASE-p2

Live run on FreeBSD 14.0-RELEASE-p11 amd64: two reboots clean,
Boot0004 NVRAM entry active.

Hardware validation across six platforms (amd64, Honeycomb LX2160A,
WDK23, RPi4B, RPi5, RPi3B), eight rounds of testing contributed by markmi. Full reports:
https://github.com/RZA-SF/freebsd-patch-for-bug279829/blob/main/docs/hardware-reports.md

Diff Detail

Repository
rG FreeBSD src repository
Lint
Lint Skipped
Unit
Tests Skipped

Event Timeline

Is it deliberate that there is coverage of armv7 and its bootarm.efi ?

usr.sbin/freebsd-update/efi_bootloader_update.sh
138

Do chroot contexts need something analogous to the above jail avoidance?

usr.sbin/freebsd-update/efi_bootloader_update.sh
248

Hmm:

# zfs get -H -o value name / 2>/dev/null
zoptb/ROOT/main-amd64
# zpool status zoptb/ROOT/main-amd64 2>/dev/null
# 
# bectl list -a
BE/Dataset/Snapshot                             Active Mountpoint Space Created

main-amd64
  zoptb/ROOT/main-amd64                         NR     /          20.1G 2025-10-14 21:25

previous-main-amd64
  zoptb/ROOT/previous-main-amd64                -      -          8K    2026-07-30 21:08
    zoptb/ROOT/main-amd64@2026-07-30-21:08:47-0 -      -          4.67G 2026-07-30 21:08
# gpart show -p
. . .
=>        40  2930277095    nda2  GPT  (1.4T)
          40      532480  nda2p1  efi  (260M)
      532520        2008          - free -  (1004K)
      534528  1073741824  nda2p2  freebsd-swap  (512G)
  1074276352  1845493760  nda2p3  freebsd-zfs  (880G)
  2919770112     8388608  nda2p4  freebsd-swap  (4.0G)
  2928158720     2118415          - free -  (1.0G)
. . .
usr.sbin/freebsd-update/efi_bootloader_update.sh
248

I should have also shown:

# zpool status
  pool: zoptb
 state: ONLINE
  scan: scrub repaired 0B in 00:00:54 with 0 errors on Mon May  5 23:40:50 2025
config:

	NAME           STATE     READ WRITE CKSUM
	zoptb          ONLINE       0     0     0
	  gpt/OptBzfs  ONLINE       0     0     0

errors: No known data errors
# ls -lodT /dev/gpt/OptB*
crw-r-----  1 root operator - 0xe8 Aug 13 12:04:56 2026 /dev/gpt/OptBefi
crw-r-----  1 root operator - 0xe9 Aug 13 12:04:57 2026 /dev/gpt/OptBswp364
crw-r-----  1 root operator - 0xea Aug 13 12:04:56 2026 /dev/gpt/OptBzfs
# gpart show -pl /dev/nda2
=>        40  2930277095    nda2  GPT  (1.4T)
          40      532480  nda2p1  OptBefi  (260M)
      532520        2008          - free -  (1004K)
      534528  1073741824  nda2p2  OptBswp364  (512G)
  1074276352  1845493760  nda2p3  OptBzfs  (880G)
  2919770112     8388608  nda2p4  HyperVswp4  (4.0G)
  2928158720     2118415          - free -  (1.0G)
261

My context has:

# awk '$2 == "/" && $1 !~ /^#/ { print $1; exit }' /etc/fstab 2>/dev/null
/dev/gpt/PBaseUFS

So gpt/PBaseUFS after /dev/ removal and later would get:

# gpart show gpt/PBaseUFS
gpart: No such geom: gpt/PBaseUFS.
usr.sbin/freebsd-update/efi_bootloader_update.sh
288

These may not be intended for coverage, but I'll note the MBR case of using "gpart add -t '!12'" (fat32 lba) for the EFI partitions:

/usr/src/release/tools/arm.subr has:

arm_create_disk() {
. . .
        if [ "${PART_SCHEME}" = "GPT" ]; then
                chroot ${CHROOTDIR} gpart add -t efi -l efi -a 512k -s ${FAT_SIZE} ${mddev}
                chroot ${CHROOTDIR} gpart add -t freebsd-ufs -l rootfs -a 64k ${mddev}
        fi
        if [ "${PART_SCHEME}" = "MBR" ]; then
                chroot ${CHROOTDIR} gpart add -t '!12' -a 512k -s ${FAT_SIZE} ${mddev}
                chroot ${CHROOTDIR} gpart set -a active -i 1 ${mddev}
                chroot ${CHROOTDIR} gpart add -t freebsd ${mddev}
                chroot ${CHROOTDIR} gpart create -s bsd ${mddev}${BSDLABEL_SUFFIX}
                chroot ${CHROOTDIR} gpart add -t freebsd-ufs -a 64k -b 64k ${mddev}${BSDLABEL_SUFFIX}
        fi

        # Create the EFI and UFS filesystems
        chroot ${CHROOTDIR} newfs_msdos -L efi -F ${FAT_TYPE} /dev/${mddev}${EFIPART_SUFFIX}
. . .

I'll note: /usr/src/release/arm64/RPI.conf:PART_SCHEME="MBR"
is still in use. For releases, it is the file system that always has a -L efi in this armv7 and embedded aarch64 area. (Manually set up MBR contexts may have a wider range of varation?)

Note: my examples show things that are possible for installations based on examples I have (or had). They may be all "no plan/reason to support for freebsd-update for EFI updating". If true, I'm fine with that. At least such likely would end up being explicitly indicated now.

So I generally like this. But I think that there's a number of simplifying assumptions that can be made.
My comments about 'unreliable' are based on running a large fleet for $WORK and being the boot loader person.
As well as fighting bugs over the years.
I've tried to offer better alternatives, though.
Plus two or three tries at writing this.

usr.sbin/freebsd-update/efi_bootloader_update.sh
30

Please consider omitting the boilerplate.

102

So this likely doesn't work.
We have a bootia32.efi for booting 64-bit kernels, and you'd need to carefully sniff out this case.

138

No.

159

I'm not sure this exists on !x86 platforms...

172

This is unreliable. mount --libxo json is easier to parse.
Any spaces will cause problems.

239

This breaks with zpools that were created using diskids.

288

This assumes that we're booting off the same disk that the zpool is on. That's often not the case.

329

This works only for GPT.
And it fails if there's any other partitions that are mounted using the diskid.

410

newvers.sh generates
bootprog_info="FreeBSD/${3} ${2}, Revision ${r}\\n"
where ${3} is the arch and ${2} is EFI, etc

502
# efibootmgr --esp
/dev/diskid/DISK-5430A0ALTMAJp1

gives you this directly.

I'm not sure why you are updating the bootmgr entry. It tends to be somewhat unreliable.
I added --esp to find the right name you'd need to update....

usr.sbin/freebsd-update/efi_bootloader_update.sh
502

Windows Dev Kit 2023 (aarch64) {my currently handy aarch64 context]:

# efibootmgr --esp
efibootmgr: Can't get BootCurrent: Function not implemented

Thank you both for the thorough review!

I've gone through all the comments carefully. Here is my understanding of the issues and proposed resolutions โ€” please correct me where I've misread the intent.

Issue 7 (line 287) โ€” ESP disk discovery architecture:
This is the core concern. The current approach derives boot disks from ZFS pool membership, which fails when the ESP lives on a dedicated disk separate from the pool. My proposed fix is to replace this with a scan of all disks via sysctl kern.disks, using gpart show to find EFI-typed partitions on each, and fingerprinting to confirm FreeBSD ownership before updating. This is the approach used by the loaders-update port. It also resolves the diskid issue (line 238) and the GPT-only device construction issue (line 328) since disk names come from the kernel's disk list rather than being derived from pool topology.

Is this what you had in mind, or were you thinking of a different mechanism? (e.g. reading the active EFI boot variable).

Line 171 โ€” mount parsing:
Will replace both mount | awk call sites with mount --libxo json.

Line 137 โ€” chroot:
To clarify: the existing jail check (security.jail.jailed) โ€” should stay, correct?

Line 101 โ€” bootia32.efi:
I didn't realize this but now I understand FreeBSD ships bootia32.efi for booting 64-bit kernels on hardware with 32-bit EFI. Is there a reliable way to detect this case at runtime, or would you prefer the script simply update BOOTia32.efi if it already exists on the ESP and fingerprints as a FreeBSD loader โ€” consistent with the existing fallback logic?

Line 409 โ€” bootprog_info fingerprint:
Thanks for the format string. Will update the fingerprint check to match FreeBSD/<arch> EFI, Revision pattern from bootprog_info. Will verify against a real binary first.

Line 501 โ€” efibootmgr reliability:
The NVRAM step is already non-fatal with a manual fallback instruction. Will add --esp <device> once the device path is known from the scan-all-disks approach. Note that markmi reports efibootmgr --esp returns "Function not implemented" on his aarch64 hardware (Windows Dev Kit 2023) โ€” so the non-fatal wrapper and manual fallback remain important.

markmi โ€” GPT label paths in fstab (lines 247, 260):
Confirmed real bug. With the scan-all-disks approach, EFI partition discovery moves to sysctl kern.disks directly, so efi_ufs_boot_disks is no longer in the EFI path. However, it remains in the BIOS bootcode path โ€” we need to know which disks are in the root filesystem before writing bootcode, since writing to an unrelated disk would be wrong. The GPT label fix (realpath resolution before suffix stripping) applies there.

markmi โ€” MBR !12 (line 287):
Planning to add MBR support alongside the scan-all-disks fix. The efi_discover_all_esps function will detect !12 (FAT32 LBA) and !ef (EFI System) typed partitions on MBR disks in addition to efi on GPT. The fingerprint check is the safety gate โ€” if the mounted FAT32 doesn't contain a FreeBSD loader, it is skipped silently, which handles the U-Boot/RPi case without EDK2 correctly.

usr.sbin/freebsd-update/efi_bootloader_update.sh
30

Sure thing.

239

Yes, actually this seemed like something the would change things quite a bit -- the main architectural issue. The current approach derives boot disks from ZFS pool membership, which fails when the ESP is on a dedicated boot device separate from the pool.

I revisited this and my proposed fix is to scan all disks via sysctl kern.disks, find EFI-typed partitions via gpart show, fingerprint to confirm FreeBSD ownership, and update only those. This is what the loaders-update port does and it's idempotent. It also resolves the diskid issue simultaneously since we're no longer deriving device names from pool topology.

What happens on systems with a bunch of disks... on a system with many spindles, this calls gpart show on every disk. gpart show is fast, but is that overhead acceptable in your view, or would you prefer a narrower heuristic?

Something on a related github page: "MBR (non-GPT) disks Not supported (BIOS-only, legacy)"

Turns out that the "BIOS-only" is false: official 64-bit RPi* releases are U-Boot UEFI based and FreeBSD release builds use MBR for that 64-bit aarch64 support. Various RPi* are MBR only (without the bootcode.bin file on a microsd card, anyway) and the RPi4B related EEPROM ones depend on how recent the EEPROM content is. (Different types of "legacy" than was likely intended.) armv7 is also this way as I remember.

[Note: I can and do use GPT USB media but accept the tradeoffs, such as needing a microsd card to be involved or not using old EEPROM content.]

Not supporting MBR at all may well stay true but, if so, more context may need to be mentioned/reported in that case.

usr.sbin/freebsd-update/efi_bootloader_update.sh
502

You need efirt in the kernel.
We should use this and maybe just fail if it's not avsilable. You can't reliably know otherwise. The complexity of guessing isn't worth it, imho, if /boot/efi isn't mounted or in /etc/fstab. Something that's 95% with something as critical as boot blocks is too unreliable

usr.sbin/freebsd-update/efi_bootloader_update.sh
502

For reference for the aarch64 boot media (installation is an official pkgbase distribution of main):

# strings /boot/kernel/kernel | grep -i efirt | less
nexus/efirtc
/home/pkgbuild/worktrees/main/sys/dev/efidev/efirt.c
(td->td_pflags & TDP_EFIRT) == 0
Total number of faults that occurred during EFIRT calls
(td->td_pflags & TDP_EFIRT) != 0
/home/pkgbuild/worktrees/main/sys/arm64/arm64/efirt_machdep.c
Print fault  information upon trap from EFIRT calls: 0 - never, 1 - once, 2 - always
efirtc
efirt
efirtc_nexus
options EFIRT
device  efirtc
print_efirt_faults
cnt_efirt_faults
usr.sbin/freebsd-update/efi_bootloader_update.sh
502

So far the small set of aarch64 systems that I have access to have had no examples of the --esp usage work, each producing one of the following messages (booting the same FreeBSD OS media moved across systems):

efibootmgr: Can't get BootCurrent: Function not implemented
efibootmgr: Can't get BootCurrent: No such file or directory
efibootmgr: Can't convert to unix path

The systems trieid are: the Honeycomb LX2160A UEFI/ACPI (Solidrun EDK2 based), the Windows Dev Kit 2023 UEFI/ACPI, as well as a RPI5 (via a draft/partial EDK2 UEFI/ACPI) and a RPi4B (U-Boot UEFI FreeBSD port, likely a rather old instance). The U-Boot UEFI context is what referenced "no such" and the WinDevKit23 is what referenced "not implemented". I've no evidence if what I'm seeing generalizes fairly commonly to aarch64.

Something on a related github page: "MBR (non-GPT) disks Not supported (BIOS-only, legacy)"

Turns out that the "BIOS-only" is false: official 64-bit RPi* releases are U-Boot UEFI based and FreeBSD release builds use MBR for that 64-bit aarch64 support. Various RPi* are MBR only (without the bootcode.bin file on a microsd card, anyway) and the RPi4B related EEPROM ones depend on how recent the EEPROM content is. (Different types of "legacy" than was likely intended.) armv7 is also this way as I remember.

[Note: I can and do use GPT USB media but accept the tradeoffs, such as needing a microsd card to be involved or not using old EEPROM content.]

Not supporting MBR at all may well stay true but, if so, more context may need to be mentioned/reported in that case.

There are changes in my revision-2 branch, which also include your feedback for the aarch64 environments, and ongoing updates for the test suite. The above was cited from the initial commit README.

If there are some cases where it makes more sense to not support, I agree that might be something to discuss further. My motivation was to make this as comprehensive and inclusive as possible.

ronald.pagani.jr_dev.us.com edited the summary of this revision. (Show Details)
ronald.pagani.jr_dev.us.com edited the test plan for this revision. (Show Details)

This is a complete revision-2 resubmission. The previous diff was the initial
submission; everything below is new.

Boot-scoped ESP discovery (replaces scan-all-disks)

  • BootCurrent NVRAM path (primary): efibootmgr -v parses the active boot entry PARTUUID โ€” inline HD(GPT,...) or dp: sub-line format โ€” and matches it against gpart list to identify the exact disk and partition the firmware booted from.
  • Root filesystem disk(s) (fallback / union): zpool status for ZFS (mirrors, RAIDz, diskid/gptid/GEOM-label vdev names, device-node labels via glabel status fallback); mount --libxo json for UFS.
  • Union + dedup + overlap check: only ESPs on disks plausibly related to the running system are updated.
  • Split-media guard (R-14): when BootCurrent identifies a boot disk with no overlap with root filesystem disks, candidates are restricted to the boot disk only โ€” prevents updating a portable or shared ESP belonging to other systems.
  • EFIRT guard: when /dev/efi is absent (i386, armv7, riscv64, custom kernels), falls back to root-disk discovery; NVRAM step skipped gracefully.

Review items resolved

  • #1 (imp): removed verbose BSD-2-Clause boilerplate; SPDX + copyright only
  • #4 (imp): machdep.bootmethod OID absent on aarch64/armv7 โ€” assume UEFI
  • #5 (imp): mount --libxo json replaces mount | awk throughout
  • #6 (imp): diskid/gptid pool vdev names resolved via realpath
  • #7 (imp): ESP not on zpool โ€” covered by BootCurrent PARTUUID path
  • #8 (imp): GPT-only mount assumptions removed
  • #9 (imp): bootprog_info fingerprint: primary strings | grep 'FreeBSD/[^ ]+ EFI,'; 2-of-3 heuristic fallback
  • M1 (markmi): armv7 bootarm.efi architecture mapping added
  • M3 (markmi): UFS GPT label paths (gpt/PBaseUFS) resolved via realpath
  • M4 (markmi): GPT labels in gpart show output do not break field parsing

MBR support

  • Detects fat32lba, fat32, efi partition types; also !12/!ef fallback
  • MBR device paths use s-suffix (/dev/da0s1) vs. GPT p-suffix
  • Covers ARM SBCs (RPi, WDK23) running UEFI via U-Boot on MBR-partitioned storage
  • Safety gate: MBR FAT32 partition only written if efi_is_freebsd_loader passes

Bug fixes from hardware validation (R-06 through R-14)

  • R-06: efibootmgr -v PARTUUID on dp: sub-line, not inline with HD(GPT,...)
  • R-07: ZFS gpt/LABEL vdev awk filter excluded label-named vdevs; fixed with in_config flag and $1 != pool guard
  • R-08: GEOM label /dev/gpt/X as device node โ€” realpath unchanged; fixed with glabel status fallback in ZFS and UFS root disk paths
  • R-09: Three MBR bugs: ufs/* label not matched; sed stripped trailing s from label names; fat32lba type not recognized
  • R-10: Dry-run space check measured root filesystem free space instead of ESP; blank-ESP notice appeared unconditionally even when ESP was pre-mounted
  • R-11: efi_esp_mountpoint did not detect ESP mounted via msdosfs GEOM label (/dev/msdosfs/EFI) โ€” "Device busy" on RPi3B/RPi4B MBR installs
  • R-12: _efi_esp_is_real flag: dry-run space/blank-ESP guards fired even when operating on a real pre-mounted ESP, producing contradictory output
  • R-13: MBR summary line used pN suffix instead of sN; WARN showed template path instead of actual path
  • R-14: Split-media guard: BootCurrent disk not in root disk set โ†’ restrict candidates to boot disk only

Loader backward compatibility

Source inspection of stand/common/bootinfo.c, stand/lua/core.lua, and
sys/sys/linker.h confirms: newer loaders (15.x, 16-CURRENT) can boot older
kernels (13.x, 14.x). core.loaderTooOld() tests whether the loader is too
old for current Lua scripts โ€” not whether the kernel is too old for the loader.
No kernel-side version gate exists. Documented in design.md ยง6.7.

Open items

  • #2 (imp): bootia32.efi โ€” source /boot/loader_ia32.efi absent on 13.x. Documented with resolution path in design.md ยง6.8; deferred.

Test suite

286 assertions across 43 files (unit, integration, error conditions).
Verified on FreeBSD 14.0-RELEASE-p11 and 15.1-RELEASE-p2.
Eight rounds of hardware validation across six platforms (amd64, Honeycomb
LX2160A, WDK23, RPi4B, RPi5, RPi3B) contributed by markmi.
Full reports: https://github.com/RZA-SF/freebsd-patch-for-bug279829/blob/main/docs/hardware-reports.md

usr.sbin/freebsd-update/efi_bootloader_update.sh
261

The script no longer contains the usage of the /dev/ removed string for this as far as I know.

Likely the above can be declared done.

(I do not seem to be able to delete my inline comments --or edit them-- when they become irrelevant.)

I'll note that efi_bootloader_update.sh could be considered to be of direct use for folks using pkgbase: pkgbase does not deal with updating the FreeBSD loaders in the ESP.

(Nor does pkgbase automatically do a reboot between a kernel upgrade and the matching world upgrade --or prompt then stop for such. freebsd-update can prompt and stop for such.)

Side note, Just for reference:
Nothing deals with updating the aarch64 or armv7 dtb/ materials in the ESP that use FreeBSD-builds of Linux based *.dtb files. Not everything has/uses such FreeBSD built files. RPi* use files from a FreeBSD port instead, as well.

ronald.pagani.jr_dev.us.com added inline comments.
usr.sbin/freebsd-update/efi_bootloader_update.sh
102

I am procuring an old Atom-based box today to work out the ia32 boot loader w/ 64-bit kernel situation. I am cautiously optimistic and looking forward to sharing this.