Skip site navigation (1)Skip section navigation (2)
Date:      Mon, 23 Jul 2018 20:36:59 +0000 (UTC)
From:      Warner Losh <imp@FreeBSD.org>
To:        src-committers@freebsd.org, svn-src-all@freebsd.org, svn-src-head@freebsd.org
Subject:   svn commit: r336656 - in head: . stand/efi/loader
Message-ID:  <201807232036.w6NKax98012346@repo.freebsd.org>

next in thread | raw e-mail | index | archive | help
Author: imp
Date: Mon Jul 23 20:36:59 2018
New Revision: 336656
URL: https://svnweb.freebsd.org/changeset/base/336656

Log:
  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

Modified:
  head/UPDATING
  head/stand/efi/loader/main.c

Modified: head/UPDATING
==============================================================================
--- head/UPDATING	Mon Jul 23 20:36:54 2018	(r336655)
+++ head/UPDATING	Mon Jul 23 20:36:59 2018	(r336656)
@@ -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

Modified: head/stand/efi/loader/main.c
==============================================================================
--- head/stand/efi/loader/main.c	Mon Jul 23 20:36:54 2018	(r336655)
+++ head/stand/efi/loader/main.c	Mon Jul 23 20:36:59 2018	(r336656)
@@ -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,9 +281,145 @@ 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;
 	EFI_HANDLE h;
@@ -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,20 +848,55 @@ 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
 	 * the timer to 5 minutes before invoking a boot option. If we
 	 * want to return to the boot manager, we have to disable the
@@ -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);
 



Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?201807232036.w6NKax98012346>