Index: usr.sbin/Makefile =================================================================== --- usr.sbin/Makefile +++ usr.sbin/Makefile @@ -126,7 +126,7 @@ SUBDIR.${MK_BSNMP}+= bsnmpd SUBDIR.${MK_CXGBETOOL}+= cxgbetool SUBDIR.${MK_DIALOG}+= bsdconfig -SUBDIR.${MK_EFI}+= efivar efidp efibootmgr +SUBDIR.${MK_EFI}+= efivar efidp efibootmgr efi-update-loader .if ${MK_OPENSSL} != "no" SUBDIR.${MK_EFI}+= uefisign .endif Index: usr.sbin/efi-update-loader/Makefile =================================================================== --- /dev/null +++ usr.sbin/efi-update-loader/Makefile @@ -0,0 +1,6 @@ +# $FreeBSD$ + +SCRIPTS=efi-update-loader.sh +MAN= efi-update-loader.8 + +.include Index: usr.sbin/efi-update-loader/efi-update-loader.8 =================================================================== --- /dev/null +++ usr.sbin/efi-update-loader/efi-update-loader.8 @@ -0,0 +1,79 @@ +\" +.\" Copyright (c) 2019 Rebecca Cran . +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +.\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +.\" SUCH DAMAGE. +.\" +.\" $FreeBSD$ +.\" +.Dd March 19, 2019 +.Dt EFI-UPDATE-LOADER 8 +.Os +.Sh NAME +.Nm efi-update-loader +.Nd Update an EFI System Partition with a FreeBSD loader +.Sh SYNOPSIS +.Nm +.Op Fl d Ar device +.Op Fl p Ar path +.Op Fl l Ar loader +.Op Fl v +.Sh DESCRIPTION +This program updates an existing FreeBSD +.Dq Unified Extensible Firmware Interface +.Pq UEFI +.Dq EFI System Partition +.Pq ESP +with a new FreeBSD +.Pa loader.efi +file. + +The ESP is a FAT12, FAT16 or FAT32 filesystem that is used by the UEFI system +firmware to load and run OS boot loaders that are compiled as +.Dq Portable Executable +.Pq PE +binaries. The UEFI Boot Manager, which can be configured with the +.Xr efibootmgr 8 +utility, contains entries which point to the location of the boot loader +within the ESP. +.Pp +The following options are available: +.Bl -tag -width indent +.It Fl d Ar device +Specify the device to use as the ESP. If not specified, any ESPs associated +with the root filesystem are used. +.It Fl p Ar path +Specify the path to a mounted ESP. +.It Fl l Ar loader +Specify the file to install as the FreeBSD boot loader. If not specified, +.Pa /boot/loader.efi +is used. This option may be used to install other loaders such as +.Pa /boot/loader_4th.efi . +.It Fl v +Enable verbose output. +.Sh SEE ALSO +.Xr efibootmgr 8 , +.Xr efivar 8 +.Sh HISTORY +The +.Nm +utility first appeared in +.Fx 13.0 Index: usr.sbin/efi-update-loader/efi-update-loader.sh =================================================================== --- /dev/null +++ usr.sbin/efi-update-loader/efi-update-loader.sh @@ -0,0 +1,379 @@ +#!/bin/sh +# +# SPDX-License-Identifier: BSD-2-Clause-FreeBSD +# +# Copyright (c) 2019 Rebecca Cran . +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# +# $FreeBSD$ + +set -e + +vout() { + if [ -n "${verbose}" ]; then echo "$*"; fi +} + + +# Find the name of the default boot loader on the current architecture. +# This file is in the /EFI/BOOT directory on the ESP. +get_uefi_bootname() { + case ${TARGET:-$(uname -m)} in + amd64) echo BOOTx64 ;; + arm64) echo BOOTaa64 ;; + i386) echo BOOTia32 ;; + arm) echo BOOTarm ;; + *) + echo "machine type $(uname -m) doesn't support UEFI" + exit 1 + ;; + esac +} + + +clean_up() { + trap 1 2 15 EXIT + + if [ -n "${espdev}" ] && [ -z "${mntpt}" ]; then exit 1; fi + + if [ -n "${espdev}" ] && mount | grep -q "${mntpt}" ; then + umount "${mntpt}" + fi + + if [ -n "${espdev}" ] && [ -d "${mntpt}" ]; then rmdir "${mntpt}"; fi + + echo "Something went wrong. The ESP(s) used were: ${esps}" + echo "Any backups of the previous loader.efi and $(get_uefi_bootname).efi are in /tmp/espback.*" +} + + +# Determine whether /EFI/BOOT/BOOTxxx.efi is the FreeBSD boot1.efi or loader.efi executable. +bootefi_is_freebsd() { + efibootname=$(get_uefi_bootname) + + if [ -f "${mntpt}/EFI/BOOT/${efibootname}.efi" ]; then + # "FreeBSD EFI boot block" is contained in boot1.efi; + # "FreeBSD/${arch} EFI loader" is contained in loader.efi + loaderstring="FreeBSD/$(uname -m) EFI loader" + boot1string="FreeBSD EFI boot block" + if grep -q -e "${boot1string}" -e "${loaderstring}" "${mntpt}/EFI/BOOT/${efibootname}.efi"; then + return 0 + fi + fi + + return 1 +} + + +# Find all ESPs on the same disk(s) as the root filesystem. +detect_esps() { + mnt=/ + fsname=$(df "${mnt}" | tail -1 | cut -d ' ' -f 1) + fstype= + + if df -t ufs "${mnt}" > /dev/null ; then + fstype=ufs + elif df -t zfs "${mnt}" > /dev/null ; then + fstype=zfs + else + echo "Unsupported filesystem type" + exit 1 + fi + + if [ "${fstype}" = "zfs" ]; then + fslabel=$(df "${mnt}" | tail -1 | awk -F '[ /]' '{print $1}') + zpool=$(zpool list -Hv "${fslabel}") + totallines=$(echo "${zpool}" | wc -l) + devlines=$(echo "${zpool}" | tail -$((totallines - 1))) + labels=$(echo "${devlines}" | awk '{print $1}') + elif [ "${fstype}" = "ufs" ]; then + label=$(echo "${fsname}" | cut -c 6-256) + + for class in mirror raid raid3 vinum stripe virstor; do + labels=$(geom ${class} status -s 2> /dev/null | grep "${label}" | awk '{print $3}') || true + if [ -n "${labels}" ]; then + break + fi + done + + if [ -z "${labels}" ]; then + labels="${label}" + fi + fi + + for lbl in ${labels}; do + lblout=$(geom label status -s | grep "${lbl}" | awk '{print $3}') + if [ -n "${lblout}" ]; then + dev="${lblout}" + else + dev="${lbl}" + fi + + # Get the disk name from the partition/slice name + disks="${disks} $(echo "${dev}" | awk -F '[ps]+[0-9]+' '{print $1}')" + done + + for disk in ${disks}; do + idx=$(gpart show "${disk}" 2> /dev/null | grep efi | awk '{print $3}') + if [ -n "${idx}" ]; then + if [ -e "/dev/${disk}p${idx}" ]; then + esps="${esps} /dev/${disk}p${idx}" + elif [ -e "/dev/${disk}s${idx}" ]; then + esps="${esps} /dev/${disk}s${idx}" + fi + fi + done + + if [ -n "${verbose}" ] && [ -n "${esps}" ]; then + echo -n "Found ESP(s): " + for esp in ${esps}; do + echo -n "${esp} " + done + echo + fi +} + + +# Return the free disk space, in KB, on the mounted ESP. +get_freespace_on_esp() { + df -k "${mntpt}" | tail -1 | awk '{print $4}' +} + + +find_dev_from_mountpoint() { + mount | while read -r m; do + l="$(echo "${m}" | awk '{print $3}')" + if [ "${l}" = "${mntpt}" ]; then + echo "${m}" | awk '{print $1}' + break + fi + done + +} + + +copyloader() { + espdev=$1 + remountedrw= + + if [ -z "${espdev}" ]; then + mntpt="${esppath}" + else + mntpt=$(mktemp -d /tmp/esp.XXXXXX) + fi + + if [ -n "${espdev}" ] && ! mount -t msdosfs "${espdev}" "${mntpt}" 2> /dev/null; then + # See if it's already mounted + m=$(mount) + d=$(echo "${espdev}" | cut -d '/' -f 3) + l=$(geom label status -s | grep "${d}" | awk '{print $1}') + if echo "${m}" | grep -q "${espdev}" || echo "${m}" | grep -q "${l}"; then + rmdir "${mntpt}" + mntpt=$(echo "${m}" | grep "${espdev}" | awk '{print $3}') + if [ -z "${mntpt}" ]; then + mntpt=$(echo "${m}" | grep "${l}" | awk '{print $3}') + fi + # Prevents the cleanup code from unmounting the ESP and removing the mountpoint + espdev= + eval vout "Using ESP mounted at ${mntpt}" + + else + echo "Failed to mount ESP device ${espdev}" + rmdir "${mntpt}" + trap 1 2 15 EXIT + exit 1 + fi + fi + + if [ ! -d "${mntpt}/EFI" ]; then + if [ -n "${espdev}" ]; then + echo "Error: The device ${espdev} does not appear to be an ESP." + exit 1 + else + echo "Error: The path ${mntpt} does not appear to be an ESP." + trap 1 2 15 EXIT + exit 1 + fi + fi + + if [ -z "${espdev}" ]; then + if ! touch "${mntpt}/EFI" 2> /dev/null; then + dev=$(find_dev_from_mountpoint) + + if [ -n "${dev}" ]; then + eval vout "Updating mount ${mntpt} r/w" + mount -o update,rw "${dev}" "${mntpt}" + remountedrw=1 + fi + fi + fi + + ldrsize=$(stat -f %z "${loader}") + ldrsize=$((ldrsize / 1024)) + + efibootname=$(get_uefi_bootname) + + # Check we have enough space to install /EFI/BOOT/BOOT${arch}.efi + if bootefi_is_freebsd; then + existing_bootefi_size=$(stat -f %z "${mntpt}/EFI/BOOT/${efibootname}.efi") + existing_bootefi_size=$((existing_bootefi_size / 1024)) + spaceneeded=$((existing_bootefi_size - ldrsize)) + fi + + if bootefi_is_freebsd && [ $(($(get_freespace_on_esp))) -lt ${spaceneeded} ]; then + echo "Error: Insufficient space on ESP ${espdev} to install ${loader} as /EFI/FreeBSD/loader.efi" + echo "Need ${spaceneeded}KB, but only $(($(get_freespace_on_esp))) remains." + exit 1 + fi + + # Check we have enough space to install /EFI/FreeBSD/loader.efi + if [ -f "${mntpt}/EFI/FreeBSD/loader.efi" ]; then + existing_ldr_size=$(stat -f %z "${mntpt}/EFI/FreeBSD/loader.efi") + existing_ldr_size=$((existing_ldr_size / 1024)) + spaceneeded=$((existing_ldr_size - ldrsize)) + fi + + if [ -f "${mntpt}/EFI/FreeBSD/loader.efi" ] && [ $(($(get_freespace_on_esp))) -lt ${spaceneeded} ]; then + echo "Error: Insufficient space on ESP ${espdev} to install ${loader} to /EFI/BOOT/${efibootname}.efi." + echo "Need ${spaceneeded}KB, but only $(($(get_freespace_on_esp))) remains." + exit 1 + fi + + backdir=$(mktemp -d /tmp/espback.XXXXXX) + + # Make backups and copy the new loader + if [ -f "${mntpt}/EFI/FreeBSD/loader.efi" ] && ! cmp -sz "${loader}" "${mntpt}/EFI/FreeBSD/loader.efi"; then + cp "${mntpt}/EFI/FreeBSD/loader.efi" "${backdir}" + eval vout "Copying ${loader} to /EFI/FreeBSD/loader.efi" + cp "${loader}" "${mntpt}/EFI/FreeBSD/loader.efi" + [ -f "${mntpt}/EFI/FreeBSD/loader-old.efi" ] && rm "${mntpt}/EFI/FreeBSD/loader-old.efi" + elif [ -f "${mntpt}/EFI/FreeBSD/loader.efi" ]; then + eval vout "${loader} is the same as /EFI/FreeBSD/loader.efi. Nothing to do." + fi + + if bootefi_is_freebsd && ! cmp -sz "${loader}" "${mntpt}/EFI/BOOT/${efibootname}.efi"; then + cp "${mntpt}/EFI/BOOT/${efibootname}.efi" "${backdir}/${efibootname}.efi" + eval vout "Copying ${loader} to /EFI/BOOT/${efibootname}.efi" + cp "${loader}" "${mntpt}/EFI/BOOT/${efibootname}.efi" + [ -f "${mntpt}/EFI/BOOT/${efibootname}-old.efi" ] && rm "${mntpt}/EFI/BOOT/${efibootname}-old.efi" + elif bootefi_is_freebsd; then + eval vout "${loader} is the same as /EFI/BOOT/${efibootname}.efi. Nothing to do." + fi + + # Copy backups to the ESP + if [ -f "${backdir}/loader.efi" ]; then + sz=$(stat -f %z "${backdir}/loader.efi") + sz=$((sz / 1024)) + if [ $(($(get_freespace_on_esp))) -gt ${sz} ]; then + eval vout "Backing up previous /EFI/FreeBSD/loader.efi to /EFI/FreeBSD/loader-old.efi" + cp "${backdir}/loader.efi" "${mntpt}/EFI/FreeBSD/loader-old.efi" + else + eval vout "Skipping backup of previous /EFI/FreeBSD/loader.efi due to insufficient disk space." + fi + fi + + if [ -f "${backdir}/${efibootname}.efi" ]; then + sz=$(stat -f %z "${backdir}/${efibootname}.efi") + sz=$((sz / 1024)) + if [ $(($(get_freespace_on_esp))) -gt ${sz} ]; then + eval vout "Backing up previous /EFI/BOOT/${efibootname}.efi to /EFI/BOOT/${efibootname}-old.efi" + cp "${backdir}/${efibootname}.efi" "${mntpt}/EFI/BOOT/${efibootname}-old.efi" + else + eval vout "Skipping backup of previous /EFI/BOOT/${efibootname}.efi due to insufficient disk space." + fi + fi + + if [ -n "${espdev}" ]; then + umount "${mntpt}" + rmdir "${mntpt}" + elif [ -n "${remountedrw}" ]; then + eval vout "Updating mount ${mntpt} r/o" + mount -o update,ro "${dev}" "${mntpt}" + fi + + if [ -f "${backdir}/loader.efi" ]; then rm "${backdir}/loader.efi"; fi + if [ -f "${backdir}/${efibootname}.efi" ]; then rm "${backdir}/${efibootname}.efi"; fi + if [ -d "${backdir}" ]; then rmdir "${backdir}"; fi +} + + +usage() { + printf 'usage: %s [-d device] [-p path] [-l loader] [-v]\n' "${progname}" + printf '\t-d device\tEFI System Partition (ESP) device name\n' + printf '\t-p path\t\tPath to a mounted EFI System Partition (ESP)\n' + printf '\t-l loader\tPath to FreeBSD EFI loader (default is /boot/loader.efi)\n' + printf '\t-v \t\tEnable verbose output\n' + exit 0 +} + + +progname=$0 +loader=/boot/loader.efi + +while getopts "vd:p:l:h" opt; do + case "$opt" in + d) + esps=${OPTARG} + ;; + p) + esppath=${OPTARG} + ;; + l) + loader=${OPTARG} + ;; + v) + verbose=1 + ;; + ?) + usage + ;; + esac +done + +if [ "$(id -u)" -ne 0 ]; then + echo "Error: this utility must be run as root" + exit 1 +fi + +trap clean_up 1 2 15 EXIT + +# If the user didn't specify a device to update, look for any ESPs on the same +# disk(s) as the root filesystem. +if [ -z "${esps}" ] && [ -z "${esppath}" ]; then + + eval detect_esps + + if [ -z "${esps}" ]; then + echo "Error: could not detect ESP containing FreeBSD loader to update" + exit 1 + fi +fi + +for esp in ${esps}; do + eval copyloader "${esp}" +done + +if [ -n "${esppath}" ]; then + eval copyloader "" +fi + +trap 1 2 15 EXIT