Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F168211080
D58990.id.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
49 KB
Referenced Files
None
Subscribers
None
D58990.id.diff
View Options
diff --git a/usr.sbin/freebsd-update/Makefile b/usr.sbin/freebsd-update/Makefile
--- a/usr.sbin/freebsd-update/Makefile
+++ b/usr.sbin/freebsd-update/Makefile
@@ -2,4 +2,9 @@
SCRIPTS=freebsd-update.sh
MAN= freebsd-update.8
+FILESGROUPS+= LIBEXEC
+LIBEXEC= efi_bootloader_update.sh
+LIBEXECDIR= /usr/libexec
+LIBEXECMODE= 0755
+
.include <bsd.prog.mk>
diff --git a/usr.sbin/freebsd-update/efi_bootloader_update.sh b/usr.sbin/freebsd-update/efi_bootloader_update.sh
new file mode 100755
--- /dev/null
+++ b/usr.sbin/freebsd-update/efi_bootloader_update.sh
@@ -0,0 +1,1185 @@
+#!/bin/sh
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2026 Ronald Pagani Jr.
+#
+# Update the EFI bootloader on the ESP and BIOS bootcode on freebsd-boot
+# partitions during FreeBSD upgrades. See freebsd-update(8).
+
+# Guard against double-sourcing
+[ -n "${_EFI_BOOTLOADER_UPDATE_SH:-}" ] && return 0
+_EFI_BOOTLOADER_UPDATE_SH=1
+
+# ============================================================
+# CONFIGURATION (overridable via environment)
+# ============================================================
+
+: "${EFI_LOADER_SRC:=/boot/loader.efi}"
+: "${EFI_DRY_RUN:=0}"
+: "${EFI_VERBOSE:=0}"
+: "${EFI_NVRAM_UPDATE:=1}" # Set to 0 to skip NVRAM boot entry management
+: "${EFI_BIOS_PMBR:=/boot/pmbr}"
+: "${EFI_BIOS_ZFS_BOOT:=/boot/gptzfsboot}"
+: "${EFI_BIOS_UFS_BOOT:=/boot/gptboot}"
+
+# Minimum number of FreeBSD-specific strings that must be present in an EFI
+# binary for it to be classified as a FreeBSD loader (reduces false-positives).
+_EFI_FINGERPRINT_THRESHOLD=2
+
+# Path to the EFI runtime services character device. Overridable in tests
+# (e.g. _EFI_DEV_EFI=/dev/null to simulate EFIRT present on a non-FreeBSD host).
+: "${_EFI_DEV_EFI:=/dev/efi}"
+
+# ============================================================
+# LOGGING
+# ============================================================
+
+_efi_log() { echo "freebsd-update: [bootloader] $*" >&2; }
+_efi_info() { _efi_log "INFO: $*"; }
+_efi_warn() { _efi_log "WARN: $*" >&2; }
+_efi_err() { _efi_log "ERROR: $*" >&2; }
+_efi_verb() { [ "${EFI_VERBOSE}" = "1" ] && _efi_log "DEBUG: $*" || true; }
+
+# ============================================================
+# ARCHITECTURE → EFI BINARY MAPPING
+# ============================================================
+
+# Returns the UEFI fallback binary name for the given machine architecture.
+# Argument: output of `uname -m`
+efi_fallback_binary_for_arch() {
+ case "$1" in
+ amd64|x86_64) echo "BOOTx64.efi" ;;
+ arm64|aarch64) echo "BOOTaa64.efi" ;;
+ arm|armv7) echo "BOOTarm.efi" ;;
+ i386) echo "BOOTia32.efi" ;;
+ riscv64) echo "BOOTriscv64.efi" ;;
+ *) return 1 ;;
+ esac
+}
+
+# Returns the fallback binary name for the currently running machine.
+efi_fallback_binary() {
+ local arch
+ arch=$(uname -m 2>/dev/null) || arch="unknown"
+ efi_fallback_binary_for_arch "$arch" || {
+ _efi_warn "Unsupported/unknown architecture for EFI: ${arch}"
+ return 1
+ }
+}
+
+# ============================================================
+# PREREQUISITE CHECKS
+# ============================================================
+
+# Returns:
+# 0 all checks passed — proceed
+# 1 fatal failure — abort
+# 2 in jail — skip gracefully (not an error)
+efi_check_prerequisites() {
+ local ok=0
+
+ if [ "$(id -u)" != "0" ]; then
+ _efi_err "Must be run as root"
+ ok=1
+ fi
+
+ if [ "$(sysctl -n security.jail.jailed 2>/dev/null)" = "1" ]; then
+ _efi_warn "Running inside a jail — bootloader update skipped"
+ return 2
+ fi
+
+ if [ ! -f "${EFI_LOADER_SRC}" ]; then
+ _efi_err "Source loader not found: ${EFI_LOADER_SRC}"
+ ok=1
+ elif [ ! -s "${EFI_LOADER_SRC}" ]; then
+ _efi_err "Source loader is empty: ${EFI_LOADER_SRC}"
+ ok=1
+ fi
+
+ return $ok
+}
+
+# ============================================================
+# SYSTEM DETECTION
+# ============================================================
+
+# Returns "UEFI", "BIOS", or "unknown"
+efi_boot_method() {
+ local m
+ m=$(sysctl -n machdep.bootmethod 2>/dev/null) || {
+ # machdep.bootmethod is absent on some platforms (e.g. aarch64 with
+ # certain firmware). Any modern FreeBSD platform lacking this OID
+ # uses UEFI — there is no BIOS boot on arm64, armv7, or riscv64.
+ _efi_verb "machdep.bootmethod unavailable; assuming UEFI"
+ echo "UEFI"
+ return
+ }
+ [ -n "$m" ] && { echo "$m"; return; }
+ _efi_verb "machdep.bootmethod returned empty; assuming UEFI"
+ echo "UEFI"
+}
+
+# Returns the root filesystem type: "zfs", "ufs", or the raw type string.
+# Uses mount --libxo json (available FreeBSD 10.1+) for reliable parsing.
+efi_root_fs_type() {
+ mount --libxo json 2>/dev/null | tr '}' '\n' | awk -F'"' '
+ /"node":"\/"/ {
+ for (i = 1; i <= NF; i++)
+ if ($i == "fstype") { print $(i+2); exit }
+ }
+ '
+}
+
+# ============================================================
+# DISK AND PARTITION DISCOVERY
+# ============================================================
+
+# Scan all disks visible to the kernel for EFI System Partitions.
+# Outputs one "disk part_index scheme" tuple per line.
+#
+# GPT: partitions of type "efi"
+# MBR: partitions of type "fat32lba" (0x0C), "fat32" (0x0B), or "efi" (0xEF)
+# gpart uses symbolic names for known types; "!12"/"!ef" kept as fallback.
+#
+# The fingerprint check in efi_update_esp is the safety gate for MBR partitions:
+# if the mounted FAT32 does not contain a FreeBSD loader the partition is skipped.
+efi_discover_all_esps() {
+ local disks
+ disks=$(sysctl -n kern.disks 2>/dev/null | tr ' ' '\n') || {
+ _efi_warn "Cannot enumerate disks via sysctl kern.disks"
+ return 1
+ }
+
+ local disk found=0
+ for disk in $disks; do
+ [ -z "$disk" ] && continue
+ local gpart_out scheme parts
+ gpart_out=$(gpart show "$disk" 2>/dev/null) || continue
+
+ # Header line: => start size device SCHEME (size)
+ scheme=$(printf '%s\n' "$gpart_out" | awk '$1 == "=>" { print $5; exit }')
+
+ case "$scheme" in
+ GPT)
+ parts=$(printf '%s\n' "$gpart_out" | \
+ awk -v d="$disk" '$4 == "efi" { print d, $3, "GPT" }')
+ ;;
+ MBR)
+ parts=$(printf '%s\n' "$gpart_out" | \
+ awk -v d="$disk" '$4 == "fat32lba" || $4 == "fat32" || $4 == "efi" || $4 == "!12" || $4 == "!ef" { print d, $3, "MBR" }')
+ ;;
+ *)
+ _efi_verb "Skipping ${disk}: unrecognised partition scheme '${scheme}'"
+ continue
+ ;;
+ esac
+
+ if [ -n "$parts" ]; then
+ printf '%s\n' "$parts"
+ found=1
+ fi
+ done
+
+ [ "$found" -eq 0 ] && return 1
+ return 0
+}
+
+# Scan all disks for freebsd-boot partitions (GPT only; type is FreeBSD-specific).
+# Outputs one "disk part_index" tuple per line.
+efi_discover_all_bios_parts() {
+ local disks
+ disks=$(sysctl -n kern.disks 2>/dev/null | tr ' ' '\n') || {
+ _efi_warn "Cannot enumerate disks via sysctl kern.disks"
+ return 1
+ }
+
+ local disk found=0
+ for disk in $disks; do
+ [ -z "$disk" ] && continue
+ local parts
+ parts=$(gpart show "$disk" 2>/dev/null | \
+ awk -v d="$disk" '$4 == "freebsd-boot" { print d, $3 }') || true
+ if [ -n "$parts" ]; then
+ printf '%s\n' "$parts"
+ found=1
+ fi
+ done
+
+ [ "$found" -eq 0 ] && return 1
+ return 0
+}
+
+# ============================================================
+# ROOT FILESYSTEM DISK IDENTIFICATION
+# ============================================================
+
+# Returns disk names (one per line) hosting the current root filesystem.
+# Argument: root filesystem type ("zfs" or "ufs")
+# ZFS: parses zpool status to find leaf vdev member disks; strips partition suffix.
+# Handles diskid/ and gptid/ aliases via realpath.
+# UFS: parses mount --libxo json root "special" device; strips /dev/ prefix and suffix.
+# Returns 1 on failure or unknown type.
+efi_root_disks() {
+ local root_type="$1"
+
+ case "$root_type" in
+ zfs)
+ # Extract pool name from root "special" field (e.g. "zroot/ROOT/default" -> "zroot")
+ local pool_name
+ pool_name=$(mount --libxo json 2>/dev/null | tr '}' '\n' | awk -F'"' '
+ /"node":"\/"/ {
+ for (i = 1; i <= NF; i++)
+ if ($i == "special") { print $(i+2); exit }
+ }
+ ' | cut -d/ -f1)
+
+ if [ -z "$pool_name" ]; then
+ _efi_warn "efi_root_disks: cannot determine ZFS pool name from mount output"
+ return 1
+ fi
+
+ local zpool_out
+ zpool_out=$(zpool status "$pool_name" 2>/dev/null) || {
+ _efi_warn "efi_root_disks: zpool status ${pool_name} failed"
+ return 1
+ }
+
+ # Parse leaf vdev members from the config: section of zpool status.
+ # Skips pool-level metadata (state:, scan:) by starting after "config:".
+ # Excludes the pool name itself, the NAME header, and vdev group names
+ # (mirror-0, raidz1-0, etc.). Handles raw device names (nda0p4),
+ # GEOM labels (gpt/OptBzfs), and aliases (diskid/DISK-xxx, gptid/GUID).
+ printf '%s\n' "$zpool_out" | awk -v pool="$pool_name" '
+ /^config:/ { in_config = 1; next }
+ in_config &&
+ $1 != pool && $1 !~ /^NAME$/ &&
+ $2 ~ /^(ONLINE|DEGRADED|FAULTED|OFFLINE|REMOVED|UNAVAIL)$/ &&
+ $1 !~ /-[0-9]+$/ {
+ print $1
+ }
+ ' | while IFS= read -r vdev; do
+ case "$vdev" in
+ diskid/*|gptid/*|gpt/*)
+ # Resolve alias to real device. On some FreeBSD versions
+ # GEOM label paths are device nodes, not symlinks, so
+ # realpath(1) returns the path unchanged. Fall back to
+ # glabel status to find the backing partition.
+ local real
+ real=$(realpath "/dev/${vdev}" 2>/dev/null) || continue
+ case "$real" in
+ /dev/gpt/*|/dev/diskid/*|/dev/gptid/*)
+ local component
+ component=$(glabel status 2>/dev/null | awk \
+ -v lbl="${vdev}" \
+ '$1 == lbl { print $NF; exit }')
+ [ -n "$component" ] || continue
+ real="/dev/${component}"
+ ;;
+ esac
+ real="${real#/dev/}"
+ real=$(printf '%s' "$real" | sed 's/[sp][0-9]*$//')
+ [ -n "$real" ] && echo "$real"
+ ;;
+ *)
+ # Strip partition suffix
+ local disk
+ disk=$(printf '%s' "$vdev" | sed 's/[sp][0-9]*$//')
+ [ -n "$disk" ] && echo "$disk"
+ ;;
+ esac
+ done | sort -u
+ ;;
+
+ ufs)
+ # Get root device from mount --libxo json "special" field
+ local root_dev
+ root_dev=$(mount --libxo json 2>/dev/null | tr '}' '\n' | awk -F'"' '
+ /"node":"\/"/ {
+ for (i = 1; i <= NF; i++)
+ if ($i == "special") { print $(i+2); exit }
+ }
+ ')
+
+ if [ -z "$root_dev" ]; then
+ _efi_warn "efi_root_disks: cannot determine UFS root device from mount output"
+ return 1
+ fi
+
+ # Strip /dev/ prefix
+ root_dev="${root_dev#/dev/}"
+
+ # Resolve GEOM label and alias paths to real device names.
+ # mount --libxo json reports the device as the kernel's geom provider
+ # name (e.g. "gpt/PBaseUFS", "ufs/rootfs", "diskid/DISK-XXXpN"),
+ # which gpart cannot accept as a disk argument. realpath(1) resolves
+ # the label when /dev/X is a symlink; on newer kernels these are device
+ # nodes so realpath returns the path unchanged. glabel status is
+ # used as a fallback to find the backing partition in that case.
+ case "$root_dev" in
+ gpt/*|diskid/*|gptid/*|ufs/*)
+ local real
+ real=$(realpath "/dev/${root_dev}" 2>/dev/null) || {
+ _efi_warn "efi_root_disks: cannot resolve /dev/${root_dev} to real device"
+ return 1
+ }
+ case "$real" in
+ /dev/gpt/*|/dev/diskid/*|/dev/gptid/*|/dev/ufs/*)
+ local component
+ component=$(glabel status 2>/dev/null | awk \
+ -v lbl="${root_dev}" \
+ '$1 == lbl { print $NF; exit }')
+ if [ -z "$component" ]; then
+ _efi_warn "efi_root_disks: cannot find backing device for /dev/${root_dev}"
+ return 1
+ fi
+ real="/dev/${component}"
+ ;;
+ esac
+ root_dev="${real#/dev/}"
+ ;;
+ esac
+
+ # Strip partition suffix (GPT: pN; MBR slice: sN; MBR BSD: sNa).
+ # Require at least one digit after [sp] so bare trailing letters in
+ # GEOM label names (e.g. "rootfs") are never accidentally stripped.
+ root_dev=$(printf '%s' "$root_dev" | sed 's/[sp][0-9]\{1,\}[a-z]\{0,1\}$//')
+ [ -n "$root_dev" ] && echo "$root_dev"
+ ;;
+
+ *)
+ _efi_warn "efi_root_disks: unknown root type '${root_type}'"
+ return 1
+ ;;
+ esac
+}
+
+# ============================================================
+# BOOT-SPECIFIC PARTITION DISCOVERY
+# ============================================================
+
+# Identify EFI System Partitions belonging to the current system only.
+# Uses the union of:
+# 1. EFI BootCurrent NVRAM variable -> PARTUUID -> gpart list rawuuid match
+# 2. Root filesystem disk(s) via efi_root_disks
+# Outputs one "disk part_index scheme" tuple per line.
+# Returns 1 if no ESPs found.
+efi_boot_esps() {
+ local root_type
+ root_type=$(efi_root_fs_type)
+
+ # ── Step 1: Try efibootmgr to identify BootCurrent disk ──────────────────
+ local boot_partuuid=""
+ if command -v efibootmgr >/dev/null 2>&1; then
+ local efibm_out
+ if ! [ -c "${_EFI_DEV_EFI}" ]; then
+ _efi_verb "EFIRT (/dev/efi) unavailable — skipping BootCurrent NVRAM lookup"
+ efibm_out=""
+ else
+ efibm_out=$(efibootmgr -v 2>/dev/null) || efibm_out=""
+ fi
+
+ local current_num
+ current_num=$(printf '%s\n' "$efibm_out" | sed -n 's/^BootCurrent: *//p')
+
+ if [ -n "$current_num" ]; then
+ # Extract PARTUUID from HD(N,GPT,<UUID>,...) for the BootCurrent entry.
+ # Two efibootmgr -v formats exist:
+ # Inline: "+Boot0004* desc<TAB>HD(1,GPT,UUID,...)/File(...)"
+ # dp-line: "+Boot0004* desc\n dp: HD(1,GPT,UUID,...)/File(...)"
+ # Use awk to match the entry line then scan forward for HD(N,GPT,...),
+ # stopping if the next boot entry is reached.
+ boot_partuuid=$(printf '%s\n' "$efibm_out" | awk \
+ -v pat="Boot${current_num}[* ]" '
+ $0 ~ pat { found = 1 }
+ found && /HD\([0-9]*,GPT,/ {
+ s = $0
+ sub(/.*HD\([0-9]*,GPT,/, "", s)
+ sub(/,.*/, "", s)
+ if (s != "") { print s; exit }
+ }
+ found && /^[+ ]Boot[0-9]/ && $0 !~ pat { exit }
+ ')
+ fi
+ fi
+
+ # ── Step 2: Get root filesystem disks ────────────────────────────────────
+ local root_disk_list=""
+ root_disk_list=$(efi_root_disks "$root_type") || root_disk_list=""
+
+ # ── Step 3: Build candidate_disks ────────────────────────────────────────
+ local candidate_disks=""
+ local _boot_disks=""
+
+ if [ -n "$boot_partuuid" ]; then
+ # Enumerate all disks and check gpart list rawuuid fields
+ local all_disks
+ all_disks=$(sysctl -n kern.disks 2>/dev/null | tr ' ' '\n') || all_disks=""
+
+ local d
+ for d in $all_disks; do
+ [ -z "$d" ] && continue
+ local glist_out
+ glist_out=$(gpart list "$d" 2>/dev/null) || continue
+
+ # Case-insensitive UUID comparison using awk tolower()
+ local matched
+ matched=$(printf '%s\n' "$glist_out" | awk -v uuid="$boot_partuuid" '
+ tolower($0) ~ "rawuuid:" {
+ # $NF is the UUID value
+ if (tolower($NF) == tolower(uuid)) { print "yes"; exit }
+ }
+ ')
+ if [ "$matched" = "yes" ]; then
+ _boot_disks="${_boot_disks}${d}
+"
+ fi
+ done
+ fi
+
+ if [ -n "$_boot_disks" ]; then
+ candidate_disks="$_boot_disks"
+
+ # Check whether the identified boot disk(s) overlap with the root
+ # filesystem disk(s). No overlap means split-media or a dedicated
+ # boot disk: the root disk(s) are not part of this system's boot
+ # path. In that case, restrict updates to the boot disk only —
+ # adding root disks could update an ESP on shared media that belongs
+ # to another system, not the one currently running.
+ #
+ # Overlap (the common case, including ZFS mirrors where the boot disk
+ # is also a pool member): add all root disks so every mirror member's
+ # ESP is kept in sync.
+ local _overlap=0
+ local _bd
+ for _bd in $_boot_disks; do
+ if printf '%s\n' "$root_disk_list" | grep -qx "$_bd"; then
+ _overlap=1
+ break
+ fi
+ done
+
+ if [ "$_overlap" = "1" ]; then
+ # Normal or mirror: boot disk is a root disk — include all root disks.
+ if [ -n "$root_disk_list" ]; then
+ candidate_disks="${candidate_disks}${root_disk_list}
+"
+ fi
+ else
+ # Split-media or dedicated boot disk: restrict to boot disk only.
+ _efi_verb "Boot disk differs from root filesystem disk(s) — restricting ESP updates to boot disk"
+ fi
+ else
+ # BootCurrent PARTUUID not matched — fall back to root disks only.
+ if [ -n "$root_disk_list" ]; then
+ candidate_disks="${root_disk_list}
+"
+ fi
+ fi
+
+ # ── Step 4: Deduplicate candidate_disks ──────────────────────────────────
+ local deduped_disks
+ deduped_disks=$(printf '%s' "$candidate_disks" | sort -u | grep -v '^$') || deduped_disks=""
+
+ if [ -z "$deduped_disks" ]; then
+ _efi_warn "efi_boot_esps: cannot determine boot/root disks"
+ return 1
+ fi
+
+ # ── Step 5: Scan each candidate disk for ESP partitions ──────────────────
+ local found=0
+ local disk
+ for disk in $deduped_disks; do
+ [ -z "$disk" ] && continue
+ local gpart_out scheme parts
+ gpart_out=$(gpart show "$disk" 2>/dev/null) || continue
+
+ scheme=$(printf '%s\n' "$gpart_out" | awk '$1 == "=>" { print $5; exit }')
+
+ case "$scheme" in
+ GPT)
+ parts=$(printf '%s\n' "$gpart_out" | \
+ awk -v d="$disk" '$4 == "efi" { print d, $3, "GPT" }')
+ ;;
+ MBR)
+ parts=$(printf '%s\n' "$gpart_out" | \
+ awk -v d="$disk" '$4 == "fat32lba" || $4 == "fat32" || $4 == "efi" || $4 == "!12" || $4 == "!ef" { print d, $3, "MBR" }')
+ ;;
+ *)
+ _efi_verb "efi_boot_esps: skipping ${disk}: unrecognised scheme '${scheme}'"
+ continue
+ ;;
+ esac
+
+ if [ -n "$parts" ]; then
+ printf '%s\n' "$parts"
+ found=1
+ fi
+ done
+
+ [ "$found" -eq 0 ] && return 1
+ return 0
+}
+
+# Identify freebsd-boot partitions belonging to the current system only.
+# Uses root filesystem disk(s) via efi_root_disks.
+# Outputs one "disk part_index" tuple per line.
+# Returns 1 if root disks cannot be determined or no freebsd-boot partitions found.
+efi_boot_bios_parts() {
+ local root_type
+ root_type=$(efi_root_fs_type)
+
+ local root_disks
+ root_disks=$(efi_root_disks "$root_type") || {
+ _efi_warn "efi_boot_bios_parts: cannot determine root filesystem disks"
+ return 1
+ }
+
+ if [ -z "$root_disks" ]; then
+ _efi_warn "efi_boot_bios_parts: root disk list is empty"
+ return 1
+ fi
+
+ local found=0
+ local disk
+ for disk in $root_disks; do
+ [ -z "$disk" ] && continue
+ local parts
+ parts=$(gpart show "$disk" 2>/dev/null | \
+ awk -v d="$disk" '$4 == "freebsd-boot" { print d, $3 }') || true
+ if [ -n "$parts" ]; then
+ printf '%s\n' "$parts"
+ found=1
+ fi
+ done
+
+ [ "$found" -eq 0 ] && return 1
+ return 0
+}
+
+# ============================================================
+# ESP MOUNTING
+# ============================================================
+
+# Module-level state for the current ESP mount operation.
+_efi_esp_mp="" # Current ESP mountpoint
+_efi_esp_did_mount=0 # 1 if we mounted it (we must unmount)
+_efi_esp_is_real=0 # 1 if esp_mp points to a real accessible ESP
+ # (pre-mounted or actually mounted); 0 in dry-run
+ # with an empty tmpdir
+_efi_tmp_mounts="" # All temp mounts we created (space-separated)
+
+# Remove all temporary mounts created by this script. Called from EXIT trap.
+efi_cleanup_mounts() {
+ local mp
+ for mp in ${_efi_tmp_mounts}; do
+ _efi_verb "Cleanup: unmounting ${mp}"
+ umount "$mp" 2>/dev/null || true
+ rmdir "$mp" 2>/dev/null || true
+ done
+ _efi_tmp_mounts=""
+}
+
+# Return the current mountpoint for a device, or empty string if not mounted.
+efi_esp_mountpoint() {
+ local device="$1"
+ case "$device" in /dev/*) ;; *) device="/dev/${device}" ;; esac
+
+ local mount_json
+ mount_json=$(mount --libxo json 2>/dev/null)
+
+ # First: direct device path match (common case — GPT disks, any system
+ # where mount reports the raw device node as special).
+ local result
+ result=$(printf '%s\n' "$mount_json" | tr '}' '\n' | awk -F'"' \
+ -v dev="$device" '
+ {
+ special = ""; node = ""
+ for (i = 1; i <= NF; i++) {
+ if ($i == "special") special = $(i+2)
+ if ($i == "node") node = $(i+2)
+ }
+ if (special == dev && node != "") { print node; exit }
+ }
+ ')
+ [ -n "$result" ] && { echo "$result"; return; }
+
+ # Second: GEOM label resolution. mount(8) may report the device under
+ # a GEOM label path rather than the raw device node — for example,
+ # /dev/msdosfs/EFI when a FAT ESP with volume label "EFI" is mounted.
+ # Use glabel status to resolve each label to its backing component and
+ # compare against the requested device.
+ local dev_base="${device#/dev/}"
+ local glabel_out
+ glabel_out=$(glabel status 2>/dev/null)
+ result=$(printf '%s\n' "$mount_json" | tr '}' '\n' | awk -F'"' '
+ {
+ special = ""; node = ""
+ for (i = 1; i <= NF; i++) {
+ if ($i == "special") special = $(i+2)
+ if ($i == "node") node = $(i+2)
+ }
+ if (special ~ "^/dev/" && node != "") print special " " node
+ }
+ ' | while read -r special node; do
+ lbl="${special#/dev/}"
+ component=$(printf '%s\n' "$glabel_out" | \
+ awk -v l="$lbl" '$1 == l { print $NF; exit }')
+ [ "$component" = "$dev_base" ] && { echo "$node"; break; }
+ done)
+ [ -n "$result" ] && echo "$result"
+}
+
+# Mount the EFI System Partition at a temporary directory.
+# Sets _efi_esp_mp and _efi_esp_did_mount.
+# Returns 0 on success, 1 on failure.
+# $3: partition scheme — "GPT" (default, uses pN suffix) or "MBR" (uses sN suffix)
+efi_mount_esp() {
+ local disk="$1"
+ local part_index="$2"
+ local scheme="${3:-GPT}"
+ local device
+ case "$scheme" in
+ MBR) device="/dev/${disk}s${part_index}" ;;
+ *) device="/dev/${disk}p${part_index}" ;;
+ esac
+
+ _efi_esp_mp=""
+ _efi_esp_did_mount=0
+ _efi_esp_is_real=0
+
+ # Reuse an existing mount if the device is already mounted.
+ local existing
+ existing=$(efi_esp_mountpoint "$device")
+ if [ -n "$existing" ]; then
+ _efi_verb "ESP ${device} already mounted at ${existing}"
+ _efi_esp_mp="$existing"
+ _efi_esp_did_mount=0
+ _efi_esp_is_real=1
+ return 0
+ fi
+
+ local tmp_mp
+ tmp_mp=$(mktemp -d 2>/dev/null) || {
+ _efi_err "Cannot create temporary mount directory"
+ return 1
+ }
+
+ if [ "${EFI_DRY_RUN}" = "1" ]; then
+ _efi_info "[DRY RUN] Would mount ${device} at ${tmp_mp}"
+ _efi_esp_mp="$tmp_mp"
+ _efi_esp_did_mount=1
+ _efi_esp_is_real=0
+ _efi_tmp_mounts="${_efi_tmp_mounts} ${tmp_mp}"
+ return 0
+ fi
+
+ if ! mount_msdosfs -o noexec -o nosuid "${device}" "${tmp_mp}" 2>/dev/null; then
+ _efi_err "Failed to mount ESP ${device} at ${tmp_mp}"
+ rmdir "$tmp_mp" 2>/dev/null
+ return 1
+ fi
+
+ _efi_esp_mp="$tmp_mp"
+ _efi_esp_did_mount=1
+ _efi_esp_is_real=1
+ _efi_tmp_mounts="${_efi_tmp_mounts} ${tmp_mp}"
+ _efi_verb "Mounted ${device} at ${tmp_mp}"
+ return 0
+}
+
+# Unmount the ESP if this script mounted it; clears state variables.
+efi_unmount_esp() {
+ if [ "${_efi_esp_did_mount}" = "1" ] && [ -n "${_efi_esp_mp}" ]; then
+ if [ "${EFI_DRY_RUN}" != "1" ]; then
+ _efi_verb "Unmounting ESP at ${_efi_esp_mp}"
+ umount "${_efi_esp_mp}" 2>/dev/null || \
+ _efi_warn "Failed to unmount ${_efi_esp_mp}"
+ rmdir "${_efi_esp_mp}" 2>/dev/null || true
+ fi
+ # Remove from cleanup list
+ _efi_tmp_mounts=$(printf '%s\n' ${_efi_tmp_mounts} | \
+ grep -Fxv "${_efi_esp_mp}" | tr '\n' ' ')
+ fi
+ _efi_esp_mp=""
+ _efi_esp_did_mount=0
+ _efi_esp_is_real=0
+}
+
+# ============================================================
+# LOADER FINGERPRINTING
+# ============================================================
+
+# Returns 0 if the given file appears to be a FreeBSD EFI loader binary.
+#
+# Primary check: bootprog_info string embedded by newvers.sh in all FreeBSD
+# loaders since FreeBSD 11 — "FreeBSD/<arch> EFI, Revision N.N". This
+# pattern is specific enough to eliminate false positives from other OSes.
+#
+# Fallback: multi-string heuristic requiring _EFI_FINGERPRINT_THRESHOLD of
+# "FreeBSD", "loader.efi", "boot/lua" — covers older binaries that predate
+# the bootprog_info format.
+efi_is_freebsd_loader() {
+ local file="$1"
+
+ [ -f "$file" ] || return 1
+ [ -s "$file" ] || return 1 # must be non-empty
+
+ # Primary: match the bootprog_info pattern.
+ if strings "$file" 2>/dev/null | grep -qE 'FreeBSD/[^ ]+ EFI,'; then
+ _efi_verb "Fingerprint '${file}': bootprog_info match"
+ return 0
+ fi
+
+ # Fallback: multi-string heuristic for binaries without bootprog_info.
+ local matches=0 sig
+ for sig in "FreeBSD" "loader.efi" "boot/lua"; do
+ strings "$file" 2>/dev/null | grep -qF "$sig" && \
+ matches=$((matches + 1))
+ done
+
+ _efi_verb "Fingerprint '${file}': ${matches}/${_EFI_FINGERPRINT_THRESHOLD} heuristic match(es)"
+ [ "$matches" -ge "${_EFI_FINGERPRINT_THRESHOLD}" ]
+}
+
+# ============================================================
+# SPACE CHECK
+# ============================================================
+
+# Returns 0 if the ESP has enough free space for at least 2 copies of
+# loader.efi (current + new) plus a 64 KiB safety margin.
+efi_check_space() {
+ local esp_mount="$1"
+
+ local src_size
+ src_size=$(stat -f '%z' "${EFI_LOADER_SRC}" 2>/dev/null) || {
+ _efi_err "Cannot stat ${EFI_LOADER_SRC}"
+ return 1
+ }
+
+ if [ "${EFI_DRY_RUN}" = "1" ] && [ "${_efi_esp_is_real:-0}" != "1" ]; then
+ _efi_verb "[DRY RUN] Space check skipped (ESP not mounted)"
+ return 0
+ fi
+
+ local avail_kb
+ avail_kb=$(df -k "$esp_mount" 2>/dev/null | awk 'NR==2 { print $4 }') || {
+ _efi_err "Cannot determine free space on ${esp_mount}"
+ return 1
+ }
+
+ local avail_bytes=$((avail_kb * 1024))
+ # Allow 2× the loader size (temp file + final) plus 64 KiB overhead
+ local required=$((src_size * 2 + 65536))
+
+ if [ "$avail_bytes" -lt "$required" ]; then
+ _efi_err "Insufficient space on ESP: ${avail_bytes} B available, ${required} B needed"
+ _efi_err "Free space on the EFI System Partition and retry"
+ return 1
+ fi
+
+ _efi_verb "ESP space OK: ${avail_bytes} B available, ${required} B needed"
+ return 0
+}
+
+# ============================================================
+# FILE OPERATIONS
+# ============================================================
+
+# Copy src to dst using a temp file + rename to minimise the corruption window
+# on the non-journaled FAT32 filesystem.
+efi_safe_copy() {
+ local src="$1"
+ local dst="$2"
+ local tmp="${dst}.new"
+
+ # _efi_copy_wrote: set to 1 if a write occurred, 0 if skipped (already
+ # current) or dry-run. Callers use this to count actual writes.
+ _efi_copy_wrote=0
+
+ if [ "${EFI_DRY_RUN}" = "1" ]; then
+ _efi_info "[DRY RUN] Would update: ${dst}"
+ return 0
+ fi
+
+ # Skip the copy if the destination already matches the source.
+ # Avoids unnecessary FAT32 writes on repeated freebsd-update install runs.
+ if [ -f "$dst" ] && cmp -s "$src" "$dst" 2>/dev/null; then
+ _efi_verb "Already up to date: ${dst}"
+ return 0
+ fi
+
+ cp -f "$src" "$tmp" 2>/dev/null || {
+ _efi_err "Copy failed: ${src} → ${tmp}"
+ rm -f "$tmp" 2>/dev/null
+ return 1
+ }
+ sync 2>/dev/null || true
+
+ mv -f "$tmp" "$dst" 2>/dev/null || {
+ _efi_err "Rename failed: ${tmp} → ${dst}"
+ rm -f "$tmp" 2>/dev/null
+ return 1
+ }
+ sync 2>/dev/null || true
+
+ _efi_copy_wrote=1
+ _efi_info "Updated: ${dst}"
+ return 0
+}
+
+# ============================================================
+# EFI PATH MANAGEMENT
+# ============================================================
+
+# Ensure a FreeBSD NVRAM boot entry pointing to /EFI/FreeBSD/loader.efi exists.
+# Non-fatal: many systems boot fine without an explicit NVRAM entry (fallback
+# path covers them), efibootmgr may not be installed, or EFIRT (/dev/efi) may
+# be unavailable (custom kernel without options EFIRT, or i386/armv7/riscv64).
+efi_ensure_nvram_entry() {
+ local esp_mount="$1"
+ local freebsd_loader_abs="$2" # absolute path on mounted ESP
+
+ if [ "${EFI_NVRAM_UPDATE}" != "1" ]; then
+ _efi_verb "EFI_NVRAM_UPDATE=0 — skipping NVRAM boot entry management"
+ return 0
+ fi
+
+ command -v efibootmgr >/dev/null 2>&1 || {
+ _efi_warn "efibootmgr not found — cannot verify NVRAM boot entry"
+ _efi_warn "Ensure UEFI NVRAM has a FreeBSD entry pointing to the loader"
+ return 0
+ }
+ [ -c "${_EFI_DEV_EFI}" ] || {
+ _efi_verb "EFIRT (/dev/efi) unavailable — skipping NVRAM boot entry management"
+ return 0
+ }
+
+ # Derive EFI-style path (relative to ESP root, backslashes)
+ local rel_path="${freebsd_loader_abs#${esp_mount}}"
+ local efi_path
+ efi_path=$(echo "$rel_path" | tr '/' '\\')
+
+ # Look for an existing FreeBSD entry that references /EFI/FreeBSD/loader.efi
+ local existing
+ existing=$(efibootmgr -v 2>/dev/null | \
+ grep -i "FreeBSD" | grep -i "loader\.efi") || true
+
+ if [ -n "$existing" ]; then
+ _efi_verb "NVRAM FreeBSD entry already exists"
+ return 0
+ fi
+
+ _efi_info "Adding NVRAM boot entry: FreeBSD → ${efi_path}"
+ if [ "${EFI_DRY_RUN}" = "1" ]; then
+ _efi_info "[DRY RUN] efibootmgr -a -c -l '${freebsd_loader_abs}' -L FreeBSD"
+ return 0
+ fi
+
+ # FreeBSD efibootmgr -l expects a Unix path on the mounted ESP,
+ # not an EFI backslash path. It resolves the partition and EFI
+ # device path itself.
+ efibootmgr -a -c -l "$freebsd_loader_abs" -L "FreeBSD" >/dev/null 2>&1 || {
+ _efi_warn "efibootmgr failed to create NVRAM entry"
+ _efi_warn "Run as root: efibootmgr -a -c -l '${freebsd_loader_abs}' -L FreeBSD"
+ return 0 # still non-fatal
+ }
+ _efi_info "NVRAM boot entry created"
+}
+
+# Update all FreeBSD EFI loaders on a mounted ESP, and create the
+# OS-specific /EFI/FreeBSD/loader.efi path + NVRAM entry if absent.
+efi_update_esp() {
+ local esp_mount="$1"
+ local fallback_binary="$2" # e.g. "BOOTx64.efi"
+ local disk="$3"
+ local part_index="$4"
+ local scheme="${5:-GPT}"
+ local device
+ case "$scheme" in
+ MBR) device="/dev/${disk}s${part_index}" ;;
+ *) device="/dev/${disk}p${part_index}" ;;
+ esac
+
+ local updated=0 errors=0
+
+ if [ "${EFI_DRY_RUN}" = "1" ] && [ "${_efi_esp_is_real:-0}" != "1" ]; then
+ _efi_info "[DRY RUN] ESP not mounted — existing file/directory detection skipped; output reflects a blank ESP"
+ fi
+
+ # ── 1. OS-specific path: /EFI/FreeBSD/loader.efi ─────────────────────────
+ #
+ # FAT32 is case-insensitive. Use case-insensitive find so we handle ESPs
+ # created by installers that chose a different capitalisation.
+
+ local freebsd_dir freebsd_loader
+
+ local found_dir
+ found_dir=$(find "${esp_mount}" -maxdepth 3 -type d \
+ -iname "FreeBSD" 2>/dev/null | head -1)
+
+ if [ -n "$found_dir" ]; then
+ freebsd_dir="$found_dir"
+ freebsd_loader="${freebsd_dir}/loader.efi"
+ efi_safe_copy "${EFI_LOADER_SRC}" "$freebsd_loader" || errors=$((errors + 1))
+ [ "${_efi_copy_wrote:-0}" = "1" ] && updated=$((updated + 1))
+ else
+ # Directory does not exist — create it (the "promote" step)
+ freebsd_dir="${esp_mount}/EFI/FreeBSD"
+ freebsd_loader="${freebsd_dir}/loader.efi"
+ _efi_info "Creating ${freebsd_dir}/ and installing loader"
+ if [ "${EFI_DRY_RUN}" != "1" ]; then
+ mkdir -p "$freebsd_dir" 2>/dev/null || {
+ _efi_err "Cannot create directory: ${freebsd_dir}"
+ errors=$((errors + 1))
+ }
+ fi
+ if [ "$errors" -eq 0 ]; then
+ efi_safe_copy "${EFI_LOADER_SRC}" "$freebsd_loader" || errors=$((errors + 1))
+ [ "${_efi_copy_wrote:-0}" = "1" ] && updated=$((updated + 1))
+ fi
+ fi
+
+ # ── 2. Fallback path: /EFI/BOOT/<arch>.efi ───────────────────────────────
+ #
+ # Only update if the file already exists AND fingerprints as a FreeBSD loader.
+ # This protects other OSes that may own this path on a shared ESP.
+
+ local boot_dir fallback_file=""
+ local found_boot
+ found_boot=$(find "${esp_mount}/EFI" -maxdepth 1 -type d \
+ -iname "BOOT" 2>/dev/null | head -1) || true
+
+ if [ -n "$found_boot" ]; then
+ boot_dir="$found_boot"
+ fallback_file=$(find "$boot_dir" -maxdepth 1 -type f \
+ -iname "$fallback_binary" 2>/dev/null | head -1) || true
+ fi
+
+ if [ -n "$fallback_file" ]; then
+ if efi_is_freebsd_loader "$fallback_file"; then
+ efi_safe_copy "${EFI_LOADER_SRC}" "$fallback_file" || errors=$((errors + 1))
+ [ "${_efi_copy_wrote:-0}" = "1" ] && updated=$((updated + 1))
+ else
+ _efi_warn "$(basename "$fallback_file") at ${fallback_file} does not fingerprint as FreeBSD — skipping"
+ _efi_warn "Another OS may own this path; FreeBSD will boot via /EFI/FreeBSD/"
+ fi
+ else
+ # No fallback binary at all — create one (common on fresh or BIOS-migrated installs)
+ if [ -z "$found_boot" ]; then
+ boot_dir="${esp_mount}/EFI/BOOT"
+ fi
+ fallback_file="${boot_dir}/${fallback_binary}"
+ _efi_info "Installing fallback loader: ${fallback_file}"
+ if [ "${EFI_DRY_RUN}" != "1" ]; then
+ mkdir -p "$boot_dir" 2>/dev/null || {
+ _efi_err "Cannot create directory: ${boot_dir}"
+ errors=$((errors + 1))
+ }
+ fi
+ if [ "$errors" -eq 0 ]; then
+ efi_safe_copy "${EFI_LOADER_SRC}" "$fallback_file" || errors=$((errors + 1))
+ [ "${_efi_copy_wrote:-0}" = "1" ] && updated=$((updated + 1))
+ fi
+ fi
+
+ # ── 3. NVRAM entry ────────────────────────────────────────────────────────
+ efi_ensure_nvram_entry "$esp_mount" "$freebsd_loader"
+
+ # ── Summary ────────────────────────────────────────────────────────────────
+ [ "$updated" -gt 0 ] && \
+ _efi_info "Updated ${updated} EFI loader file(s) on ${device}"
+ [ "$errors" -gt 0 ] && {
+ _efi_warn "${errors} error(s) updating ESP on ${device}"
+ return 1
+ }
+ return 0
+}
+
+# ============================================================
+# BIOS BOOTCODE
+# ============================================================
+
+# Write BIOS-mode GPT bootcode to a freebsd-boot partition.
+# Selects gptzfsboot or gptboot based on the root filesystem type.
+efi_update_bios_bootcode() {
+ local disk="$1"
+ local part_index="$2"
+
+ local root_fs bootprog
+ root_fs=$(efi_root_fs_type)
+
+ case "$root_fs" in
+ zfs) bootprog="${EFI_BIOS_ZFS_BOOT}" ;;
+ ufs) bootprog="${EFI_BIOS_UFS_BOOT}" ;;
+ *)
+ _efi_warn "Unknown root FS '${root_fs}' on ${disk} — skipping BIOS bootcode"
+ return 0
+ ;;
+ esac
+
+ for f in "${EFI_BIOS_PMBR}" "$bootprog"; do
+ [ -f "$f" ] || {
+ _efi_warn "BIOS boot file not found: ${f}"
+ return 1
+ }
+ done
+
+ _efi_info "Updating BIOS bootcode on ${disk}p${part_index} (${root_fs})"
+ if [ "${EFI_DRY_RUN}" = "1" ]; then
+ _efi_info "[DRY RUN] gpart bootcode -b ${EFI_BIOS_PMBR} -p ${bootprog} -i ${part_index} ${disk}"
+ return 0
+ fi
+
+ gpart bootcode -b "${EFI_BIOS_PMBR}" \
+ -p "$bootprog" \
+ -i "$part_index" \
+ "$disk" 2>/dev/null || {
+ _efi_err "gpart bootcode failed on ${disk}p${part_index}"
+ return 1
+ }
+
+ _efi_info "BIOS bootcode updated on ${disk}p${part_index}"
+}
+
+# ============================================================
+# MAIN ORCHESTRATION
+# ============================================================
+
+# Update all bootloaders on all boot disks.
+# Returns 0 if all updates succeeded, 1 if any failed.
+update_bootloaders() {
+ local total_errors=0
+
+ # ── Prerequisites ──────────────────────────────────────────────────────────
+ local rc
+ efi_check_prerequisites; rc=$?
+ case $rc in
+ 0) ;; # all good
+ 2) return 0 ;; # in jail — skip silently
+ *) return 1 ;; # fatal
+ esac
+
+ # ── Boot method and architecture ───────────────────────────────────────────
+ local boot_method fallback_binary=""
+ boot_method=$(efi_boot_method)
+ _efi_info "Boot method detected: ${boot_method}"
+
+ if [ "$boot_method" = "UEFI" ]; then
+ fallback_binary=$(efi_fallback_binary) || {
+ _efi_warn "Cannot determine EFI binary name — EFI partition update skipped"
+ # Continue: BIOS bootcode on freebsd-boot partitions can still be updated.
+ }
+ fi
+
+ # Register cleanup so temp mounts are removed even on error or signal.
+ trap 'efi_cleanup_mounts' EXIT INT TERM
+
+ # ── EFI System Partitions — scoped to current system's boot/root disks ────
+ if [ "$boot_method" = "UEFI" ] && [ -n "$fallback_binary" ]; then
+ local esp_list
+ esp_list=$(efi_boot_esps) || {
+ _efi_warn "No EFI System Partitions found for this system's boot/root disks"
+ _efi_warn "If using hardware RAID or an unusual topology, update the bootloader manually"
+ }
+
+ if [ -n "$esp_list" ]; then
+ local esp_disk esp_pidx esp_scheme
+ while IFS=' ' read -r esp_disk esp_pidx esp_scheme; do
+ _efi_info "Processing EFI partition: ${esp_disk} partition ${esp_pidx} (${esp_scheme})"
+
+ if ! efi_mount_esp "$esp_disk" "$esp_pidx" "$esp_scheme"; then
+ _efi_err "Skipping ${esp_disk} partition ${esp_pidx} — mount failed"
+ total_errors=$((total_errors + 1))
+ continue
+ fi
+
+ local esp_mp="${_efi_esp_mp}"
+
+ if ! efi_check_space "$esp_mp"; then
+ total_errors=$((total_errors + 1))
+ efi_unmount_esp
+ continue
+ fi
+
+ efi_update_esp "$esp_mp" "$fallback_binary" "$esp_disk" "$esp_pidx" "$esp_scheme" || \
+ total_errors=$((total_errors + 1))
+
+ efi_unmount_esp
+ done <<_ESPS_
+$esp_list
+_ESPS_
+ fi
+ fi
+
+ # ── BIOS freebsd-boot partitions — scoped to root filesystem disks ────────
+ local bios_list
+ bios_list=$(efi_boot_bios_parts) || true
+
+ if [ -n "$bios_list" ]; then
+ local bios_disk bios_pidx
+ while IFS=' ' read -r bios_disk bios_pidx; do
+ efi_update_bios_bootcode "$bios_disk" "$bios_pidx" || \
+ total_errors=$((total_errors + 1))
+ done <<_BIOS_
+$bios_list
+_BIOS_
+ fi
+
+ # ── Final status ───────────────────────────────────────────────────────────
+ if [ "$total_errors" -gt 0 ]; then
+ _efi_warn "Bootloader update finished with ${total_errors} error(s)"
+ _efi_warn "Review the messages above and update any failed bootloaders manually"
+ return 1
+ fi
+
+ if [ "${EFI_DRY_RUN}" = "1" ]; then
+ _efi_info "[DRY RUN] Bootloader update complete (no changes made)"
+ else
+ _efi_info "Bootloader update complete"
+ fi
+ return 0
+}
+
+# ============================================================
+# STANDALONE ENTRY POINT
+# ============================================================
+
+# When executed directly (not sourced), parse arguments and run.
+_efi_script_name="${0##*/}"
+if [ "${_efi_script_name}" = "efi_bootloader_update.sh" ]; then
+ while [ $# -gt 0 ]; do
+ case "$1" in
+ --dry-run|-n) EFI_DRY_RUN=1 ;;
+ --verbose|-v) EFI_VERBOSE=1 ;;
+ --help|-h)
+ cat <<EOF
+Usage: ${_efi_script_name} [OPTIONS]
+
+Updates the FreeBSD EFI bootloader on the EFI System Partition(s) and the
+BIOS bootcode on freebsd-boot partition(s) for all disks participating in
+the root filesystem.
+
+Options:
+ -n, --dry-run Show what would be done without making any changes
+ -v, --verbose Enable debug/verbose output
+ -h, --help Show this help message
+
+Environment:
+ EFI_LOADER_SRC Source loader path (default: /boot/loader.efi)
+ EFI_DRY_RUN 1 = dry-run mode
+ EFI_VERBOSE 1 = verbose/debug mode
+EOF
+ exit 0
+ ;;
+ *)
+ echo "${_efi_script_name}: unknown option: $1" >&2
+ exit 1
+ ;;
+ esac
+ shift
+ done
+
+ update_bootloaders
+ exit $?
+fi
diff --git a/usr.sbin/freebsd-update/freebsd-update.8 b/usr.sbin/freebsd-update/freebsd-update.8
--- a/usr.sbin/freebsd-update/freebsd-update.8
+++ b/usr.sbin/freebsd-update/freebsd-update.8
@@ -203,6 +203,22 @@
Returns exit code 2 if there are no updates to install and the
.Cm fetch
command wasn't passed as an earlier argument in the same invocation.
+.Pp
+After installing updates,
+.Nm
+automatically updates the EFI bootloader on the EFI System Partition (ESP)
+and the BIOS bootcode on
+.Xr gpart 8
+.Dq freebsd-boot
+partitions.
+This ensures the firmware-facing bootloader is consistent with the newly
+installed
+.Pa /boot/loader.efi
+and Lua scripts, preventing boot failures after major version upgrades.
+To disable, set
+.Cm UpdateBootloader no
+in
+.Xr freebsd-update.conf 5 .
.It Cm rollback
Uninstall the most recently installed updates.
.It Cm IDS
@@ -258,6 +274,14 @@
.Pa /var/db/freebsd-update/
may be deleted if an upgrade is not in progress and rollback will not be
required.
+.It Pa /usr/libexec/efi_bootloader_update.sh
+EFI and BIOS bootloader update library, sourced by
+.Nm
+during
+.Cm install
+to update bootloaders on the ESP and
+.Dq freebsd-boot
+partitions.
.El
.Sh SEE ALSO
.Xr freebsd-version 1 ,
diff --git a/usr.sbin/freebsd-update/freebsd-update.conf b/usr.sbin/freebsd-update/freebsd-update.conf
--- a/usr.sbin/freebsd-update/freebsd-update.conf
+++ b/usr.sbin/freebsd-update/freebsd-update.conf
@@ -76,3 +76,8 @@
# Create a new boot environment when installing patches
# CreateBootEnv yes
+
+# Automatically update the EFI bootloader on the ESP and BIOS bootcode on
+# freebsd-boot partitions when installing updates. Disable only if you manage
+# bootloaders manually or use a custom boot configuration.
+# UpdateBootloader yes
diff --git a/usr.sbin/freebsd-update/freebsd-update.sh b/usr.sbin/freebsd-update/freebsd-update.sh
--- a/usr.sbin/freebsd-update/freebsd-update.sh
+++ b/usr.sbin/freebsd-update/freebsd-update.sh
@@ -98,7 +98,8 @@
CONFIGOPTIONS="KEYPRINT WORKDIR SERVERNAME MAILTO ALLOWADD ALLOWDELETE
KEEPMODIFIEDMETADATA COMPONENTS IGNOREPATHS UPDATEIFUNMODIFIED
BASEDIR VERBOSELEVEL TARGETRELEASE STRICTCOMPONENTS MERGECHANGES
- IDSIGNOREPATHS BACKUPKERNEL BACKUPKERNELDIR BACKUPKERNELSYMBOLFILES"
+ IDSIGNOREPATHS BACKUPKERNEL BACKUPKERNELDIR BACKUPKERNELSYMBOLFILES \
+ UPDATEBOOTLOADER"
# Set all the configuration options to "".
nullconfig () {
@@ -440,6 +441,24 @@
return 1
fi
}
+config_UpdateBootloader () {
+ if [ -z ${UPDATEBOOTLOADER} ]; then
+ case $1 in
+ [Yy][Ee][Ss])
+ UPDATEBOOTLOADER=yes
+ ;;
+ [Nn][Oo])
+ UPDATEBOOTLOADER=no
+ ;;
+ *)
+ return 1
+ ;;
+ esac
+ else
+ return 1
+ fi
+}
+
# Handle one line of configuration
configline () {
if [ $# -eq 0 ]; then
@@ -621,6 +640,7 @@
config_BackupKernelDir /boot/kernel.old
config_BackupKernelSymbolFiles no
config_CreateBootEnv yes
+ config_UpdateBootloader yes
# Merge these defaults into the earlier-configured settings
mergeconfig
@@ -3252,6 +3272,28 @@
}
# Actually install updates
+# Update EFI and BIOS bootloaders after the new world/kernel is installed.
+# Sources /usr/libexec/efi_bootloader_update.sh to allow independent testing.
+# Controlled by UpdateBootloader in freebsd-update.conf (default: yes).
+update_bootloaders_after_install () {
+ if [ "${UPDATEBOOTLOADER}" = "no" ]; then
+ return 0
+ fi
+
+ _efi_lib="${BASEDIR}/usr/libexec/efi_bootloader_update.sh"
+
+ if [ ! -f "${_efi_lib}" ]; then
+ echo "freebsd-update: WARNING: ${_efi_lib} not found" \
+ "-- bootloader not automatically updated" >&2
+ return 0
+ fi
+
+ # shellcheck source=/usr/libexec/efi_bootloader_update.sh
+ . "${_efi_lib}"
+ update_bootloaders || true # warnings already printed; never block install
+ unset _efi_lib
+}
+
install_run () {
echo -n "Installing updates..."
@@ -3270,6 +3312,10 @@
install_setup_rollback
echo " done."
+
+ # Update EFI and BIOS bootloaders now that new world/kernel is in place.
+ # Runs after install_files so /boot/loader.efi is already updated.
+ update_bootloaders_after_install
}
# Rearrange bits to allow the previous set of updates to be rolled back next.
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Thu, Aug 27, 10:17 PM (9 h, 50 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
37306278
Default Alt Text
D58990.id.diff (49 KB)
Attached To
Mode
D58990: freebsd-update: automatically update EFI and BIOS bootloaders on install
Attached
Detach File
Event Timeline
Log In to Comment