Finalize the boot manager protocol support for next-stage boot

loading.

If we are booting in a conforming UEFI Boot Manager Environment, then
use the BootCurrent variable to find the BootXXXX we're using. Once we
find that, then if it contains more than one EFI_DEVICE_PATH in its
what to boot section, try to use the last one as the kernel to
load. This will also set the default root partition as well. If
there's only one path, or if there's an error along the way, assume
that nothing specific was specified and revert to the old
algorithm. If something was specified, but not found, then fail the
boot. Otherwise you that, specific thing. On FreeBSD, this can be set
using efibootmgr -l <loader> -k <kernel>. We try a few variations of
kernel to cope with the fact that UEFI comes from a DOS world where
paths might be upper case and/or contain back-slashes.

Note: In an ideal world, we'd work out where we are in chain loading
by looking at the passed-in image handle and doing name
matching. However, that's unreliable since at least boot1.efi booted
images don't have that, hence the assumption that loader.efi needs to
load the last thing on the list, if possible.

The reason we fail for something specific is so that we can fully
participate in the UEFI Boot Manager Protocol and fail over to the
next item in the list of BootOrder choices when something goes wrong
at this stage.

This implements was was talked about in freebsd-arch@ last year
https://docs.freebsd.org/cgi/getmsg.cgi?fetch=3576+0+archive/2017/freebsd-arch/20171022.freebsd-arch
and documented in full (after changed resulting from the discussion) in
https://docs.google.com/document/d/1aK9IqF-60JPEbUeSAUAkYjF2W_8EnmczFs6RqCT90Jg/edit#
although one or two minor details may have been modified in this
implementation to make it work, and the ZFS MEDIA PATH extension isn't
implemented. This does not yet move things to ESP:\efi\freebsd\loader.efi.

RelNotes: Yes
Sponsored by: Netflix
Differential Revision: https://reviews.freebsd.org/D16403
This commit is contained in:
Warner Losh 2018-07-23 20:36:59 +00:00
parent 00a47597a3
commit b43c6042c3
2 changed files with 225 additions and 17 deletions

View File

@ -31,6 +31,10 @@ NOTE TO PEOPLE WHO THINK THAT FreeBSD 12.x IS SLOW:
disable the most expensive debugging functionality run
"ln -s 'abort:false,junk:false' /etc/malloc.conf".)
20180723:
loader.efi has been augmented to participate more fully in the
UEFI boot manager protocol.
20180720:
zfsloader's functionality has now been folded into loader.
zfsloader is no longer necesasary once you've updated your

View File

@ -1,6 +1,7 @@
/*-
* Copyright (c) 2008-2010 Rui Paulo
* Copyright (c) 2006 Marcel Moolenaar
* Copyright (c) 2018 Netflix, Inc
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@ -41,6 +42,7 @@ __FBSDID("$FreeBSD$");
#include <efi.h>
#include <efilib.h>
#include <efichar.h>
#include <uuid.h>
@ -83,6 +85,11 @@ EFI_GUID inputid = SIMPLE_TEXT_INPUT_PROTOCOL;
*/
static int fail_timeout = 5;
/*
* Current boot variable
*/
UINT16 boot_current;
static bool
has_keyboard(void)
{
@ -274,8 +281,144 @@ try_as_currdev(pdinfo_t *hd, pdinfo_t *pp)
return (sanity_check_currdev());
}
/*
* Sometimes we get filenames that are all upper case
* and/or have backslashes in them. Filter all this out
* if it looks like we need to do so.
*/
static void
fix_dosisms(char *p)
{
while (*p) {
if (isupper(*p))
*p = tolower(*p);
else if (*p == '\\')
*p = '/';
p++;
}
}
enum { BOOT_INFO_OK = 0, BAD_CHOICE = 1, NOT_SPECIFIC = 2 };
static int
find_currdev(EFI_LOADED_IMAGE *img)
match_boot_info(EFI_LOADED_IMAGE *img __unused, char *boot_info, size_t bisz)
{
uint32_t attr;
uint16_t fplen;
size_t len;
char *walker, *ep;
EFI_DEVICE_PATH *dp, *edp, *first_dp, *last_dp;
pdinfo_t *pp;
CHAR16 *descr;
char *kernel = NULL;
FILEPATH_DEVICE_PATH *fp;
struct stat st;
/*
* FreeBSD encodes it's boot loading path into the boot loader
* BootXXXX variable. We look for the last one in the path
* and use that to load the kernel. However, if we only fine
* one DEVICE_PATH, then there's nothing specific and we should
* fall back.
*
* In an ideal world, we'd look at the image handle we were
* passed, match up with the loader we are and then return the
* next one in the path. This would be most flexible and cover
* many chain booting scenarios where you need to use this
* boot loader to get to the next boot loader. However, that
* doesn't work. We rarely have the path to the image booted
* (just the device) so we can't count on that. So, we do the
* enxt best thing, we look through the device path(s) passed
* in the BootXXXX varaible. If there's only one, we return
* NOT_SPECIFIC. Otherwise, we look at the last one and try to
* load that. If we can, we return BOOT_INFO_OK. Otherwise we
* return BAD_CHOICE for the caller to sort out.
*/
if (bisz < sizeof(attr) + sizeof(fplen) + sizeof(CHAR16))
return NOT_SPECIFIC;
walker = boot_info;
ep = walker + bisz;
memcpy(&attr, walker, sizeof(attr));
walker += sizeof(attr);
memcpy(&fplen, walker, sizeof(fplen));
walker += sizeof(fplen);
descr = (CHAR16 *)(intptr_t)walker;
len = ucs2len(descr);
walker += (len + 1) * sizeof(CHAR16);
last_dp = first_dp = dp = (EFI_DEVICE_PATH *)walker;
edp = (EFI_DEVICE_PATH *)(walker + fplen);
if ((char *)edp > ep)
return NOT_SPECIFIC;
while (dp < edp) {
last_dp = dp;
dp = (EFI_DEVICE_PATH *)((char *)dp + efi_devpath_length(dp));
}
/*
* If there's only one item in the list, then nothing was
* specified.
*/
if (last_dp == first_dp)
return NOT_SPECIFIC;
/*
* OK. At this point we either have a good path or a bad one.
* Let's check.
*/
pp = efiblk_get_pdinfo_by_device_path(last_dp);
if (pp == NULL)
return BAD_CHOICE;
set_currdev_pdinfo(pp);
if (!sanity_check_currdev())
return BAD_CHOICE;
/*
* OK. We've found a device that matches, next we need to check the last
* component of the path. If it's a file, then we set the default kernel
* to that. Otherwise, just use this as the default root.
*
* Reminder: we're running very early, before we've parsed the defaults
* file, so we may need to have a hack override.
*/
dp = efi_devpath_last_node(last_dp);
if (DevicePathType(dp) != MEDIA_DEVICE_PATH ||
DevicePathSubType(dp) != MEDIA_FILEPATH_DP)
return (BOOT_INFO_OK); /* use currdir, default kernel */
fp = (FILEPATH_DEVICE_PATH *)dp;
ucs2_to_utf8(fp->PathName, &kernel);
if (kernel == NULL)
return (BAD_CHOICE);
if (*kernel == '\\' || isupper(*kernel))
fix_dosisms(kernel);
if (stat(kernel, &st) != 0) {
free(kernel);
return (BAD_CHOICE);
}
setenv("kernel", kernel, 1);
free(kernel);
return (BOOT_INFO_OK);
}
/*
* Look at the passed-in boot_info, if any. If we find it then we need
* to see if we can find ourselves in the boot chain. If we can, and
* there's another specified thing to boot next, assume that the file
* is loaded from / and use that for the root filesystem. If can't
* find the specified thing, we must fail the boot. If we're last on
* the list, then we fallback to looking for the first available /
* candidate (ZFS, if there's a bootable zpool, otherwise a UFS
* partition that has either /boot/defaults/loader.conf on it or
* /boot/kernel/kernel (the default kernel) that we can use.
*
* We always fail if we can't find the right thing. However, as
* a concession to buggy UEFI implementations, like u-boot, if
* we have determined that the host is violating the UEFI boot
* manager protocol, we'll signal the rest of the program that
* a drop to the OK boot loader prompt is possible.
*/
static int
find_currdev(EFI_LOADED_IMAGE *img, bool do_bootmgr, bool is_last,
char *boot_info, size_t boot_info_sz)
{
pdinfo_t *dp, *pp;
EFI_DEVICE_PATH *devpath, *copy;
@ -284,8 +427,13 @@ find_currdev(EFI_LOADED_IMAGE *img)
struct devsw *dev;
int unit;
uint64_t extra;
int rv;
char *rootdev;
/*
* First choice: if rootdev is already set, use that, even if
* it's wrong.
*/
rootdev = getenv("rootdev");
if (rootdev != NULL) {
printf("Setting currdev to configured rootdev %s\n", rootdev);
@ -293,6 +441,25 @@ find_currdev(EFI_LOADED_IMAGE *img)
return (0);
}
/*
* Second choice: If we can find out image boot_info, and there's
* a follow-on boot image in that boot_info, use that. In this
* case root will be the partition specified in that image and
* we'll load the kernel specified by the file path. Should there
* not be a filepath, we use the default. This filepath overrides
* loader.conf.
*/
if (do_bootmgr) {
rv = match_boot_info(img, boot_info, boot_info_sz);
switch (rv) {
case BOOT_INFO_OK: /* We found it */
return (0);
case BAD_CHOICE: /* specified file not found -> error */
/* XXX do we want to have an escape hatch for last in boot order? */
return (ENOENT);
} /* Nothing specified, try normal match */
}
#ifdef EFI_ZFS_BOOT
/*
* Did efi_zfs_probe() detect the boot pool? If so, use the zpool
@ -332,7 +499,7 @@ find_currdev(EFI_LOADED_IMAGE *img)
/*
* Roll up the ZFS special case
* for those partitions that have
* zpools on them
* zpools on them.
*/
if (try_as_currdev(dp, pp))
return (0);
@ -529,15 +696,17 @@ main(int argc, CHAR16 *argv[])
EFI_GUID *guid;
int howto, i, uhowto;
UINTN k;
bool has_kbd;
bool has_kbd, is_last;
char *s;
EFI_DEVICE_PATH *imgpath;
CHAR16 *text;
EFI_STATUS status;
UINT16 boot_current;
size_t sz;
EFI_STATUS rv;
size_t sz, bosz = 0, bisz = 0;
UINT16 boot_order[100];
char boot_info[4096];
EFI_LOADED_IMAGE *img;
char buf[32];
bool uefi_boot_mgr;
archsw.arch_autoload = efi_autoload;
archsw.arch_getdev = efi_getdev;
@ -669,8 +838,8 @@ main(int argc, CHAR16 *argv[])
efi_free_devpath_name(text);
}
status = BS->HandleProtocol(img->DeviceHandle, &devid, (void **)&imgpath);
if (status == EFI_SUCCESS) {
rv = BS->HandleProtocol(img->DeviceHandle, &devid, (void **)&imgpath);
if (rv == EFI_SUCCESS) {
text = efi_devpath_name(imgpath);
if (text != NULL) {
printf(" Load Device: %S\n", text);
@ -679,18 +848,53 @@ main(int argc, CHAR16 *argv[])
}
}
uefi_boot_mgr = true;
boot_current = 0;
sz = sizeof(boot_current);
efi_global_getenv("BootCurrent", &boot_current, &sz);
printf(" BootCurrent: %04x\n", boot_current);
rv = efi_global_getenv("BootCurrent", &boot_current, &sz);
if (rv == EFI_SUCCESS)
printf(" BootCurrent: %04x\n", boot_current);
else {
boot_current = 0xffff;
uefi_boot_mgr = false;
}
sz = sizeof(boot_order);
efi_global_getenv("BootOrder", &boot_order, &sz);
printf(" BootOrder:");
for (i = 0; i < sz / sizeof(boot_order[0]); i++)
printf(" %04x%s", boot_order[i],
boot_order[i] == boot_current ? "[*]" : "");
printf("\n");
rv = efi_global_getenv("BootOrder", &boot_order, &sz);
if (rv == EFI_SUCCESS) {
printf(" BootOrder:");
for (i = 0; i < sz / sizeof(boot_order[0]); i++)
printf(" %04x%s", boot_order[i],
boot_order[i] == boot_current ? "[*]" : "");
printf("\n");
is_last = boot_order[(sz / sizeof(boot_order[0])) - 1] == boot_current;
bosz = sz;
} else if (uefi_boot_mgr) {
/*
* u-boot doesn't set BootOrder, but otherwise participates in the
* boot manager protocol. So we fake it here and don't consider it
* a failure.
*/
bosz = sizeof(boot_order[0]);
boot_order[0] = boot_current;
is_last = true;
}
/*
* Next, find the boot info structure the UEFI boot manager is
* supposed to setup. We need this so we can walk through it to
* find where we are in the booting process and what to try to
* boot next.
*/
if (uefi_boot_mgr) {
snprintf(buf, sizeof(buf), "Boot%04X", boot_current);
sz = sizeof(boot_info);
rv = efi_global_getenv(buf, &boot_info, &sz);
if (rv == EFI_SUCCESS)
bisz = sz;
else
uefi_boot_mgr = false;
}
/*
* Disable the watchdog timer. By default the boot manager sets
@ -710,7 +914,7 @@ main(int argc, CHAR16 *argv[])
* the boot protocol and also allow an escape hatch for users wishing
* to try something different.
*/
if (find_currdev(img) != 0)
if (find_currdev(img, uefi_boot_mgr, is_last, boot_info, bisz) != 0)
if (!interactive_interrupt("Failed to find bootable partition"))
return (EFI_NOT_FOUND);