summaryrefslogtreecommitdiffstats
path: root/src/boot
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 15:35:18 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 15:35:18 +0000
commitb750101eb236130cf056c675997decbac904cc49 (patch)
treea5df1a06754bdd014cb975c051c83b01c9a97532 /src/boot
parentInitial commit. (diff)
downloadsystemd-b750101eb236130cf056c675997decbac904cc49.tar.xz
systemd-b750101eb236130cf056c675997decbac904cc49.zip
Adding upstream version 252.22.upstream/252.22upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/boot')
-rw-r--r--src/boot/bless-boot-generator.c55
-rw-r--r--src/boot/bless-boot.c527
-rw-r--r--src/boot/boot-check-no-failures.c113
-rw-r--r--src/boot/bootctl.c2636
-rw-r--r--src/boot/efi/assert.c12
-rw-r--r--src/boot/efi/bcd.c306
-rw-r--r--src/boot/efi/bcd.h7
-rw-r--r--src/boot/efi/boot.c2778
-rw-r--r--src/boot/efi/console.c309
-rw-r--r--src/boot/efi/console.h37
-rw-r--r--src/boot/efi/cpio.c568
-rw-r--r--src/boot/efi/cpio.h34
-rw-r--r--src/boot/efi/devicetree.c154
-rw-r--r--src/boot/efi/devicetree.h16
-rw-r--r--src/boot/efi/disk.c40
-rw-r--r--src/boot/efi/disk.h7
-rw-r--r--src/boot/efi/drivers.c117
-rw-r--r--src/boot/efi/drivers.h10
-rw-r--r--src/boot/efi/efi-string.c460
-rw-r--r--src/boot/efi/efi-string.h132
-rw-r--r--src/boot/efi/fuzz-bcd.c22
-rw-r--r--src/boot/efi/fuzz-efi-string.c40
-rw-r--r--src/boot/efi/graphics.c43
-rw-r--r--src/boot/efi/graphics.h11
-rw-r--r--src/boot/efi/initrd.c140
-rw-r--r--src/boot/efi/initrd.h16
-rw-r--r--src/boot/efi/linux.c155
-rw-r--r--src/boot/efi/linux.h20
-rw-r--r--src/boot/efi/linux_x86.c215
-rw-r--r--src/boot/efi/measure.c221
-rw-r--r--src/boot/efi/measure.h39
-rw-r--r--src/boot/efi/meson.build500
-rw-r--r--src/boot/efi/missing_efi.h400
-rw-r--r--src/boot/efi/pe.c338
-rw-r--r--src/boot/efi/pe.h20
-rw-r--r--src/boot/efi/random-seed.c321
-rw-r--r--src/boot/efi/random-seed.h22
-rw-r--r--src/boot/efi/secure-boot.c217
-rw-r--r--src/boot/efi/secure-boot.h27
-rw-r--r--src/boot/efi/shim.c101
-rw-r--r--src/boot/efi/shim.h16
-rw-r--r--src/boot/efi/splash.c322
-rw-r--r--src/boot/efi/splash.h6
-rw-r--r--src/boot/efi/stub.c415
-rw-r--r--src/boot/efi/test-bcd.c162
-rw-r--r--src/boot/efi/test-efi-string.c523
-rw-r--r--src/boot/efi/ticks.c84
-rw-r--r--src/boot/efi/ticks.h6
-rw-r--r--src/boot/efi/util.c794
-rw-r--r--src/boot/efi/util.h220
-rw-r--r--src/boot/efi/vmm.c39
-rw-r--r--src/boot/efi/vmm.h7
-rw-r--r--src/boot/efi/xbootldr.c285
-rw-r--r--src/boot/efi/xbootldr.h9
-rw-r--r--src/boot/measure.c1164
-rw-r--r--src/boot/pcrphase.c275
56 files changed, 15513 insertions, 0 deletions
diff --git a/src/boot/bless-boot-generator.c b/src/boot/bless-boot-generator.c
new file mode 100644
index 0000000..6adef5b
--- /dev/null
+++ b/src/boot/bless-boot-generator.c
@@ -0,0 +1,55 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+#include <unistd.h>
+
+#include "efi-loader.h"
+#include "generator.h"
+#include "log.h"
+#include "mkdir.h"
+#include "special.h"
+#include "string-util.h"
+#include "virt.h"
+
+/* This generator pulls systemd-bless-boot.service into the initial transaction if the "LoaderBootCountPath"
+ * EFI variable is set, i.e. the system boots up with boot counting in effect, which means we should mark the
+ * boot as "good" if we manage to boot up far enough. */
+
+static int run(const char *dest, const char *dest_early, const char *dest_late) {
+
+ if (in_initrd() > 0) {
+ log_debug("Skipping generator, running in the initrd.");
+ return 0;
+ }
+
+ if (detect_container() > 0) {
+ log_debug("Skipping generator, running in a container.");
+ return 0;
+ }
+
+ if (!is_efi_boot()) {
+ log_debug("Skipping generator, not an EFI boot.");
+ return 0;
+ }
+
+ if (access(EFIVAR_PATH(EFI_LOADER_VARIABLE(LoaderBootCountPath)), F_OK) < 0) {
+
+ if (errno == ENOENT) {
+ log_debug_errno(errno, "Skipping generator, not booted with boot counting in effect.");
+ return 0;
+ }
+
+ return log_error_errno(errno, "Failed to check if LoaderBootCountPath EFI variable exists: %m");
+ }
+
+ /* We pull this in from basic.target so that it ends up in all "regular" boot ups, but not in
+ * rescue.target or even emergency.target. */
+ const char *p = strjoina(dest_early, "/" SPECIAL_BASIC_TARGET ".wants/systemd-bless-boot.service");
+ (void) mkdir_parents(p, 0755);
+ if (symlink(SYSTEM_DATA_UNIT_DIR "/systemd-bless-boot.service", p) < 0)
+ return log_error_errno(errno, "Failed to create symlink '%s': %m", p);
+
+ return 0;
+}
+
+DEFINE_MAIN_GENERATOR_FUNCTION(run);
diff --git a/src/boot/bless-boot.c b/src/boot/bless-boot.c
new file mode 100644
index 0000000..268651a
--- /dev/null
+++ b/src/boot/bless-boot.c
@@ -0,0 +1,527 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.h>
+#include <stdlib.h>
+
+#include "alloc-util.h"
+#include "bootspec.h"
+#include "devnum-util.h"
+#include "efi-api.h"
+#include "efi-loader.h"
+#include "efivars.h"
+#include "fd-util.h"
+#include "find-esp.h"
+#include "fs-util.h"
+#include "log.h"
+#include "main-func.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "pretty-print.h"
+#include "sync-util.h"
+#include "terminal-util.h"
+#include "util.h"
+#include "verbs.h"
+#include "virt.h"
+
+static char **arg_path = NULL;
+
+STATIC_DESTRUCTOR_REGISTER(arg_path, strv_freep);
+
+static int help(int argc, char *argv[], void *userdata) {
+ _cleanup_free_ char *link = NULL;
+ int r;
+
+ r = terminal_urlify_man("systemd-bless-boot.service", "8", &link);
+ if (r < 0)
+ return log_oom();
+
+ printf("%s [OPTIONS...] COMMAND\n"
+ "\n%sMark the boot process as good or bad.%s\n"
+ "\nCommands:\n"
+ " status Show status of current boot loader entry\n"
+ " good Mark this boot as good\n"
+ " bad Mark this boot as bad\n"
+ " indeterminate Undo any marking as good or bad\n"
+ "\nOptions:\n"
+ " -h --help Show this help\n"
+ " --version Print version\n"
+ " --path=PATH Path to the $BOOT partition (may be used multiple times)\n"
+ "\nSee the %s for details.\n",
+ program_invocation_short_name,
+ ansi_highlight(),
+ ansi_normal(),
+ link);
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+ enum {
+ ARG_PATH = 0x100,
+ ARG_VERSION,
+ };
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, ARG_VERSION },
+ { "path", required_argument, NULL, ARG_PATH },
+ {}
+ };
+
+ int c, r;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0)
+ switch (c) {
+
+ case 'h':
+ help(0, NULL, NULL);
+ return 0;
+
+ case ARG_VERSION:
+ return version();
+
+ case ARG_PATH:
+ r = strv_extend(&arg_path, optarg);
+ if (r < 0)
+ return log_oom();
+ break;
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached();
+ }
+
+ return 1;
+}
+
+static int acquire_path(void) {
+ _cleanup_free_ char *esp_path = NULL, *xbootldr_path = NULL;
+ dev_t esp_devid = 0, xbootldr_devid = 0;
+ char **a;
+ int r;
+
+ if (!strv_isempty(arg_path))
+ return 0;
+
+ r = find_esp_and_warn(NULL, NULL, /* unprivileged_mode= */ false, &esp_path, NULL, NULL, NULL, NULL, &esp_devid);
+ if (r < 0 && r != -ENOKEY) /* ENOKEY means not found, and is the only error the function won't log about on its own */
+ return r;
+
+ r = find_xbootldr_and_warn(NULL, NULL, /* unprivileged_mode= */ false, &xbootldr_path, NULL, &xbootldr_devid);
+ if (r < 0 && r != -ENOKEY)
+ return r;
+
+ if (!esp_path && !xbootldr_path)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
+ "Couldn't find $BOOT partition. It is recommended to mount it to /boot.\n"
+ "Alternatively, use --path= to specify path to mount point.");
+
+ if (esp_path && xbootldr_path && !devnum_set_and_equal(esp_devid, xbootldr_devid)) /* in case the two paths refer to the same inode, suppress one */
+ a = strv_new(esp_path, xbootldr_path);
+ else if (esp_path)
+ a = strv_new(esp_path);
+ else
+ a = strv_new(xbootldr_path);
+ if (!a)
+ return log_oom();
+
+ strv_free_and_replace(arg_path, a);
+
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *j = NULL;
+
+ j = strv_join(arg_path, ":");
+ log_debug("Using %s as boot loader drop-in search path.", strna(j));
+ }
+
+ return 0;
+}
+
+static int parse_counter(
+ const char *path,
+ const char **p,
+ uint64_t *ret_left,
+ uint64_t *ret_done) {
+
+ uint64_t left, done;
+ const char *z, *e;
+ size_t k;
+ int r;
+
+ assert(path);
+ assert(p);
+
+ e = *p;
+ assert(e);
+ assert(*e == '+');
+
+ e++;
+
+ k = strspn(e, DIGITS);
+ if (k == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Can't parse empty 'tries left' counter from LoaderBootCountPath: %s",
+ path);
+
+ z = strndupa_safe(e, k);
+ r = safe_atou64(z, &left);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse 'tries left' counter from LoaderBootCountPath: %s", path);
+
+ e += k;
+
+ if (*e == '-') {
+ e++;
+
+ k = strspn(e, DIGITS);
+ if (k == 0) /* If there's a "-" there also needs to be at least one digit */
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Can't parse empty 'tries done' counter from LoaderBootCountPath: %s",
+ path);
+
+ z = strndupa_safe(e, k);
+ r = safe_atou64(z, &done);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse 'tries done' counter from LoaderBootCountPath: %s", path);
+
+ e += k;
+ } else
+ done = 0;
+
+ if (done == 0)
+ log_warning("The 'tries done' counter is currently at zero. This can't really be, after all we are running, and this boot must hence count as one. Proceeding anyway.");
+
+ *p = e;
+
+ if (ret_left)
+ *ret_left = left;
+
+ if (ret_done)
+ *ret_done = done;
+
+ return 0;
+}
+
+static int acquire_boot_count_path(
+ char **ret_path,
+ char **ret_prefix,
+ uint64_t *ret_left,
+ uint64_t *ret_done,
+ char **ret_suffix) {
+
+ _cleanup_free_ char *path = NULL, *prefix = NULL, *suffix = NULL;
+ const char *last, *e;
+ uint64_t left, done;
+ int r;
+
+ r = efi_get_variable_string(EFI_LOADER_VARIABLE(LoaderBootCountPath), &path);
+ if (r == -ENOENT)
+ return -EUNATCH; /* in this case, let the caller print a message */
+ if (r < 0)
+ return log_error_errno(r, "Failed to read LoaderBootCountPath EFI variable: %m");
+
+ efi_tilt_backslashes(path);
+
+ if (!path_is_normalized(path))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Path read from LoaderBootCountPath is not normalized, refusing: %s",
+ path);
+
+ if (!path_is_absolute(path))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Path read from LoaderBootCountPath is not absolute, refusing: %s",
+ path);
+
+ last = last_path_component(path);
+ e = strrchr(last, '+');
+ if (!e)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Path read from LoaderBootCountPath does not contain a counter, refusing: %s",
+ path);
+
+ if (ret_prefix) {
+ prefix = strndup(path, e - path);
+ if (!prefix)
+ return log_oom();
+ }
+
+ r = parse_counter(path, &e, &left, &done);
+ if (r < 0)
+ return r;
+
+ if (ret_suffix) {
+ suffix = strdup(e);
+ if (!suffix)
+ return log_oom();
+
+ *ret_suffix = TAKE_PTR(suffix);
+ }
+
+ if (ret_path)
+ *ret_path = TAKE_PTR(path);
+ if (ret_prefix)
+ *ret_prefix = TAKE_PTR(prefix);
+ if (ret_left)
+ *ret_left = left;
+ if (ret_done)
+ *ret_done = done;
+
+ return 0;
+}
+
+static int make_good(const char *prefix, const char *suffix, char **ret) {
+ _cleanup_free_ char *good = NULL;
+
+ assert(prefix);
+ assert(suffix);
+ assert(ret);
+
+ /* Generate the path we'd use on good boots. This one is easy. If we are successful, we simple drop the counter
+ * pair entirely from the name. After all, we know all is good, and the logs will contain information about the
+ * tries we needed to come here, hence it's safe to drop the counters from the name. */
+
+ good = strjoin(prefix, suffix);
+ if (!good)
+ return -ENOMEM;
+
+ *ret = TAKE_PTR(good);
+ return 0;
+}
+
+static int make_bad(const char *prefix, uint64_t done, const char *suffix, char **ret) {
+ _cleanup_free_ char *bad = NULL;
+
+ assert(prefix);
+ assert(suffix);
+ assert(ret);
+
+ /* Generate the path we'd use on bad boots. Let's simply set the 'left' counter to zero, and keep the 'done'
+ * counter. The information might be interesting to boot loaders, after all. */
+
+ if (done == 0) {
+ bad = strjoin(prefix, "+0", suffix);
+ if (!bad)
+ return -ENOMEM;
+ } else {
+ if (asprintf(&bad, "%s+0-%" PRIu64 "%s", prefix, done, suffix) < 0)
+ return -ENOMEM;
+ }
+
+ *ret = TAKE_PTR(bad);
+ return 0;
+}
+
+static const char *skip_slash(const char *path) {
+ assert(path);
+ assert(path[0] == '/');
+
+ return path + 1;
+}
+
+static int verb_status(int argc, char *argv[], void *userdata) {
+ _cleanup_free_ char *path = NULL, *prefix = NULL, *suffix = NULL, *good = NULL, *bad = NULL;
+ uint64_t left, done;
+ int r;
+
+ r = acquire_boot_count_path(&path, &prefix, &left, &done, &suffix);
+ if (r == -EUNATCH) { /* No boot count in place, then let's consider this a "clean" boot, as "good", "bad" or "indeterminate" don't apply. */
+ puts("clean");
+ return 0;
+ }
+ if (r < 0)
+ return r;
+
+ r = acquire_path();
+ if (r < 0)
+ return r;
+
+ r = make_good(prefix, suffix, &good);
+ if (r < 0)
+ return log_oom();
+
+ r = make_bad(prefix, done, suffix, &bad);
+ if (r < 0)
+ return log_oom();
+
+ log_debug("Booted file: %s\n"
+ "The same modified for 'good': %s\n"
+ "The same modified for 'bad': %s\n",
+ path,
+ good,
+ bad);
+
+ log_debug("Tries left: %" PRIu64"\n"
+ "Tries done: %" PRIu64"\n",
+ left, done);
+
+ STRV_FOREACH(p, arg_path) {
+ _cleanup_close_ int fd = -1;
+
+ fd = open(*p, O_DIRECTORY|O_CLOEXEC|O_RDONLY);
+ if (fd < 0) {
+ if (errno == ENOENT)
+ continue;
+
+ return log_error_errno(errno, "Failed to open $BOOT partition '%s': %m", *p);
+ }
+
+ if (faccessat(fd, skip_slash(path), F_OK, 0) >= 0) {
+ puts("indeterminate");
+ return 0;
+ }
+ if (errno != ENOENT)
+ return log_error_errno(errno, "Failed to check if '%s' exists: %m", path);
+
+ if (faccessat(fd, skip_slash(good), F_OK, 0) >= 0) {
+ puts("good");
+ return 0;
+ }
+
+ if (errno != ENOENT)
+ return log_error_errno(errno, "Failed to check if '%s' exists: %m", good);
+
+ if (faccessat(fd, skip_slash(bad), F_OK, 0) >= 0) {
+ puts("bad");
+ return 0;
+ }
+ if (errno != ENOENT)
+ return log_error_errno(errno, "Failed to check if '%s' exists: %m", bad);
+
+ /* We didn't find any of the three? If so, let's try the next directory, before we give up. */
+ }
+
+ return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Couldn't determine boot state: %m");
+}
+
+static int verb_set(int argc, char *argv[], void *userdata) {
+ _cleanup_free_ char *path = NULL, *prefix = NULL, *suffix = NULL, *good = NULL, *bad = NULL;
+ const char *target, *source1, *source2;
+ uint64_t done;
+ int r;
+
+ r = acquire_boot_count_path(&path, &prefix, NULL, &done, &suffix);
+ if (r == -EUNATCH) /* acquire_boot_count_path() won't log on its own for this specific error */
+ return log_error_errno(r, "Not booted with boot counting in effect.");
+ if (r < 0)
+ return r;
+
+ r = acquire_path();
+ if (r < 0)
+ return r;
+
+ r = make_good(prefix, suffix, &good);
+ if (r < 0)
+ return log_oom();
+
+ r = make_bad(prefix, done, suffix, &bad);
+ if (r < 0)
+ return log_oom();
+
+ /* Figure out what rename to what */
+ if (streq(argv[0], "good")) {
+ target = good;
+ source1 = path;
+ source2 = bad; /* Maybe this boot was previously marked as 'bad'? */
+ } else if (streq(argv[0], "bad")) {
+ target = bad;
+ source1 = path;
+ source2 = good; /* Maybe this boot was previously marked as 'good'? */
+ } else {
+ assert(streq(argv[0], "indeterminate"));
+ target = path;
+ source1 = good;
+ source2 = bad;
+ }
+
+ STRV_FOREACH(p, arg_path) {
+ _cleanup_close_ int fd = -1;
+
+ fd = open(*p, O_DIRECTORY|O_CLOEXEC|O_RDONLY);
+ if (fd < 0)
+ return log_error_errno(errno, "Failed to open $BOOT partition '%s': %m", *p);
+
+ r = rename_noreplace(fd, skip_slash(source1), fd, skip_slash(target));
+ if (r == -EEXIST)
+ goto exists;
+ if (r == -ENOENT) {
+
+ r = rename_noreplace(fd, skip_slash(source2), fd, skip_slash(target));
+ if (r == -EEXIST)
+ goto exists;
+ if (r == -ENOENT) {
+
+ if (faccessat(fd, skip_slash(target), F_OK, 0) >= 0) /* Hmm, if we can't find either source file, maybe the destination already exists? */
+ goto exists;
+
+ if (errno != ENOENT)
+ return log_error_errno(errno, "Failed to determine if %s already exists: %m", target);
+
+ /* We found none of the snippets here, try the next directory */
+ continue;
+ }
+ if (r < 0)
+ return log_error_errno(r, "Failed to rename '%s' to '%s': %m", source2, target);
+
+ log_debug("Successfully renamed '%s' to '%s'.", source2, target);
+ } else if (r < 0)
+ return log_error_errno(r, "Failed to rename '%s' to '%s': %m", source1, target);
+ else
+ log_debug("Successfully renamed '%s' to '%s'.", source1, target);
+
+ /* First, fsync() the directory these files are located in */
+ r = fsync_parent_at(fd, skip_slash(target));
+ if (r < 0)
+ log_debug_errno(errno, "Failed to synchronize image directory, ignoring: %m");
+
+ /* Secondly, syncfs() the whole file system these files are located in */
+ if (syncfs(fd) < 0)
+ log_debug_errno(errno, "Failed to synchronize $BOOT partition, ignoring: %m");
+
+ log_info("Marked boot as '%s'. (Boot attempt counter is at %" PRIu64".)", argv[0], done);
+ return 0;
+ }
+
+ log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Can't find boot counter source file for '%s': %m", target);
+ return 1;
+
+exists:
+ log_debug("Operation already executed before, not doing anything.");
+ return 0;
+}
+
+static int run(int argc, char *argv[]) {
+ static const Verb verbs[] = {
+ { "help", VERB_ANY, VERB_ANY, 0, help },
+ { "status", VERB_ANY, 1, VERB_DEFAULT, verb_status },
+ { "good", VERB_ANY, 1, 0, verb_set },
+ { "bad", VERB_ANY, 1, 0, verb_set },
+ { "indeterminate", VERB_ANY, 1, 0, verb_set },
+ {}
+ };
+
+ int r;
+
+ log_parse_environment();
+ log_open();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ if (detect_container() > 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "Marking a boot is not supported in containers.");
+
+ if (!is_efi_boot())
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "Marking a boot is only supported on EFI systems.");
+
+ return dispatch_verb(argc, argv, verbs, NULL);
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/boot/boot-check-no-failures.c b/src/boot/boot-check-no-failures.c
new file mode 100644
index 0000000..7864206
--- /dev/null
+++ b/src/boot/boot-check-no-failures.c
@@ -0,0 +1,113 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "sd-bus.h"
+
+#include "alloc-util.h"
+#include "bus-error.h"
+#include "log.h"
+#include "main-func.h"
+#include "pretty-print.h"
+#include "terminal-util.h"
+#include "util.h"
+
+static int help(void) {
+ _cleanup_free_ char *link = NULL;
+ int r;
+
+ r = terminal_urlify_man("systemd-boot-check-no-failures.service", "8", &link);
+ if (r < 0)
+ return log_oom();
+
+ printf("%s [OPTIONS...]\n"
+ "\n%sVerify system operational state.%s\n\n"
+ " -h --help Show this help\n"
+ " --version Print version\n"
+ "\nSee the %s for details.\n",
+ program_invocation_short_name,
+ ansi_highlight(),
+ ansi_normal(),
+ link);
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+ enum {
+ ARG_PATH = 0x100,
+ ARG_VERSION,
+ };
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, ARG_VERSION },
+ {}
+ };
+
+ int c;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0)
+ switch (c) {
+
+ case 'h':
+ help();
+ return 0;
+
+ case ARG_VERSION:
+ return version();
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached();
+ }
+
+ return 1;
+}
+
+static int run(int argc, char *argv[]) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ uint32_t n;
+ int r;
+
+ log_parse_environment();
+ log_open();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ r = sd_bus_open_system(&bus);
+ if (r < 0)
+ return log_error_errno(r, "Failed to connect to system bus: %m");
+
+ r = sd_bus_get_property_trivial(
+ bus,
+ "org.freedesktop.systemd1",
+ "/org/freedesktop/systemd1",
+ "org.freedesktop.systemd1.Manager",
+ "NFailedUnits",
+ &error,
+ 'u',
+ &n);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get failed units counter: %s", bus_error_message(&error, r));
+
+ if (n > 0)
+ log_notice("Health check: %" PRIu32 " units have failed.", n);
+ else
+ log_info("Health check: no failed units.");
+
+ return n > 0;
+}
+
+DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run);
diff --git a/src/boot/bootctl.c b/src/boot/bootctl.c
new file mode 100644
index 0000000..61eed61
--- /dev/null
+++ b/src/boot/bootctl.c
@@ -0,0 +1,2636 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <ctype.h>
+#include <errno.h>
+#include <getopt.h>
+#include <limits.h>
+#include <linux/magic.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+#include "sd-id128.h"
+
+#include "alloc-util.h"
+#include "blkid-util.h"
+#include "bootspec.h"
+#include "chase-symlinks.h"
+#include "copy.h"
+#include "devnum-util.h"
+#include "dirent-util.h"
+#include "dissect-image.h"
+#include "efi-api.h"
+#include "efi-loader.h"
+#include "efivars.h"
+#include "env-file.h"
+#include "env-util.h"
+#include "escape.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "find-esp.h"
+#include "fs-util.h"
+#include "glyph-util.h"
+#include "main-func.h"
+#include "mkdir.h"
+#include "mount-util.h"
+#include "os-util.h"
+#include "pager.h"
+#include "parse-argument.h"
+#include "parse-util.h"
+#include "pretty-print.h"
+#include "random-util.h"
+#include "rm-rf.h"
+#include "stat-util.h"
+#include "stdio-util.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "strv.h"
+#include "sync-util.h"
+#include "terminal-util.h"
+#include "tmpfile-util.h"
+#include "tmpfile-util-label.h"
+#include "tpm2-util.h"
+#include "umask-util.h"
+#include "utf8.h"
+#include "util.h"
+#include "verbs.h"
+#include "virt.h"
+
+/* EFI_BOOT_OPTION_DESCRIPTION_MAX sets the maximum length for the boot option description
+ * stored in NVRAM. The UEFI spec does not specify a minimum or maximum length for this
+ * string, but we limit the length to something reasonable to prevent from the firmware
+ * having to deal with a potentially too long string. */
+#define EFI_BOOT_OPTION_DESCRIPTION_MAX ((size_t) 255)
+
+static char *arg_esp_path = NULL;
+static char *arg_xbootldr_path = NULL;
+static bool arg_print_esp_path = false;
+static bool arg_print_dollar_boot_path = false;
+static bool arg_touch_variables = true;
+static PagerFlags arg_pager_flags = 0;
+static bool arg_graceful = false;
+static bool arg_quiet = false;
+static int arg_make_entry_directory = false; /* tri-state: < 0 for automatic logic */
+static sd_id128_t arg_machine_id = SD_ID128_NULL;
+static char *arg_install_layout = NULL;
+static enum {
+ ARG_ENTRY_TOKEN_MACHINE_ID,
+ ARG_ENTRY_TOKEN_OS_IMAGE_ID,
+ ARG_ENTRY_TOKEN_OS_ID,
+ ARG_ENTRY_TOKEN_LITERAL,
+ ARG_ENTRY_TOKEN_AUTO,
+} arg_entry_token_type = ARG_ENTRY_TOKEN_AUTO;
+static char *arg_entry_token = NULL;
+static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF;
+static bool arg_arch_all = false;
+static char *arg_root = NULL;
+static char *arg_image = NULL;
+static enum {
+ ARG_INSTALL_SOURCE_IMAGE,
+ ARG_INSTALL_SOURCE_HOST,
+ ARG_INSTALL_SOURCE_AUTO,
+} arg_install_source = ARG_INSTALL_SOURCE_AUTO;
+static char *arg_efi_boot_option_description = NULL;
+
+STATIC_DESTRUCTOR_REGISTER(arg_esp_path, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_xbootldr_path, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_install_layout, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_entry_token, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_image, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_efi_boot_option_description, freep);
+
+static const char *arg_dollar_boot_path(void) {
+ /* $BOOT shall be the XBOOTLDR partition if it exists, and otherwise the ESP */
+ return arg_xbootldr_path ?: arg_esp_path;
+}
+
+static const char *pick_efi_boot_option_description(void) {
+ return arg_efi_boot_option_description ?: "Linux Boot Manager";
+}
+
+static int acquire_esp(
+ bool unprivileged_mode,
+ bool graceful,
+ uint32_t *ret_part,
+ uint64_t *ret_pstart,
+ uint64_t *ret_psize,
+ sd_id128_t *ret_uuid,
+ dev_t *ret_devid) {
+
+ char *np;
+ int r;
+
+ /* Find the ESP, and log about errors. Note that find_esp_and_warn() will log in all error cases on
+ * its own, except for ENOKEY (which is good, we want to show our own message in that case,
+ * suggesting use of --esp-path=) and EACCESS (only when we request unprivileged mode; in this case
+ * we simply eat up the error here, so that --list and --status work too, without noise about
+ * this). */
+
+ r = find_esp_and_warn(arg_root, arg_esp_path, unprivileged_mode, &np, ret_part, ret_pstart, ret_psize, ret_uuid, ret_devid);
+ if (r == -ENOKEY) {
+ if (graceful)
+ return log_full_errno(arg_quiet ? LOG_DEBUG : LOG_INFO, r,
+ "Couldn't find EFI system partition, skipping.");
+
+ return log_error_errno(r,
+ "Couldn't find EFI system partition. It is recommended to mount it to /boot or /efi.\n"
+ "Alternatively, use --esp-path= to specify path to mount point.");
+ }
+ if (r < 0)
+ return r;
+
+ free_and_replace(arg_esp_path, np);
+ log_debug("Using EFI System Partition at %s.", arg_esp_path);
+
+ return 0;
+}
+
+static int acquire_xbootldr(
+ bool unprivileged_mode,
+ sd_id128_t *ret_uuid,
+ dev_t *ret_devid) {
+
+ char *np;
+ int r;
+
+ r = find_xbootldr_and_warn(arg_root, arg_xbootldr_path, unprivileged_mode, &np, ret_uuid, ret_devid);
+ if (r == -ENOKEY) {
+ log_debug_errno(r, "Didn't find an XBOOTLDR partition, using the ESP as $BOOT.");
+ arg_xbootldr_path = mfree(arg_xbootldr_path);
+
+ if (ret_uuid)
+ *ret_uuid = SD_ID128_NULL;
+ if (ret_devid)
+ *ret_devid = 0;
+ return 0;
+ }
+ if (r < 0)
+ return r;
+
+ free_and_replace(arg_xbootldr_path, np);
+ log_debug("Using XBOOTLDR partition at %s as $BOOT.", arg_xbootldr_path);
+
+ return 1;
+}
+
+static int load_etc_machine_id(void) {
+ int r;
+
+ r = sd_id128_get_machine(&arg_machine_id);
+ if (IN_SET(r, -ENOENT, -ENOMEDIUM, -ENOPKG)) /* Not set or empty */
+ return 0;
+ if (r < 0)
+ return log_error_errno(r, "Failed to get machine-id: %m");
+
+ log_debug("Loaded machine ID %s from /etc/machine-id.", SD_ID128_TO_STRING(arg_machine_id));
+ return 0;
+}
+
+static int load_etc_machine_info(void) {
+ /* systemd v250 added support to store the kernel-install layout setting and the machine ID to use
+ * for setting up the ESP in /etc/machine-info. The newer /etc/kernel/entry-token file, as well as
+ * the $layout field in /etc/kernel/install.conf are better replacements for this though, hence this
+ * has been deprecated and is only returned for compatibility. */
+ _cleanup_free_ char *s = NULL, *layout = NULL;
+ int r;
+
+ r = parse_env_file(NULL, "/etc/machine-info",
+ "KERNEL_INSTALL_LAYOUT", &layout,
+ "KERNEL_INSTALL_MACHINE_ID", &s);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse /etc/machine-info: %m");
+
+ if (!isempty(s)) {
+ if (!arg_quiet)
+ log_notice("Read $KERNEL_INSTALL_MACHINE_ID from /etc/machine-info. "
+ "Please move it to /etc/kernel/entry-token.");
+
+ r = sd_id128_from_string(s, &arg_machine_id);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse KERNEL_INSTALL_MACHINE_ID=%s in /etc/machine-info: %m", s);
+
+ log_debug("Loaded KERNEL_INSTALL_MACHINE_ID=%s from KERNEL_INSTALL_MACHINE_ID in /etc/machine-info.",
+ SD_ID128_TO_STRING(arg_machine_id));
+ }
+
+ if (!isempty(layout)) {
+ if (!arg_quiet)
+ log_notice("Read $KERNEL_INSTALL_LAYOUT from /etc/machine-info. "
+ "Please move it to the layout= setting of /etc/kernel/install.conf.");
+
+ log_debug("KERNEL_INSTALL_LAYOUT=%s is specified in /etc/machine-info.", layout);
+ free_and_replace(arg_install_layout, layout);
+ }
+
+ return 0;
+}
+
+static int load_etc_kernel_install_conf(void) {
+ _cleanup_free_ char *layout = NULL;
+ int r;
+
+ r = parse_env_file(NULL, "/etc/kernel/install.conf",
+ "layout", &layout);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse /etc/kernel/install.conf: %m");
+
+ if (!isempty(layout)) {
+ log_debug("layout=%s is specified in /etc/machine-info.", layout);
+ free_and_replace(arg_install_layout, layout);
+ }
+
+ return 0;
+}
+
+static int settle_entry_token(void) {
+ int r;
+
+ switch (arg_entry_token_type) {
+
+ case ARG_ENTRY_TOKEN_AUTO: {
+ _cleanup_free_ char *buf = NULL;
+ r = read_one_line_file("/etc/kernel/entry-token", &buf);
+ if (r < 0 && r != -ENOENT)
+ return log_error_errno(r, "Failed to read /etc/kernel/entry-token: %m");
+
+ if (!isempty(buf)) {
+ free_and_replace(arg_entry_token, buf);
+ arg_entry_token_type = ARG_ENTRY_TOKEN_LITERAL;
+ } else if (sd_id128_is_null(arg_machine_id)) {
+ _cleanup_free_ char *id = NULL, *image_id = NULL;
+
+ r = parse_os_release(NULL,
+ "IMAGE_ID", &image_id,
+ "ID", &id);
+ if (r < 0)
+ return log_error_errno(r, "Failed to load /etc/os-release: %m");
+
+ if (!isempty(image_id)) {
+ free_and_replace(arg_entry_token, image_id);
+ arg_entry_token_type = ARG_ENTRY_TOKEN_OS_IMAGE_ID;
+ } else if (!isempty(id)) {
+ free_and_replace(arg_entry_token, id);
+ arg_entry_token_type = ARG_ENTRY_TOKEN_OS_ID;
+ } else
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No machine ID set, and /etc/os-release carries no ID=/IMAGE_ID= fields.");
+ } else {
+ r = free_and_strdup_warn(&arg_entry_token, SD_ID128_TO_STRING(arg_machine_id));
+ if (r < 0)
+ return r;
+
+ arg_entry_token_type = ARG_ENTRY_TOKEN_MACHINE_ID;
+ }
+
+ break;
+ }
+
+ case ARG_ENTRY_TOKEN_MACHINE_ID:
+ if (sd_id128_is_null(arg_machine_id))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No machine ID set.");
+
+ r = free_and_strdup_warn(&arg_entry_token, SD_ID128_TO_STRING(arg_machine_id));
+ if (r < 0)
+ return r;
+
+ break;
+
+ case ARG_ENTRY_TOKEN_OS_IMAGE_ID: {
+ _cleanup_free_ char *buf = NULL;
+
+ r = parse_os_release(NULL, "IMAGE_ID", &buf);
+ if (r < 0)
+ return log_error_errno(r, "Failed to load /etc/os-release: %m");
+
+ if (isempty(buf))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "IMAGE_ID= field not set in /etc/os-release.");
+
+ free_and_replace(arg_entry_token, buf);
+ break;
+ }
+
+ case ARG_ENTRY_TOKEN_OS_ID: {
+ _cleanup_free_ char *buf = NULL;
+
+ r = parse_os_release(NULL, "ID", &buf);
+ if (r < 0)
+ return log_error_errno(r, "Failed to load /etc/os-release: %m");
+
+ if (isempty(buf))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "ID= field not set in /etc/os-release.");
+
+ free_and_replace(arg_entry_token, buf);
+ break;
+ }
+
+ case ARG_ENTRY_TOKEN_LITERAL:
+ assert(!isempty(arg_entry_token)); /* already filled in by command line parser */
+ break;
+ }
+
+ if (isempty(arg_entry_token) || !(utf8_is_valid(arg_entry_token) && string_is_safe(arg_entry_token)))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected entry token not valid: %s", arg_entry_token);
+
+ log_debug("Using entry token: %s", arg_entry_token);
+ return 0;
+}
+
+static bool use_boot_loader_spec_type1(void) {
+ /* If the layout is not specified, or if it is set explicitly to "bls" we assume Boot Loader
+ * Specification Type #1 is the chosen format for our boot loader entries */
+ return !arg_install_layout || streq(arg_install_layout, "bls");
+}
+
+static int settle_make_entry_directory(void) {
+ int r;
+
+ r = load_etc_machine_id();
+ if (r < 0)
+ return r;
+
+ r = load_etc_machine_info();
+ if (r < 0)
+ return r;
+
+ r = load_etc_kernel_install_conf();
+ if (r < 0)
+ return r;
+
+ r = settle_entry_token();
+ if (r < 0)
+ return r;
+
+ bool layout_type1 = use_boot_loader_spec_type1();
+ if (arg_make_entry_directory < 0) { /* Automatic mode */
+ if (layout_type1) {
+ if (arg_entry_token_type == ARG_ENTRY_TOKEN_MACHINE_ID) {
+ r = path_is_temporary_fs("/etc/machine-id");
+ if (r < 0)
+ return log_debug_errno(r, "Couldn't determine whether /etc/machine-id is on a temporary file system: %m");
+
+ arg_make_entry_directory = r == 0;
+ } else
+ arg_make_entry_directory = true;
+ } else
+ arg_make_entry_directory = false;
+ }
+
+ if (arg_make_entry_directory > 0 && !layout_type1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "KERNEL_INSTALL_LAYOUT=%s is configured, but Boot Loader Specification Type #1 entry directory creation was requested.",
+ arg_install_layout);
+
+ return 0;
+}
+
+/* search for "#### LoaderInfo: systemd-boot 218 ####" string inside the binary */
+static int get_file_version(int fd, char **v) {
+ struct stat st;
+ char *buf;
+ const char *s, *e;
+ char *x = NULL;
+ int r;
+
+ assert(fd >= 0);
+ assert(v);
+
+ if (fstat(fd, &st) < 0)
+ return log_error_errno(errno, "Failed to stat EFI binary: %m");
+
+ r = stat_verify_regular(&st);
+ if (r < 0)
+ return log_error_errno(r, "EFI binary is not a regular file: %m");
+
+ if (st.st_size < 27 || file_offset_beyond_memory_size(st.st_size)) {
+ *v = NULL;
+ return 0;
+ }
+
+ buf = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
+ if (buf == MAP_FAILED)
+ return log_error_errno(errno, "Failed to memory map EFI binary: %m");
+
+ s = mempmem_safe(buf, st.st_size - 8, "#### LoaderInfo: ", 17);
+ if (!s)
+ goto finish;
+
+ e = memmem_safe(s, st.st_size - (s - buf), " ####", 5);
+ if (!e || e - s < 3) {
+ r = log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Malformed version string.");
+ goto finish;
+ }
+
+ x = strndup(s, e - s);
+ if (!x) {
+ r = log_oom();
+ goto finish;
+ }
+ r = 1;
+
+finish:
+ (void) munmap(buf, st.st_size);
+ *v = x;
+ return r;
+}
+
+static const char *get_efi_arch(void) {
+ /* Detect EFI firmware architecture of the running system. On mixed mode systems, it could be 32bit
+ * while the kernel is running in 64bit. */
+
+#ifdef __x86_64__
+ _cleanup_free_ char *platform_size = NULL;
+ int r;
+
+ r = read_one_line_file("/sys/firmware/efi/fw_platform_size", &platform_size);
+ if (r == -ENOENT)
+ return EFI_MACHINE_TYPE_NAME;
+ if (r < 0) {
+ log_warning_errno(r,
+ "Error reading EFI firmware word size, assuming machine type '%s': %m",
+ EFI_MACHINE_TYPE_NAME);
+ return EFI_MACHINE_TYPE_NAME;
+ }
+
+ if (streq(platform_size, "64"))
+ return EFI_MACHINE_TYPE_NAME;
+ if (streq(platform_size, "32"))
+ return "ia32";
+
+ log_warning(
+ "Unknown EFI firmware word size '%s', using machine type '%s'.",
+ platform_size,
+ EFI_MACHINE_TYPE_NAME);
+#endif
+
+ return EFI_MACHINE_TYPE_NAME;
+}
+
+static int enumerate_binaries(
+ const char *esp_path,
+ const char *path,
+ const char *prefix,
+ char **previous,
+ bool *is_first) {
+
+ _cleanup_closedir_ DIR *d = NULL;
+ _cleanup_free_ char *p = NULL;
+ int c = 0, r;
+
+ assert(esp_path);
+ assert(path);
+ assert(previous);
+ assert(is_first);
+
+ r = chase_symlinks_and_opendir(path, esp_path, CHASE_PREFIX_ROOT, &p, &d);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
+ return log_error_errno(r, "Failed to read \"%s/%s\": %m", esp_path, path);
+
+ FOREACH_DIRENT(de, d, break) {
+ _cleanup_free_ char *v = NULL;
+ _cleanup_close_ int fd = -1;
+
+ if (!endswith_no_case(de->d_name, ".efi"))
+ continue;
+
+ if (prefix && !startswith_no_case(de->d_name, prefix))
+ continue;
+
+ fd = openat(dirfd(d), de->d_name, O_RDONLY|O_CLOEXEC);
+ if (fd < 0)
+ return log_error_errno(errno, "Failed to open \"%s/%s\" for reading: %m", p, de->d_name);
+
+ r = get_file_version(fd, &v);
+ if (r < 0)
+ return r;
+
+ if (*previous) { /* let's output the previous entry now, since now we know that there will be one more, and can draw the tree glyph properly */
+ printf(" %s %s%s\n",
+ *is_first ? "File:" : " ",
+ special_glyph(SPECIAL_GLYPH_TREE_BRANCH), *previous);
+ *is_first = false;
+ *previous = mfree(*previous);
+ }
+
+ /* Do not output this entry immediately, but store what should be printed in a state
+ * variable, because we only will know the tree glyph to print (branch or final edge) once we
+ * read one more entry */
+ if (r > 0)
+ r = asprintf(previous, "/%s/%s (%s%s%s)", path, de->d_name, ansi_highlight(), v, ansi_normal());
+ else
+ r = asprintf(previous, "/%s/%s", path, de->d_name);
+ if (r < 0)
+ return log_oom();
+
+ c++;
+ }
+
+ return c;
+}
+
+static int status_binaries(const char *esp_path, sd_id128_t partition) {
+ _cleanup_free_ char *last = NULL;
+ bool is_first = true;
+ int r, k;
+
+ printf("%sAvailable Boot Loaders on ESP:%s\n", ansi_underline(), ansi_normal());
+
+ if (!esp_path) {
+ printf(" ESP: Cannot find or access mount point of ESP.\n\n");
+ return -ENOENT;
+ }
+
+ printf(" ESP: %s", esp_path);
+ if (!sd_id128_is_null(partition))
+ printf(" (/dev/disk/by-partuuid/" SD_ID128_UUID_FORMAT_STR ")", SD_ID128_FORMAT_VAL(partition));
+ printf("\n");
+
+ r = enumerate_binaries(esp_path, "EFI/systemd", NULL, &last, &is_first);
+ if (r < 0) {
+ printf("\n");
+ return r;
+ }
+
+ k = enumerate_binaries(esp_path, "EFI/BOOT", "boot", &last, &is_first);
+ if (k < 0) {
+ printf("\n");
+ return k;
+ }
+
+ if (last) /* let's output the last entry now, since now we know that there will be no more, and can draw the tree glyph properly */
+ printf(" %s %s%s\n",
+ is_first ? "File:" : " ",
+ special_glyph(SPECIAL_GLYPH_TREE_RIGHT), last);
+
+ if (r == 0 && !arg_quiet)
+ log_info("systemd-boot not installed in ESP.");
+ if (k == 0 && !arg_quiet)
+ log_info("No default/fallback boot loader installed in ESP.");
+
+ printf("\n");
+ return 0;
+}
+
+static int print_efi_option(uint16_t id, int *n_printed, bool in_order) {
+ _cleanup_free_ char *title = NULL;
+ _cleanup_free_ char *path = NULL;
+ sd_id128_t partition;
+ bool active;
+ int r;
+
+ assert(n_printed);
+
+ r = efi_get_boot_option(id, &title, &partition, &path, &active);
+ if (r == -ENOENT) {
+ log_debug_errno(r, "Boot option 0x%04X referenced but missing, ignoring: %m", id);
+ return 0;
+ }
+ if (r < 0)
+ return log_error_errno(r, "Failed to read boot option 0x%04X: %m", id);
+
+ /* print only configured entries with partition information */
+ if (!path || sd_id128_is_null(partition)) {
+ log_debug("Ignoring boot entry 0x%04X without partition information.", id);
+ return 0;
+ }
+
+ efi_tilt_backslashes(path);
+
+ if (*n_printed == 0) /* Print section title before first entry */
+ printf("%sBoot Loaders Listed in EFI Variables:%s\n", ansi_underline(), ansi_normal());
+
+ printf(" Title: %s%s%s\n", ansi_highlight(), strna(title), ansi_normal());
+ printf(" ID: 0x%04X\n", id);
+ printf(" Status: %sactive%s\n", active ? "" : "in", in_order ? ", boot-order" : "");
+ printf(" Partition: /dev/disk/by-partuuid/" SD_ID128_UUID_FORMAT_STR "\n",
+ SD_ID128_FORMAT_VAL(partition));
+ printf(" File: %s%s\n", special_glyph(SPECIAL_GLYPH_TREE_RIGHT), path);
+ printf("\n");
+
+ (*n_printed)++;
+ return 1;
+}
+
+static int status_variables(void) {
+ _cleanup_free_ uint16_t *options = NULL, *order = NULL;
+ int n_options, n_order, n_printed = 0;
+
+ n_options = efi_get_boot_options(&options);
+ if (n_options == -ENOENT)
+ return log_error_errno(n_options,
+ "Failed to access EFI variables, efivarfs"
+ " needs to be available at /sys/firmware/efi/efivars/.");
+ if (n_options < 0)
+ return log_error_errno(n_options, "Failed to read EFI boot entries: %m");
+
+ n_order = efi_get_boot_order(&order);
+ if (n_order == -ENOENT)
+ n_order = 0;
+ else if (n_order < 0)
+ return log_error_errno(n_order, "Failed to read EFI boot order: %m");
+
+ /* print entries in BootOrder first */
+ for (int i = 0; i < n_order; i++)
+ (void) print_efi_option(order[i], &n_printed, /* in_order= */ true);
+
+ /* print remaining entries */
+ for (int i = 0; i < n_options; i++) {
+ for (int j = 0; j < n_order; j++)
+ if (options[i] == order[j])
+ goto next_option;
+
+ (void) print_efi_option(options[i], &n_printed, /* in_order= */ false);
+
+ next_option:
+ continue;
+ }
+
+ if (n_printed == 0)
+ printf("No boot loaders listed in EFI Variables.\n\n");
+
+ return 0;
+}
+
+static int boot_config_load_and_select(
+ BootConfig *config,
+ const char *esp_path,
+ dev_t esp_devid,
+ const char *xbootldr_path,
+ dev_t xbootldr_devid) {
+
+ int r;
+
+ /* If XBOOTLDR and ESP actually refer to the same block device, suppress XBOOTLDR, since it would
+ * find the same entries twice. */
+ bool same = esp_path && xbootldr_path && devnum_set_and_equal(esp_devid, xbootldr_devid);
+
+ r = boot_config_load(config, esp_path, same ? NULL : xbootldr_path);
+ if (r < 0)
+ return r;
+
+ if (!arg_root) {
+ _cleanup_strv_free_ char **efi_entries = NULL;
+
+ r = efi_loader_get_entries(&efi_entries);
+ if (r == -ENOENT || ERRNO_IS_NOT_SUPPORTED(r))
+ log_debug_errno(r, "Boot loader reported no entries.");
+ else if (r < 0)
+ log_warning_errno(r, "Failed to determine entries reported by boot loader, ignoring: %m");
+ else
+ (void) boot_config_augment_from_loader(config, efi_entries, /* only_auto= */ false);
+ }
+
+ return boot_config_select_special_entries(config, /* skip_efivars= */ !!arg_root);
+}
+
+static int status_entries(
+ const BootConfig *config,
+ const char *esp_path,
+ sd_id128_t esp_partition_uuid,
+ const char *xbootldr_path,
+ sd_id128_t xbootldr_partition_uuid) {
+
+ sd_id128_t dollar_boot_partition_uuid;
+ const char *dollar_boot_path;
+ int r;
+
+ assert(config);
+ assert(esp_path || xbootldr_path);
+
+ if (xbootldr_path) {
+ dollar_boot_path = xbootldr_path;
+ dollar_boot_partition_uuid = xbootldr_partition_uuid;
+ } else {
+ dollar_boot_path = esp_path;
+ dollar_boot_partition_uuid = esp_partition_uuid;
+ }
+
+ printf("%sBoot Loader Entries:%s\n"
+ " $BOOT: %s", ansi_underline(), ansi_normal(), dollar_boot_path);
+ if (!sd_id128_is_null(dollar_boot_partition_uuid))
+ printf(" (/dev/disk/by-partuuid/" SD_ID128_UUID_FORMAT_STR ")",
+ SD_ID128_FORMAT_VAL(dollar_boot_partition_uuid));
+ printf("\n\n");
+
+ if (config->default_entry < 0)
+ printf("%zu entries, no entry could be determined as default.\n", config->n_entries);
+ else {
+ printf("%sDefault Boot Loader Entry:%s\n", ansi_underline(), ansi_normal());
+
+ r = show_boot_entry(
+ boot_config_default_entry(config),
+ /* show_as_default= */ false,
+ /* show_as_selected= */ false,
+ /* show_discovered= */ false);
+ if (r > 0)
+ /* < 0 is already logged by the function itself, let's just emit an extra warning if
+ the default entry is broken */
+ printf("\nWARNING: default boot entry is broken\n");
+ }
+
+ return 0;
+}
+
+static int compare_product(const char *a, const char *b) {
+ size_t x, y;
+
+ assert(a);
+ assert(b);
+
+ x = strcspn(a, " ");
+ y = strcspn(b, " ");
+ if (x != y)
+ return x < y ? -1 : x > y ? 1 : 0;
+
+ return strncmp(a, b, x);
+}
+
+static int compare_version(const char *a, const char *b) {
+ assert(a);
+ assert(b);
+
+ a += strcspn(a, " ");
+ a += strspn(a, " ");
+ b += strcspn(b, " ");
+ b += strspn(b, " ");
+
+ return strverscmp_improved(a, b);
+}
+
+static int version_check(int fd_from, const char *from, int fd_to, const char *to) {
+ _cleanup_free_ char *a = NULL, *b = NULL;
+ int r;
+
+ assert(fd_from >= 0);
+ assert(from);
+ assert(fd_to >= 0);
+ assert(to);
+
+ r = get_file_version(fd_from, &a);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return log_notice_errno(SYNTHETIC_ERRNO(EREMOTE),
+ "Source file \"%s\" does not carry version information!",
+ from);
+
+ r = get_file_version(fd_to, &b);
+ if (r < 0)
+ return r;
+ if (r == 0 || compare_product(a, b) != 0)
+ return log_notice_errno(SYNTHETIC_ERRNO(EREMOTE),
+ "Skipping \"%s\", since it's owned by another boot loader.",
+ to);
+
+ r = compare_version(a, b);
+ log_debug("Comparing versions: \"%s\" %s \"%s", a, comparison_operator(r), b);
+ if (r < 0)
+ return log_warning_errno(SYNTHETIC_ERRNO(ESTALE),
+ "Skipping \"%s\", since newer boot loader version in place already.", to);
+ if (r == 0)
+ return log_info_errno(SYNTHETIC_ERRNO(ESTALE),
+ "Skipping \"%s\", since same boot loader version in place already.", to);
+
+ return 0;
+}
+
+static int copy_file_with_version_check(const char *from, const char *to, bool force) {
+ _cleanup_close_ int fd_from = -1, fd_to = -1;
+ _cleanup_free_ char *t = NULL;
+ int r;
+
+ fd_from = open(from, O_RDONLY|O_CLOEXEC|O_NOCTTY);
+ if (fd_from < 0)
+ return log_error_errno(errno, "Failed to open \"%s\" for reading: %m", from);
+
+ if (!force) {
+ fd_to = open(to, O_RDONLY|O_CLOEXEC|O_NOCTTY);
+ if (fd_to < 0) {
+ if (errno != ENOENT)
+ return log_error_errno(errno, "Failed to open \"%s\" for reading: %m", to);
+ } else {
+ r = version_check(fd_from, from, fd_to, to);
+ if (r < 0)
+ return r;
+
+ if (lseek(fd_from, 0, SEEK_SET) == (off_t) -1)
+ return log_error_errno(errno, "Failed to seek in \"%s\": %m", from);
+
+ fd_to = safe_close(fd_to);
+ }
+ }
+
+ r = tempfn_random(to, NULL, &t);
+ if (r < 0)
+ return log_oom();
+
+ RUN_WITH_UMASK(0000) {
+ fd_to = open(t, O_WRONLY|O_CREAT|O_CLOEXEC|O_EXCL|O_NOFOLLOW, 0644);
+ if (fd_to < 0)
+ return log_error_errno(errno, "Failed to open \"%s\" for writing: %m", t);
+ }
+
+ r = copy_bytes(fd_from, fd_to, UINT64_MAX, COPY_REFLINK);
+ if (r < 0) {
+ (void) unlink(t);
+ return log_error_errno(r, "Failed to copy data from \"%s\" to \"%s\": %m", from, t);
+ }
+
+ (void) copy_times(fd_from, fd_to, 0);
+
+ r = fsync_full(fd_to);
+ if (r < 0) {
+ (void) unlink_noerrno(t);
+ return log_error_errno(r, "Failed to copy data from \"%s\" to \"%s\": %m", from, t);
+ }
+
+ if (renameat(AT_FDCWD, t, AT_FDCWD, to) < 0) {
+ (void) unlink_noerrno(t);
+ return log_error_errno(errno, "Failed to rename \"%s\" to \"%s\": %m", t, to);
+ }
+
+ log_info("Copied \"%s\" to \"%s\".", from, to);
+
+ return 0;
+}
+
+static int mkdir_one(const char *prefix, const char *suffix) {
+ _cleanup_free_ char *p = NULL;
+
+ p = path_join(prefix, suffix);
+ if (mkdir(p, 0700) < 0) {
+ if (errno != EEXIST)
+ return log_error_errno(errno, "Failed to create \"%s\": %m", p);
+ } else
+ log_info("Created \"%s\".", p);
+
+ return 0;
+}
+
+static const char *const esp_subdirs[] = {
+ /* The directories to place in the ESP */
+ "EFI",
+ "EFI/systemd",
+ "EFI/BOOT",
+ "loader",
+ NULL
+};
+
+static const char *const dollar_boot_subdirs[] = {
+ /* The directories to place in the XBOOTLDR partition or the ESP, depending what exists */
+ "loader",
+ "loader/entries", /* Type #1 entries */
+ "EFI",
+ "EFI/Linux", /* Type #2 entries */
+ NULL
+};
+
+static int create_subdirs(const char *root, const char * const *subdirs) {
+ int r;
+
+ STRV_FOREACH(i, subdirs) {
+ r = mkdir_one(root, *i);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int copy_one_file(const char *esp_path, const char *name, bool force) {
+ char *root = IN_SET(arg_install_source, ARG_INSTALL_SOURCE_AUTO, ARG_INSTALL_SOURCE_IMAGE) ? arg_root : NULL;
+ _cleanup_free_ char *source_path = NULL, *dest_path = NULL, *p = NULL, *q = NULL;
+ const char *e;
+ char *dest_name, *s;
+ int r, ret;
+
+ dest_name = strdupa_safe(name);
+ s = endswith_no_case(dest_name, ".signed");
+ if (s)
+ *s = 0;
+
+ p = path_join(BOOTLIBDIR, name);
+ if (!p)
+ return log_oom();
+
+ r = chase_symlinks(p, root, CHASE_PREFIX_ROOT, &source_path, NULL);
+ /* If we had a root directory to try, we didn't find it and we are in auto mode, retry on the host */
+ if (r == -ENOENT && root && arg_install_source == ARG_INSTALL_SOURCE_AUTO)
+ r = chase_symlinks(p, NULL, CHASE_PREFIX_ROOT, &source_path, NULL);
+ if (r < 0)
+ return log_error_errno(r,
+ "Failed to resolve path %s%s%s: %m",
+ p,
+ root ? " under directory " : "",
+ strempty(root));
+
+ q = path_join("/EFI/systemd/", dest_name);
+ if (!q)
+ return log_oom();
+
+ r = chase_symlinks(q, esp_path, CHASE_PREFIX_ROOT | CHASE_NONEXISTENT, &dest_path, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to resolve path %s under directory %s: %m", q, esp_path);
+
+ /* Note that if this fails we do the second copy anyway, but return this error code,
+ * so we stash it away in a separate variable. */
+ ret = copy_file_with_version_check(source_path, dest_path, force);
+
+ e = startswith(dest_name, "systemd-boot");
+ if (e) {
+ _cleanup_free_ char *default_dest_path = NULL;
+ char *v;
+
+ /* Create the EFI default boot loader name (specified for removable devices) */
+ v = strjoina("/EFI/BOOT/BOOT", e);
+ ascii_strupper(strrchr(v, '/') + 1);
+
+ r = chase_symlinks(v, esp_path, CHASE_PREFIX_ROOT | CHASE_NONEXISTENT, &default_dest_path, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to resolve path %s under directory %s: %m", v, esp_path);
+
+ r = copy_file_with_version_check(source_path, default_dest_path, force);
+ if (r < 0 && ret == 0)
+ ret = r;
+ }
+
+ return ret;
+}
+
+static int install_binaries(const char *esp_path, const char *arch, bool force) {
+ char *root = IN_SET(arg_install_source, ARG_INSTALL_SOURCE_AUTO, ARG_INSTALL_SOURCE_IMAGE) ? arg_root : NULL;
+ _cleanup_closedir_ DIR *d = NULL;
+ _cleanup_free_ char *path = NULL;
+ int r;
+
+ r = chase_symlinks_and_opendir(BOOTLIBDIR, root, CHASE_PREFIX_ROOT, &path, &d);
+ /* If we had a root directory to try, we didn't find it and we are in auto mode, retry on the host */
+ if (r == -ENOENT && root && arg_install_source == ARG_INSTALL_SOURCE_AUTO)
+ r = chase_symlinks_and_opendir(BOOTLIBDIR, NULL, CHASE_PREFIX_ROOT, &path, &d);
+ if (r < 0)
+ return log_error_errno(r, "Failed to open boot loader directory %s%s: %m", strempty(root), BOOTLIBDIR);
+
+ const char *suffix = strjoina(arch, ".efi");
+ const char *suffix_signed = strjoina(arch, ".efi.signed");
+
+ FOREACH_DIRENT(de, d, return log_error_errno(errno, "Failed to read \"%s\": %m", path)) {
+ int k;
+
+ if (!endswith_no_case(de->d_name, suffix) && !endswith_no_case(de->d_name, suffix_signed))
+ continue;
+
+ /* skip the .efi file, if there's a .signed version of it */
+ if (endswith_no_case(de->d_name, ".efi")) {
+ _cleanup_free_ const char *s = strjoin(de->d_name, ".signed");
+ if (!s)
+ return log_oom();
+ if (faccessat(dirfd(d), s, F_OK, 0) >= 0)
+ continue;
+ }
+
+ k = copy_one_file(esp_path, de->d_name, force);
+ /* Don't propagate an error code if no update necessary, installed version already equal or
+ * newer version, or other boot loader in place. */
+ if (arg_graceful && IN_SET(k, -ESTALE, -EREMOTE))
+ continue;
+ if (k < 0 && r == 0)
+ r = k;
+ }
+
+ return r;
+}
+
+static bool same_entry(uint16_t id, sd_id128_t uuid, const char *path) {
+ _cleanup_free_ char *opath = NULL;
+ sd_id128_t ouuid;
+ int r;
+
+ r = efi_get_boot_option(id, NULL, &ouuid, &opath, NULL);
+ if (r < 0)
+ return false;
+ if (!sd_id128_equal(uuid, ouuid))
+ return false;
+
+ /* Some motherboards convert the path to uppercase under certain circumstances
+ * (e.g. after booting into the Boot Menu in the ASUS ROG STRIX B350-F GAMING),
+ * so use case-insensitive checking */
+ if (!strcaseeq_ptr(path, opath))
+ return false;
+
+ return true;
+}
+
+static int find_slot(sd_id128_t uuid, const char *path, uint16_t *id) {
+ _cleanup_free_ uint16_t *options = NULL;
+
+ int n = efi_get_boot_options(&options);
+ if (n < 0)
+ return n;
+
+ /* find already existing systemd-boot entry */
+ for (int i = 0; i < n; i++)
+ if (same_entry(options[i], uuid, path)) {
+ *id = options[i];
+ return 1;
+ }
+
+ /* find free slot in the sorted BootXXXX variable list */
+ for (int i = 0; i < n; i++)
+ if (i != options[i]) {
+ *id = i;
+ return 0;
+ }
+
+ /* use the next one */
+ if (n == 0xffff)
+ return -ENOSPC;
+ *id = n;
+ return 0;
+}
+
+static int insert_into_order(uint16_t slot, bool first) {
+ _cleanup_free_ uint16_t *order = NULL;
+ uint16_t *t;
+ int n;
+
+ n = efi_get_boot_order(&order);
+ if (n <= 0)
+ /* no entry, add us */
+ return efi_set_boot_order(&slot, 1);
+
+ /* are we the first and only one? */
+ if (n == 1 && order[0] == slot)
+ return 0;
+
+ /* are we already in the boot order? */
+ for (int i = 0; i < n; i++) {
+ if (order[i] != slot)
+ continue;
+
+ /* we do not require to be the first one, all is fine */
+ if (!first)
+ return 0;
+
+ /* move us to the first slot */
+ memmove(order + 1, order, i * sizeof(uint16_t));
+ order[0] = slot;
+ return efi_set_boot_order(order, n);
+ }
+
+ /* extend array */
+ t = reallocarray(order, n + 1, sizeof(uint16_t));
+ if (!t)
+ return -ENOMEM;
+ order = t;
+
+ /* add us to the top or end of the list */
+ if (first) {
+ memmove(order + 1, order, n * sizeof(uint16_t));
+ order[0] = slot;
+ } else
+ order[n] = slot;
+
+ return efi_set_boot_order(order, n + 1);
+}
+
+static int remove_from_order(uint16_t slot) {
+ _cleanup_free_ uint16_t *order = NULL;
+ int n;
+
+ n = efi_get_boot_order(&order);
+ if (n <= 0)
+ return n;
+
+ for (int i = 0; i < n; i++) {
+ if (order[i] != slot)
+ continue;
+
+ if (i + 1 < n)
+ memmove(order + i, order + i+1, (n - i) * sizeof(uint16_t));
+ return efi_set_boot_order(order, n - 1);
+ }
+
+ return 0;
+}
+
+static int install_variables(
+ const char *esp_path,
+ uint32_t part,
+ uint64_t pstart,
+ uint64_t psize,
+ sd_id128_t uuid,
+ const char *path,
+ bool first,
+ bool graceful) {
+
+ uint16_t slot;
+ int r;
+
+ if (arg_root) {
+ log_info("Acting on %s, skipping EFI variable setup.",
+ arg_image ? "image" : "root directory");
+ return 0;
+ }
+
+ if (!is_efi_boot()) {
+ log_warning("Not booted with EFI, skipping EFI variable setup.");
+ return 0;
+ }
+
+ r = chase_symlinks_and_access(path, esp_path, CHASE_PREFIX_ROOT, F_OK, NULL, NULL);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
+ return log_error_errno(r, "Cannot access \"%s/%s\": %m", esp_path, path);
+
+ r = find_slot(uuid, path, &slot);
+ if (r < 0) {
+ int level = graceful ? arg_quiet ? LOG_DEBUG : LOG_INFO : LOG_ERR;
+ const char *skip = graceful ? ", skipping" : "";
+
+ log_full_errno(level, r,
+ r == -ENOENT ?
+ "Failed to access EFI variables%s. Is the \"efivarfs\" filesystem mounted?" :
+ "Failed to determine current boot order%s: %m", skip);
+
+ return graceful ? 0 : r;
+ }
+
+ if (first || r == 0) {
+ r = efi_add_boot_option(slot, pick_efi_boot_option_description(),
+ part, pstart, psize,
+ uuid, path);
+ if (r < 0) {
+ int level = graceful ? arg_quiet ? LOG_DEBUG : LOG_INFO : LOG_ERR;
+ const char *skip = graceful ? ", skipping" : "";
+
+ log_full_errno(level, r, "Failed to create EFI Boot variable entry%s: %m", skip);
+
+ return graceful ? 0 : r;
+ }
+
+ log_info("Created EFI boot entry \"%s\".", pick_efi_boot_option_description());
+ }
+
+ return insert_into_order(slot, first);
+}
+
+static int remove_boot_efi(const char *esp_path) {
+ _cleanup_closedir_ DIR *d = NULL;
+ _cleanup_free_ char *p = NULL;
+ int r, c = 0;
+
+ r = chase_symlinks_and_opendir("/EFI/BOOT", esp_path, CHASE_PREFIX_ROOT, &p, &d);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
+ return log_error_errno(r, "Failed to open directory \"%s/EFI/BOOT\": %m", esp_path);
+
+ FOREACH_DIRENT(de, d, break) {
+ _cleanup_close_ int fd = -1;
+ _cleanup_free_ char *v = NULL;
+
+ if (!endswith_no_case(de->d_name, ".efi"))
+ continue;
+
+ if (!startswith_no_case(de->d_name, "boot"))
+ continue;
+
+ fd = openat(dirfd(d), de->d_name, O_RDONLY|O_CLOEXEC);
+ if (fd < 0)
+ return log_error_errno(errno, "Failed to open \"%s/%s\" for reading: %m", p, de->d_name);
+
+ r = get_file_version(fd, &v);
+ if (r < 0)
+ return r;
+ if (r > 0 && startswith(v, "systemd-boot ")) {
+ r = unlinkat(dirfd(d), de->d_name, 0);
+ if (r < 0)
+ return log_error_errno(errno, "Failed to remove \"%s/%s\": %m", p, de->d_name);
+
+ log_info("Removed \"%s/%s\".", p, de->d_name);
+ }
+
+ c++;
+ }
+
+ return c;
+}
+
+static int rmdir_one(const char *prefix, const char *suffix) {
+ const char *p;
+
+ p = prefix_roota(prefix, suffix);
+ if (rmdir(p) < 0) {
+ bool ignore = IN_SET(errno, ENOENT, ENOTEMPTY);
+
+ log_full_errno(ignore ? LOG_DEBUG : LOG_ERR, errno,
+ "Failed to remove directory \"%s\": %m", p);
+ if (!ignore)
+ return -errno;
+ } else
+ log_info("Removed \"%s\".", p);
+
+ return 0;
+}
+
+static int remove_subdirs(const char *root, const char *const *subdirs) {
+ int r, q;
+
+ /* We use recursion here to destroy the directories in reverse order. Which should be safe given how
+ * short the array is. */
+
+ if (!subdirs[0]) /* A the end of the list */
+ return 0;
+
+ r = remove_subdirs(root, subdirs + 1);
+ q = rmdir_one(root, subdirs[0]);
+
+ return r < 0 ? r : q;
+}
+
+static int remove_entry_directory(const char *root) {
+ assert(root);
+ assert(arg_make_entry_directory >= 0);
+
+ if (!arg_make_entry_directory || !arg_entry_token)
+ return 0;
+
+ return rmdir_one(root, arg_entry_token);
+}
+
+static int remove_binaries(const char *esp_path) {
+ const char *p;
+ int r, q;
+
+ p = prefix_roota(esp_path, "/EFI/systemd");
+ r = rm_rf(p, REMOVE_ROOT|REMOVE_PHYSICAL);
+
+ q = remove_boot_efi(esp_path);
+ if (q < 0 && r == 0)
+ r = q;
+
+ return r;
+}
+
+static int remove_file(const char *root, const char *file) {
+ const char *p;
+
+ assert(root);
+ assert(file);
+
+ p = prefix_roota(root, file);
+ if (unlink(p) < 0) {
+ log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_ERR, errno,
+ "Failed to unlink file \"%s\": %m", p);
+
+ return errno == ENOENT ? 0 : -errno;
+ }
+
+ log_info("Removed \"%s\".", p);
+ return 1;
+}
+
+static int remove_variables(sd_id128_t uuid, const char *path, bool in_order) {
+ uint16_t slot;
+ int r;
+
+ if (arg_root || !is_efi_boot())
+ return 0;
+
+ r = find_slot(uuid, path, &slot);
+ if (r != 1)
+ return 0;
+
+ r = efi_remove_boot_option(slot);
+ if (r < 0)
+ return r;
+
+ if (in_order)
+ return remove_from_order(slot);
+
+ return 0;
+}
+
+static int remove_loader_variables(void) {
+ int r = 0;
+
+ /* Remove all persistent loader variables we define */
+
+ FOREACH_STRING(var,
+ EFI_LOADER_VARIABLE(LoaderConfigTimeout),
+ EFI_LOADER_VARIABLE(LoaderConfigTimeoutOneShot),
+ EFI_LOADER_VARIABLE(LoaderEntryDefault),
+ EFI_LOADER_VARIABLE(LoaderEntryOneShot),
+ EFI_LOADER_VARIABLE(LoaderSystemToken)){
+
+ int q;
+
+ q = efi_set_variable(var, NULL, 0);
+ if (q == -ENOENT)
+ continue;
+ if (q < 0) {
+ log_warning_errno(q, "Failed to remove EFI variable %s: %m", var);
+ if (r >= 0)
+ r = q;
+ } else
+ log_info("Removed EFI variable %s.", var);
+ }
+
+ return r;
+}
+
+static int install_loader_config(const char *esp_path) {
+ _cleanup_(unlink_and_freep) char *t = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ const char *p;
+ int r;
+
+ assert(arg_make_entry_directory >= 0);
+
+ p = prefix_roota(esp_path, "/loader/loader.conf");
+ if (access(p, F_OK) >= 0) /* Silently skip creation if the file already exists (early check) */
+ return 0;
+
+ r = fopen_tmpfile_linkable(p, O_WRONLY|O_CLOEXEC, &t, &f);
+ if (r < 0)
+ return log_error_errno(r, "Failed to open \"%s\" for writing: %m", p);
+
+ fprintf(f, "#timeout 3\n"
+ "#console-mode keep\n");
+
+ if (arg_make_entry_directory) {
+ assert(arg_entry_token);
+ fprintf(f, "default %s-*\n", arg_entry_token);
+ }
+
+ r = flink_tmpfile(f, t, p);
+ if (r == -EEXIST)
+ return 0; /* Silently skip creation if the file exists now (recheck) */
+ if (r < 0)
+ return log_error_errno(r, "Failed to move \"%s\" into place: %m", p);
+
+ t = mfree(t);
+ return 1;
+}
+
+static int install_loader_specification(const char *root) {
+ _cleanup_(unlink_and_freep) char *t = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_free_ char *p = NULL;
+ int r;
+
+ p = path_join(root, "/loader/entries.srel");
+ if (!p)
+ return log_oom();
+
+ if (access(p, F_OK) >= 0) /* Silently skip creation if the file already exists (early check) */
+ return 0;
+
+ r = fopen_tmpfile_linkable(p, O_WRONLY|O_CLOEXEC, &t, &f);
+ if (r < 0)
+ return log_error_errno(r, "Failed to open \"%s\" for writing: %m", p);
+
+ fprintf(f, "type1\n");
+
+ r = flink_tmpfile(f, t, p);
+ if (r == -EEXIST)
+ return 0; /* Silently skip creation if the file exists now (recheck) */
+ if (r < 0)
+ return log_error_errno(r, "Failed to move \"%s\" into place: %m", p);
+
+ t = mfree(t);
+ return 1;
+}
+
+static int install_entry_directory(const char *root) {
+ assert(root);
+ assert(arg_make_entry_directory >= 0);
+
+ if (!arg_make_entry_directory)
+ return 0;
+
+ assert(arg_entry_token);
+ return mkdir_one(root, arg_entry_token);
+}
+
+static int install_entry_token(void) {
+ int r;
+
+ assert(arg_make_entry_directory >= 0);
+ assert(arg_entry_token);
+
+ /* Let's save the used entry token in /etc/kernel/entry-token if we used it to create the entry
+ * directory, or if anything else but the machine ID */
+
+ if (!arg_make_entry_directory && arg_entry_token_type == ARG_ENTRY_TOKEN_MACHINE_ID)
+ return 0;
+
+ r = write_string_file("/etc/kernel/entry-token", arg_entry_token, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_MKDIR_0755);
+ if (r < 0)
+ return log_error_errno(r, "Failed to write entry token '%s' to /etc/kernel/entry-token: %m", arg_entry_token);
+
+ return 0;
+}
+
+static int help(int argc, char *argv[], void *userdata) {
+ _cleanup_free_ char *link = NULL;
+ int r;
+
+ r = terminal_urlify_man("bootctl", "1", &link);
+ if (r < 0)
+ return log_oom();
+
+ printf("%1$s [OPTIONS...] COMMAND ...\n"
+ "\n%5$sControl EFI firmware boot settings and manage boot loader.%6$s\n"
+ "\n%3$sGeneric EFI Firmware/Boot Loader Commands:%4$s\n"
+ " status Show status of installed boot loader and EFI variables\n"
+ " reboot-to-firmware [BOOL]\n"
+ " Query or set reboot-to-firmware EFI flag\n"
+ " systemd-efi-options [STRING]\n"
+ " Query or set system options string in EFI variable\n"
+ "\n%3$sBoot Loader Specification Commands:%4$s\n"
+ " list List boot loader entries\n"
+ " set-default ID Set default boot loader entry\n"
+ " set-oneshot ID Set default boot loader entry, for next boot only\n"
+ " set-timeout SECONDS Set the menu timeout\n"
+ " set-timeout-oneshot SECONDS\n"
+ " Set the menu timeout for the next boot only\n"
+ "\n%3$ssystemd-boot Commands:%4$s\n"
+ " install Install systemd-boot to the ESP and EFI variables\n"
+ " update Update systemd-boot in the ESP and EFI variables\n"
+ " remove Remove systemd-boot from the ESP and EFI variables\n"
+ " is-installed Test whether systemd-boot is installed in the ESP\n"
+ " random-seed Initialize random seed in ESP and EFI variables\n"
+ "\n%3$sOptions:%4$s\n"
+ " -h --help Show this help\n"
+ " --version Print version\n"
+ " --esp-path=PATH Path to the EFI System Partition (ESP)\n"
+ " --boot-path=PATH Path to the $BOOT partition\n"
+ " --root=PATH Operate on an alternate filesystem root\n"
+ " --image=PATH Operate on disk image as filesystem root\n"
+ " --install-source=auto|image|host\n"
+ " Where to pick files when using --root=/--image=\n"
+ " -p --print-esp-path Print path to the EFI System Partition\n"
+ " -x --print-boot-path Print path to the $BOOT partition\n"
+ " --no-variables Don't touch EFI variables\n"
+ " --no-pager Do not pipe output into a pager\n"
+ " --graceful Don't fail when the ESP cannot be found or EFI\n"
+ " variables cannot be written\n"
+ " -q --quiet Suppress output\n"
+ " --make-entry-directory=yes|no|auto\n"
+ " Create $BOOT/ENTRY-TOKEN/ directory\n"
+ " --entry-token=machine-id|os-id|os-image-id|auto|literal:…\n"
+ " Entry token to use for this installation\n"
+ " --json=pretty|short|off\n"
+ " Generate JSON output\n"
+ " --all-architectures\n"
+ " Install all supported EFI architectures\n"
+ " --efi-boot-option-description=DESCRIPTION\n"
+ " Description of the entry in the boot option list\n"
+ "\nSee the %2$s for details.\n",
+ program_invocation_short_name,
+ link,
+ ansi_underline(),
+ ansi_normal(),
+ ansi_highlight(),
+ ansi_normal());
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+ enum {
+ ARG_ESP_PATH = 0x100,
+ ARG_BOOT_PATH,
+ ARG_ROOT,
+ ARG_IMAGE,
+ ARG_INSTALL_SOURCE,
+ ARG_VERSION,
+ ARG_NO_VARIABLES,
+ ARG_NO_PAGER,
+ ARG_GRACEFUL,
+ ARG_MAKE_ENTRY_DIRECTORY,
+ ARG_ENTRY_TOKEN,
+ ARG_JSON,
+ ARG_ARCH_ALL,
+ ARG_EFI_BOOT_OPTION_DESCRIPTION,
+ };
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, ARG_VERSION },
+ { "esp-path", required_argument, NULL, ARG_ESP_PATH },
+ { "path", required_argument, NULL, ARG_ESP_PATH }, /* Compatibility alias */
+ { "boot-path", required_argument, NULL, ARG_BOOT_PATH },
+ { "root", required_argument, NULL, ARG_ROOT },
+ { "image", required_argument, NULL, ARG_IMAGE },
+ { "install-source", required_argument, NULL, ARG_INSTALL_SOURCE },
+ { "print-esp-path", no_argument, NULL, 'p' },
+ { "print-path", no_argument, NULL, 'p' }, /* Compatibility alias */
+ { "print-boot-path", no_argument, NULL, 'x' },
+ { "no-variables", no_argument, NULL, ARG_NO_VARIABLES },
+ { "no-pager", no_argument, NULL, ARG_NO_PAGER },
+ { "graceful", no_argument, NULL, ARG_GRACEFUL },
+ { "quiet", no_argument, NULL, 'q' },
+ { "make-entry-directory", required_argument, NULL, ARG_MAKE_ENTRY_DIRECTORY },
+ { "make-machine-id-directory", required_argument, NULL, ARG_MAKE_ENTRY_DIRECTORY }, /* Compatibility alias */
+ { "entry-token", required_argument, NULL, ARG_ENTRY_TOKEN },
+ { "json", required_argument, NULL, ARG_JSON },
+ { "all-architectures", no_argument, NULL, ARG_ARCH_ALL },
+ { "efi-boot-option-description", required_argument, NULL, ARG_EFI_BOOT_OPTION_DESCRIPTION },
+ {}
+ };
+
+ int c, r;
+ bool b;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ while ((c = getopt_long(argc, argv, "hpx", options, NULL)) >= 0)
+ switch (c) {
+
+ case 'h':
+ help(0, NULL, NULL);
+ return 0;
+
+ case ARG_VERSION:
+ return version();
+
+ case ARG_ESP_PATH:
+ r = free_and_strdup(&arg_esp_path, optarg);
+ if (r < 0)
+ return log_oom();
+ break;
+
+ case ARG_BOOT_PATH:
+ r = free_and_strdup(&arg_xbootldr_path, optarg);
+ if (r < 0)
+ return log_oom();
+ break;
+
+ case ARG_ROOT:
+ r = parse_path_argument(optarg, /* suppress_root= */ true, &arg_root);
+ if (r < 0)
+ return r;
+ break;
+
+ case ARG_IMAGE:
+ r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_image);
+ if (r < 0)
+ return r;
+ break;
+
+ case ARG_INSTALL_SOURCE:
+ if (streq(optarg, "auto"))
+ arg_install_source = ARG_INSTALL_SOURCE_AUTO;
+ else if (streq(optarg, "image"))
+ arg_install_source = ARG_INSTALL_SOURCE_IMAGE;
+ else if (streq(optarg, "host"))
+ arg_install_source = ARG_INSTALL_SOURCE_HOST;
+ else
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Unexpected parameter for --install-source=: %s", optarg);
+
+ break;
+
+ case 'p':
+ if (arg_print_dollar_boot_path)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "--print-boot-path/-x cannot be combined with --print-esp-path/-p");
+ arg_print_esp_path = true;
+ break;
+
+ case 'x':
+ if (arg_print_esp_path)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "--print-boot-path/-x cannot be combined with --print-esp-path/-p");
+ arg_print_dollar_boot_path = true;
+ break;
+
+ case ARG_NO_VARIABLES:
+ arg_touch_variables = false;
+ break;
+
+ case ARG_NO_PAGER:
+ arg_pager_flags |= PAGER_DISABLE;
+ break;
+
+ case ARG_GRACEFUL:
+ arg_graceful = true;
+ break;
+
+ case 'q':
+ arg_quiet = true;
+ break;
+
+ case ARG_ENTRY_TOKEN: {
+ const char *e;
+
+ if (streq(optarg, "machine-id")) {
+ arg_entry_token_type = ARG_ENTRY_TOKEN_MACHINE_ID;
+ arg_entry_token = mfree(arg_entry_token);
+ } else if (streq(optarg, "os-image-id")) {
+ arg_entry_token_type = ARG_ENTRY_TOKEN_OS_IMAGE_ID;
+ arg_entry_token = mfree(arg_entry_token);
+ } else if (streq(optarg, "os-id")) {
+ arg_entry_token_type = ARG_ENTRY_TOKEN_OS_ID;
+ arg_entry_token = mfree(arg_entry_token);
+ } else if ((e = startswith(optarg, "literal:"))) {
+ arg_entry_token_type = ARG_ENTRY_TOKEN_LITERAL;
+
+ r = free_and_strdup_warn(&arg_entry_token, e);
+ if (r < 0)
+ return r;
+ } else
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Unexpected parameter for --entry-token=: %s", optarg);
+
+ break;
+ }
+
+ case ARG_MAKE_ENTRY_DIRECTORY:
+ if (streq(optarg, "auto")) /* retained for backwards compatibility */
+ arg_make_entry_directory = -1; /* yes if machine-id is permanent */
+ else {
+ r = parse_boolean_argument("--make-entry-directory=", optarg, &b);
+ if (r < 0)
+ return r;
+
+ arg_make_entry_directory = b;
+ }
+ break;
+
+ case ARG_JSON:
+ r = parse_json_argument(optarg, &arg_json_format_flags);
+ if (r <= 0)
+ return r;
+ break;
+
+ case ARG_ARCH_ALL:
+ arg_arch_all = true;
+ break;
+
+ case ARG_EFI_BOOT_OPTION_DESCRIPTION:
+ if (isempty(optarg) || !(string_is_safe(optarg) && utf8_is_valid(optarg))) {
+ _cleanup_free_ char *escaped = NULL;
+
+ escaped = cescape(optarg);
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Invalid --efi-boot-option-description=: %s", strna(escaped));
+ }
+ if (strlen(optarg) > EFI_BOOT_OPTION_DESCRIPTION_MAX)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "--efi-boot-option-description= too long: %zu > %zu", strlen(optarg), EFI_BOOT_OPTION_DESCRIPTION_MAX);
+ r = free_and_strdup_warn(&arg_efi_boot_option_description, optarg);
+ if (r < 0)
+ return r;
+ break;
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached();
+ }
+
+ if ((arg_root || arg_image) && argv[optind] && !STR_IN_SET(argv[optind], "status", "list",
+ "install", "update", "remove", "is-installed", "random-seed"))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Options --root= and --image= are not supported with verb %s.",
+ argv[optind]);
+
+ if (arg_root && arg_image)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Please specify either --root= or --image=, the combination of both is not supported.");
+
+ if (arg_install_source != ARG_INSTALL_SOURCE_AUTO && !arg_root && !arg_image)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--install-from-host is only supported with --root= or --image=.");
+
+ return 1;
+}
+
+static void read_efi_var(const char *variable, char **ret) {
+ int r;
+
+ r = efi_get_variable_string(variable, ret);
+ if (r < 0 && r != -ENOENT)
+ log_warning_errno(r, "Failed to read EFI variable %s: %m", variable);
+}
+
+static void print_yes_no_line(bool first, bool good, const char *name) {
+ printf("%s%s %s\n",
+ first ? " Features: " : " ",
+ COLOR_MARK_BOOL(good),
+ name);
+}
+
+static int are_we_installed(const char *esp_path) {
+ int r;
+
+ /* Tests whether systemd-boot is installed. It's not obvious what to use as check here: we could
+ * check EFI variables, we could check what binary /EFI/BOOT/BOOT*.EFI points to, or whether the
+ * loader entries directory exists. Here we opted to check whether /EFI/systemd/ is non-empty, which
+ * should be a suitable and very minimal check for a number of reasons:
+ *
+ * → The check is architecture independent (i.e. we check if any systemd-boot loader is installed,
+ * not a specific one.)
+ *
+ * → It doesn't assume we are the only boot loader (i.e doesn't check if we own the main
+ * /EFI/BOOT/BOOT*.EFI fallback binary.
+ *
+ * → It specifically checks for systemd-boot, not for other boot loaders (which a check for
+ * /boot/loader/entries would do). */
+
+ _cleanup_free_ char *p = path_join(esp_path, "/EFI/systemd/");
+ if (!p)
+ return log_oom();
+
+ log_debug("Checking whether %s contains any files%s", p, special_glyph(SPECIAL_GLYPH_ELLIPSIS));
+ r = dir_is_empty(p, /* ignore_hidden_or_backup= */ false);
+ if (r < 0 && r != -ENOENT)
+ return log_error_errno(r, "Failed to check whether %s contains any files: %m", p);
+
+ return r == 0;
+}
+
+static int verb_status(int argc, char *argv[], void *userdata) {
+ sd_id128_t esp_uuid = SD_ID128_NULL, xbootldr_uuid = SD_ID128_NULL;
+ dev_t esp_devid = 0, xbootldr_devid = 0;
+ int r, k;
+
+ r = acquire_esp(/* unprivileged_mode= */ geteuid() != 0, /* graceful= */ false, NULL, NULL, NULL, &esp_uuid, &esp_devid);
+ if (arg_print_esp_path) {
+ if (r == -EACCES) /* If we couldn't acquire the ESP path, log about access errors (which is the only
+ * error the find_esp_and_warn() won't log on its own) */
+ return log_error_errno(r, "Failed to determine ESP location: %m");
+ if (r < 0)
+ return r;
+
+ puts(arg_esp_path);
+ }
+
+ r = acquire_xbootldr(/* unprivileged_mode= */ geteuid() != 0, &xbootldr_uuid, &xbootldr_devid);
+ if (arg_print_dollar_boot_path) {
+ if (r == -EACCES)
+ return log_error_errno(r, "Failed to determine XBOOTLDR partition: %m");
+ if (r < 0)
+ return r;
+
+ const char *path = arg_dollar_boot_path();
+ if (!path)
+ return log_error_errno(SYNTHETIC_ERRNO(EACCES), "Failed to determine XBOOTLDR location: %m");
+
+ puts(path);
+ }
+
+ if (arg_print_esp_path || arg_print_dollar_boot_path)
+ return 0;
+
+ r = 0; /* If we couldn't determine the path, then don't consider that a problem from here on, just
+ * show what we can show */
+
+ pager_open(arg_pager_flags);
+
+ if (!arg_root && is_efi_boot()) {
+ static const struct {
+ uint64_t flag;
+ const char *name;
+ } loader_flags[] = {
+ { EFI_LOADER_FEATURE_BOOT_COUNTING, "Boot counting" },
+ { EFI_LOADER_FEATURE_CONFIG_TIMEOUT, "Menu timeout control" },
+ { EFI_LOADER_FEATURE_CONFIG_TIMEOUT_ONE_SHOT, "One-shot menu timeout control" },
+ { EFI_LOADER_FEATURE_ENTRY_DEFAULT, "Default entry control" },
+ { EFI_LOADER_FEATURE_ENTRY_ONESHOT, "One-shot entry control" },
+ { EFI_LOADER_FEATURE_XBOOTLDR, "Support for XBOOTLDR partition" },
+ { EFI_LOADER_FEATURE_RANDOM_SEED, "Support for passing random seed to OS" },
+ { EFI_LOADER_FEATURE_LOAD_DRIVER, "Load drop-in drivers" },
+ { EFI_LOADER_FEATURE_SORT_KEY, "Support Type #1 sort-key field" },
+ { EFI_LOADER_FEATURE_SAVED_ENTRY, "Support @saved pseudo-entry" },
+ { EFI_LOADER_FEATURE_DEVICETREE, "Support Type #1 devicetree field" },
+ };
+ static const struct {
+ uint64_t flag;
+ const char *name;
+ } stub_flags[] = {
+ { EFI_STUB_FEATURE_REPORT_BOOT_PARTITION, "Stub sets ESP information" },
+ { EFI_STUB_FEATURE_PICK_UP_CREDENTIALS, "Picks up credentials from boot partition" },
+ { EFI_STUB_FEATURE_PICK_UP_SYSEXTS, "Picks up system extension images from boot partition" },
+ { EFI_STUB_FEATURE_THREE_PCRS, "Measures kernel+command line+sysexts" },
+ };
+ _cleanup_free_ char *fw_type = NULL, *fw_info = NULL, *loader = NULL, *loader_path = NULL, *stub = NULL;
+ sd_id128_t loader_part_uuid = SD_ID128_NULL;
+ uint64_t loader_features = 0, stub_features = 0;
+ Tpm2Support s;
+ int have;
+
+ read_efi_var(EFI_LOADER_VARIABLE(LoaderFirmwareType), &fw_type);
+ read_efi_var(EFI_LOADER_VARIABLE(LoaderFirmwareInfo), &fw_info);
+ read_efi_var(EFI_LOADER_VARIABLE(LoaderInfo), &loader);
+ read_efi_var(EFI_LOADER_VARIABLE(StubInfo), &stub);
+ read_efi_var(EFI_LOADER_VARIABLE(LoaderImageIdentifier), &loader_path);
+ (void) efi_loader_get_features(&loader_features);
+ (void) efi_stub_get_features(&stub_features);
+
+ if (loader_path)
+ efi_tilt_backslashes(loader_path);
+
+ k = efi_loader_get_device_part_uuid(&loader_part_uuid);
+ if (k < 0 && k != -ENOENT)
+ r = log_warning_errno(k, "Failed to read EFI variable LoaderDevicePartUUID: %m");
+
+ SecureBootMode secure = efi_get_secure_boot_mode();
+ printf("%sSystem:%s\n", ansi_underline(), ansi_normal());
+ printf(" Firmware: %s%s (%s)%s\n", ansi_highlight(), strna(fw_type), strna(fw_info), ansi_normal());
+ printf(" Firmware Arch: %s\n", get_efi_arch());
+ printf(" Secure Boot: %sd (%s)\n",
+ enable_disable(IN_SET(secure, SECURE_BOOT_USER, SECURE_BOOT_DEPLOYED)),
+ secure_boot_mode_to_string(secure));
+
+ s = tpm2_support();
+ printf(" TPM2 Support: %s%s%s\n",
+ FLAGS_SET(s, TPM2_SUPPORT_FIRMWARE|TPM2_SUPPORT_DRIVER) ? ansi_highlight_green() :
+ (s & (TPM2_SUPPORT_FIRMWARE|TPM2_SUPPORT_DRIVER)) != 0 ? ansi_highlight_red() : ansi_highlight_yellow(),
+ FLAGS_SET(s, TPM2_SUPPORT_FIRMWARE|TPM2_SUPPORT_DRIVER) ? "yes" :
+ (s & TPM2_SUPPORT_FIRMWARE) ? "firmware only, driver unavailable" :
+ (s & TPM2_SUPPORT_DRIVER) ? "driver only, firmware unavailable" : "no",
+ ansi_normal());
+
+ k = efi_get_reboot_to_firmware();
+ if (k > 0)
+ printf(" Boot into FW: %sactive%s\n", ansi_highlight_yellow(), ansi_normal());
+ else if (k == 0)
+ printf(" Boot into FW: supported\n");
+ else if (k == -EOPNOTSUPP)
+ printf(" Boot into FW: not supported\n");
+ else {
+ errno = -k;
+ printf(" Boot into FW: %sfailed%s (%m)\n", ansi_highlight_red(), ansi_normal());
+ }
+ printf("\n");
+
+ printf("%sCurrent Boot Loader:%s\n", ansi_underline(), ansi_normal());
+ printf(" Product: %s%s%s\n", ansi_highlight(), strna(loader), ansi_normal());
+
+ for (size_t i = 0; i < ELEMENTSOF(loader_flags); i++)
+ print_yes_no_line(i == 0, FLAGS_SET(loader_features, loader_flags[i].flag), loader_flags[i].name);
+
+ sd_id128_t bootloader_esp_uuid;
+ bool have_bootloader_esp_uuid = efi_loader_get_device_part_uuid(&bootloader_esp_uuid) >= 0;
+
+ print_yes_no_line(false, have_bootloader_esp_uuid, "Boot loader sets ESP information");
+ if (have_bootloader_esp_uuid && !sd_id128_is_null(esp_uuid) &&
+ !sd_id128_equal(esp_uuid, bootloader_esp_uuid))
+ printf("WARNING: The boot loader reports a different ESP UUID than detected ("SD_ID128_UUID_FORMAT_STR" vs. "SD_ID128_UUID_FORMAT_STR")!\n",
+ SD_ID128_FORMAT_VAL(bootloader_esp_uuid),
+ SD_ID128_FORMAT_VAL(esp_uuid));
+
+ if (stub) {
+ printf(" Stub: %s\n", stub);
+ for (size_t i = 0; i < ELEMENTSOF(stub_flags); i++)
+ print_yes_no_line(i == 0, FLAGS_SET(stub_features, stub_flags[i].flag), stub_flags[i].name);
+ }
+ if (!sd_id128_is_null(loader_part_uuid))
+ printf(" ESP: /dev/disk/by-partuuid/" SD_ID128_UUID_FORMAT_STR "\n",
+ SD_ID128_FORMAT_VAL(loader_part_uuid));
+ else
+ printf(" ESP: n/a\n");
+ printf(" File: %s%s\n", special_glyph(SPECIAL_GLYPH_TREE_RIGHT), strna(loader_path));
+ printf("\n");
+
+ printf("%sRandom Seed:%s\n", ansi_underline(), ansi_normal());
+ have = access(EFIVAR_PATH(EFI_LOADER_VARIABLE(LoaderRandomSeed)), F_OK) >= 0;
+ printf(" Passed to OS: %s\n", yes_no(have));
+ have = access(EFIVAR_PATH(EFI_LOADER_VARIABLE(LoaderSystemToken)), F_OK) >= 0;
+ printf(" System Token: %s\n", have ? "set" : "not set");
+
+ if (arg_esp_path) {
+ _cleanup_free_ char *p = NULL;
+
+ p = path_join(arg_esp_path, "/loader/random-seed");
+ if (!p)
+ return log_oom();
+
+ have = access(p, F_OK) >= 0;
+ printf(" Exists: %s\n", yes_no(have));
+ }
+
+ printf("\n");
+ } else
+ printf("%sSystem:%s\n"
+ "Not booted with EFI\n\n",
+ ansi_underline(), ansi_normal());
+
+ if (arg_esp_path) {
+ k = status_binaries(arg_esp_path, esp_uuid);
+ if (k < 0)
+ r = k;
+ }
+
+ if (!arg_root && is_efi_boot()) {
+ k = status_variables();
+ if (k < 0)
+ r = k;
+ }
+
+ if (arg_esp_path || arg_xbootldr_path) {
+ _cleanup_(boot_config_free) BootConfig config = BOOT_CONFIG_NULL;
+
+ k = boot_config_load_and_select(&config,
+ arg_esp_path, esp_devid,
+ arg_xbootldr_path, xbootldr_devid);
+ if (k < 0)
+ r = k;
+ else {
+ k = status_entries(&config,
+ arg_esp_path, esp_uuid,
+ arg_xbootldr_path, xbootldr_uuid);
+ if (k < 0)
+ r = k;
+ }
+ }
+
+ return r;
+}
+
+static int verb_list(int argc, char *argv[], void *userdata) {
+ _cleanup_(boot_config_free) BootConfig config = BOOT_CONFIG_NULL;
+ dev_t esp_devid = 0, xbootldr_devid = 0;
+ int r;
+
+ /* If we lack privileges we invoke find_esp_and_warn() in "unprivileged mode" here, which does two
+ * things: turn off logging about access errors and turn off potentially privileged device probing.
+ * Here we're interested in the latter but not the former, hence request the mode, and log about
+ * EACCES. */
+
+ r = acquire_esp(/* unprivileged_mode= */ geteuid() != 0, /* graceful= */ false, NULL, NULL, NULL, NULL, &esp_devid);
+ if (r == -EACCES) /* We really need the ESP path for this call, hence also log about access errors */
+ return log_error_errno(r, "Failed to determine ESP location: %m");
+ if (r < 0)
+ return r;
+
+ r = acquire_xbootldr(/* unprivileged_mode= */ geteuid() != 0, NULL, &xbootldr_devid);
+ if (r == -EACCES)
+ return log_error_errno(r, "Failed to determine XBOOTLDR partition: %m");
+ if (r < 0)
+ return r;
+
+ r = boot_config_load_and_select(&config, arg_esp_path, esp_devid, arg_xbootldr_path, xbootldr_devid);
+ if (r < 0)
+ return r;
+
+ if (config.n_entries == 0 && FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) {
+ log_info("No boot loader entries found.");
+ return 0;
+ }
+
+ pager_open(arg_pager_flags);
+ return show_boot_entries(&config, arg_json_format_flags);
+}
+
+static int install_random_seed(const char *esp) {
+ _cleanup_(unlink_and_freep) char *tmp = NULL;
+ _cleanup_free_ void *buffer = NULL;
+ _cleanup_free_ char *path = NULL;
+ _cleanup_close_ int fd = -1;
+ size_t sz, token_size;
+ ssize_t n;
+ int r;
+
+ assert(esp);
+
+ path = path_join(esp, "/loader/random-seed");
+ if (!path)
+ return log_oom();
+
+ sz = random_pool_size();
+
+ buffer = malloc(sz);
+ if (!buffer)
+ return log_oom();
+
+ r = crypto_random_bytes(buffer, sz);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire random seed: %m");
+
+ /* Normally create_subdirs() should already have created everything we need, but in case "bootctl
+ * random-seed" is called we want to just create the minimum we need for it, and not the full
+ * list. */
+ r = mkdir_parents(path, 0755);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create parent directory for %s: %m", path);
+
+ r = tempfn_random(path, "bootctl", &tmp);
+ if (r < 0)
+ return log_oom();
+
+ fd = open(tmp, O_CREAT|O_EXCL|O_NOFOLLOW|O_NOCTTY|O_WRONLY|O_CLOEXEC, 0600);
+ if (fd < 0) {
+ tmp = mfree(tmp);
+ return log_error_errno(fd, "Failed to open random seed file for writing: %m");
+ }
+
+ n = write(fd, buffer, sz);
+ if (n < 0)
+ return log_error_errno(errno, "Failed to write random seed file: %m");
+ if ((size_t) n != sz)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short write while writing random seed file.");
+
+ if (rename(tmp, path) < 0)
+ return log_error_errno(r, "Failed to move random seed file into place: %m");
+
+ tmp = mfree(tmp);
+
+ log_info("Random seed file %s successfully written (%zu bytes).", path, sz);
+
+ if (!arg_touch_variables)
+ return 0;
+
+ if (!is_efi_boot()) {
+ log_notice("Not booted with EFI, skipping EFI variable setup.");
+ return 0;
+ }
+
+ if (arg_root) {
+ log_warning("Acting on %s, skipping EFI variable setup.",
+ arg_image ? "image" : "root directory");
+ return 0;
+ }
+
+ r = getenv_bool("SYSTEMD_WRITE_SYSTEM_TOKEN");
+ if (r < 0) {
+ if (r != -ENXIO)
+ log_warning_errno(r, "Failed to parse $SYSTEMD_WRITE_SYSTEM_TOKEN, ignoring.");
+
+ if (detect_vm() > 0) {
+ /* Let's not write a system token if we detect we are running in a VM
+ * environment. Why? Our default security model for the random seed uses the system
+ * token as a mechanism to ensure we are not vulnerable to golden master sloppiness
+ * issues, i.e. that people initialize the random seed file, then copy the image to
+ * many systems and end up with the same random seed in each that is assumed to be
+ * valid but in reality is the same for all machines. By storing a system token in
+ * the EFI variable space we can make sure that even though the random seeds on disk
+ * are all the same they will be different on each system under the assumption that
+ * the EFI variable space is maintained separate from the random seed storage. That
+ * is generally the case on physical systems, as the ESP is stored on persistent
+ * storage, and the EFI variables in NVRAM. However in virtualized environments this
+ * is generally not true: the EFI variable set is typically stored along with the
+ * disk image itself. For example, using the OVMF EFI firmware the EFI variables are
+ * stored in a file in the ESP itself. */
+
+ log_notice("Not installing system token, since we are running in a virtualized environment.");
+ return 0;
+ }
+ } else if (r == 0) {
+ log_notice("Not writing system token, because $SYSTEMD_WRITE_SYSTEM_TOKEN is set to false.");
+ return 0;
+ }
+
+ r = efi_get_variable(EFI_LOADER_VARIABLE(LoaderSystemToken), NULL, NULL, &token_size);
+ if (r == -ENODATA)
+ log_debug_errno(r, "LoaderSystemToken EFI variable is invalid (too short?), replacing.");
+ else if (r < 0) {
+ if (r != -ENOENT)
+ return log_error_errno(r, "Failed to test system token validity: %m");
+ } else {
+ if (token_size >= sz) {
+ /* Let's avoid writes if we can, and initialize this only once. */
+ log_debug("System token already written, not updating.");
+ return 0;
+ }
+
+ log_debug("Existing system token size (%zu) does not match our expectations (%zu), replacing.", token_size, sz);
+ }
+
+ r = crypto_random_bytes(buffer, sz);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire random seed: %m");
+
+ /* Let's write this variable with an umask in effect, so that unprivileged users can't see the token
+ * and possibly get identification information or too much insight into the kernel's entropy pool
+ * state. */
+ RUN_WITH_UMASK(0077) {
+ r = efi_set_variable(EFI_LOADER_VARIABLE(LoaderSystemToken), buffer, sz);
+ if (r < 0) {
+ if (!arg_graceful)
+ return log_error_errno(r, "Failed to write 'LoaderSystemToken' EFI variable: %m");
+
+ if (r == -EINVAL)
+ log_warning_errno(r, "Unable to write 'LoaderSystemToken' EFI variable (firmware problem?), ignoring: %m");
+ else
+ log_warning_errno(r, "Unable to write 'LoaderSystemToken' EFI variable, ignoring: %m");
+ } else
+ log_info("Successfully initialized system token in EFI variable with %zu bytes.", sz);
+ }
+
+ return 0;
+}
+
+static int sync_everything(void) {
+ int ret = 0, k;
+
+ if (arg_esp_path) {
+ k = syncfs_path(AT_FDCWD, arg_esp_path);
+ if (k < 0)
+ ret = log_error_errno(k, "Failed to synchronize the ESP '%s': %m", arg_esp_path);
+ }
+
+ if (arg_xbootldr_path) {
+ k = syncfs_path(AT_FDCWD, arg_xbootldr_path);
+ if (k < 0)
+ ret = log_error_errno(k, "Failed to synchronize $BOOT '%s': %m", arg_xbootldr_path);
+ }
+
+ return ret;
+}
+
+static int verb_install(int argc, char *argv[], void *userdata) {
+ sd_id128_t uuid = SD_ID128_NULL;
+ uint64_t pstart = 0, psize = 0;
+ uint32_t part = 0;
+ bool install, graceful;
+ int r;
+
+ /* Invoked for both "update" and "install" */
+
+ install = streq(argv[0], "install");
+ graceful = !install && arg_graceful; /* support graceful mode for updates */
+
+ r = acquire_esp(/* unprivileged_mode= */ false, graceful, &part, &pstart, &psize, &uuid, NULL);
+ if (graceful && r == -ENOKEY)
+ return 0; /* If --graceful is specified and we can't find an ESP, handle this cleanly */
+ if (r < 0)
+ return r;
+
+ if (!install) {
+ /* If we are updating, don't do anything if sd-boot wasn't actually installed. */
+ r = are_we_installed(arg_esp_path);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ log_debug("Skipping update because sd-boot is not installed in the ESP.");
+ return 0;
+ }
+ }
+
+ r = acquire_xbootldr(/* unprivileged_mode= */ false, NULL, NULL);
+ if (r < 0)
+ return r;
+
+ r = settle_make_entry_directory();
+ if (r < 0)
+ return r;
+
+ const char *arch = arg_arch_all ? "" : get_efi_arch();
+
+ RUN_WITH_UMASK(0002) {
+ if (install) {
+ /* Don't create any of these directories when we are just updating. When we update
+ * we'll drop-in our files (unless there are newer ones already), but we won't create
+ * the directories for them in the first place. */
+ r = create_subdirs(arg_esp_path, esp_subdirs);
+ if (r < 0)
+ return r;
+
+ r = create_subdirs(arg_dollar_boot_path(), dollar_boot_subdirs);
+ if (r < 0)
+ return r;
+ }
+
+ r = install_binaries(arg_esp_path, arch, install);
+ if (r < 0)
+ return r;
+
+ if (install) {
+ r = install_loader_config(arg_esp_path);
+ if (r < 0)
+ return r;
+
+ r = install_entry_directory(arg_dollar_boot_path());
+ if (r < 0)
+ return r;
+
+ r = install_entry_token();
+ if (r < 0)
+ return r;
+
+ r = install_random_seed(arg_esp_path);
+ if (r < 0)
+ return r;
+ }
+
+ r = install_loader_specification(arg_dollar_boot_path());
+ if (r < 0)
+ return r;
+ }
+
+ (void) sync_everything();
+
+ if (!arg_touch_variables)
+ return 0;
+
+ if (arg_arch_all) {
+ log_info("Not changing EFI variables with --all-architectures.");
+ return 0;
+ }
+
+ char *path = strjoina("/EFI/systemd/systemd-boot", arch, ".efi");
+ return install_variables(arg_esp_path, part, pstart, psize, uuid, path, install, graceful);
+}
+
+static int verb_remove(int argc, char *argv[], void *userdata) {
+ sd_id128_t uuid = SD_ID128_NULL;
+ int r, q;
+
+ r = acquire_esp(/* unprivileged_mode= */ false, /* graceful= */ false, NULL, NULL, NULL, &uuid, NULL);
+ if (r < 0)
+ return r;
+
+ r = acquire_xbootldr(/* unprivileged_mode= */ false, NULL, NULL);
+ if (r < 0)
+ return r;
+
+ r = settle_make_entry_directory();
+ if (r < 0)
+ return r;
+
+ r = remove_binaries(arg_esp_path);
+
+ q = remove_file(arg_esp_path, "/loader/loader.conf");
+ if (q < 0 && r >= 0)
+ r = q;
+
+ q = remove_file(arg_esp_path, "/loader/random-seed");
+ if (q < 0 && r >= 0)
+ r = q;
+
+ q = remove_file(arg_esp_path, "/loader/entries.srel");
+ if (q < 0 && r >= 0)
+ r = q;
+
+ q = remove_subdirs(arg_esp_path, esp_subdirs);
+ if (q < 0 && r >= 0)
+ r = q;
+
+ q = remove_subdirs(arg_esp_path, dollar_boot_subdirs);
+ if (q < 0 && r >= 0)
+ r = q;
+
+ q = remove_entry_directory(arg_esp_path);
+ if (q < 0 && r >= 0)
+ r = q;
+
+ if (arg_xbootldr_path) {
+ /* Remove a subset of these also from the XBOOTLDR partition if it exists */
+
+ q = remove_file(arg_xbootldr_path, "/loader/entries.srel");
+ if (q < 0 && r >= 0)
+ r = q;
+
+ q = remove_subdirs(arg_xbootldr_path, dollar_boot_subdirs);
+ if (q < 0 && r >= 0)
+ r = q;
+
+ q = remove_entry_directory(arg_xbootldr_path);
+ if (q < 0 && r >= 0)
+ r = q;
+ }
+
+ (void) sync_everything();
+
+ if (!arg_touch_variables)
+ return r;
+
+ if (arg_arch_all) {
+ log_info("Not changing EFI variables with --all-architectures.");
+ return r;
+ }
+
+ char *path = strjoina("/EFI/systemd/systemd-boot", get_efi_arch(), ".efi");
+ q = remove_variables(uuid, path, true);
+ if (q < 0 && r >= 0)
+ r = q;
+
+ q = remove_loader_variables();
+ if (q < 0 && r >= 0)
+ r = q;
+
+ return r;
+}
+
+static int verb_is_installed(int argc, char *argv[], void *userdata) {
+ int r;
+
+ r = acquire_esp(/* privileged_mode= */ false,
+ /* graceful= */ arg_graceful,
+ NULL, NULL, NULL, NULL, NULL);
+ if (r < 0)
+ return r;
+
+ r = are_we_installed(arg_esp_path);
+ if (r < 0)
+ return r;
+
+ if (r > 0) {
+ if (!arg_quiet)
+ puts("yes");
+ return EXIT_SUCCESS;
+ } else {
+ if (!arg_quiet)
+ puts("no");
+ return EXIT_FAILURE;
+ }
+}
+
+static int parse_timeout(const char *arg1, char16_t **ret_timeout, size_t *ret_timeout_size) {
+ char utf8[DECIMAL_STR_MAX(usec_t)];
+ char16_t *encoded;
+ usec_t timeout;
+ int r;
+
+ assert(arg1);
+ assert(ret_timeout);
+ assert(ret_timeout_size);
+
+ if (streq(arg1, "menu-force"))
+ timeout = USEC_INFINITY;
+ else if (streq(arg1, "menu-hidden"))
+ timeout = 0;
+ else {
+ r = parse_time(arg1, &timeout, USEC_PER_SEC);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse timeout '%s': %m", arg1);
+ if (timeout != USEC_INFINITY && timeout > UINT32_MAX * USEC_PER_SEC)
+ log_warning("Timeout is too long and will be treated as 'menu-force' instead.");
+ }
+
+ xsprintf(utf8, USEC_FMT, MIN(timeout / USEC_PER_SEC, UINT32_MAX));
+
+ encoded = utf8_to_utf16(utf8, strlen(utf8));
+ if (!encoded)
+ return log_oom();
+
+ *ret_timeout = encoded;
+ *ret_timeout_size = char16_strlen(encoded) * 2 + 2;
+ return 0;
+}
+
+static int parse_loader_entry_target_arg(const char *arg1, char16_t **ret_target, size_t *ret_target_size) {
+ char16_t *encoded = NULL;
+ int r;
+
+ assert(arg1);
+ assert(ret_target);
+ assert(ret_target_size);
+
+ if (streq(arg1, "@current")) {
+ r = efi_get_variable(EFI_LOADER_VARIABLE(LoaderEntrySelected), NULL, (void *) ret_target, ret_target_size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get EFI variable 'LoaderEntrySelected': %m");
+
+ } else if (streq(arg1, "@oneshot")) {
+ r = efi_get_variable(EFI_LOADER_VARIABLE(LoaderEntryOneShot), NULL, (void *) ret_target, ret_target_size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get EFI variable 'LoaderEntryOneShot': %m");
+
+ } else if (streq(arg1, "@default")) {
+ r = efi_get_variable(EFI_LOADER_VARIABLE(LoaderEntryDefault), NULL, (void *) ret_target, ret_target_size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get EFI variable 'LoaderEntryDefault': %m");
+
+ } else if (arg1[0] == '@' && !streq(arg1, "@saved"))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unsupported special entry identifier: %s", arg1);
+ else {
+ encoded = utf8_to_utf16(arg1, strlen(arg1));
+ if (!encoded)
+ return log_oom();
+
+ *ret_target = encoded;
+ *ret_target_size = char16_strlen(encoded) * 2 + 2;
+ }
+
+ return 0;
+}
+
+static int verb_set_efivar(int argc, char *argv[], void *userdata) {
+ int r;
+
+ if (arg_root)
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "Acting on %s, skipping EFI variable setup.",
+ arg_image ? "image" : "root directory");
+
+ if (!is_efi_boot())
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "Not booted with UEFI.");
+
+ if (access(EFIVAR_PATH(EFI_LOADER_VARIABLE(LoaderInfo)), F_OK) < 0) {
+ if (errno == ENOENT) {
+ log_error_errno(errno, "Not booted with a supported boot loader.");
+ return -EOPNOTSUPP;
+ }
+
+ return log_error_errno(errno, "Failed to detect whether boot loader supports '%s' operation: %m", argv[0]);
+ }
+
+ if (detect_container() > 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "'%s' operation not supported in a container.",
+ argv[0]);
+
+ if (!arg_touch_variables)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "'%s' operation cannot be combined with --no-variables.",
+ argv[0]);
+
+ const char *variable;
+ int (* arg_parser)(const char *, char16_t **, size_t *);
+
+ if (streq(argv[0], "set-default")) {
+ variable = EFI_LOADER_VARIABLE(LoaderEntryDefault);
+ arg_parser = parse_loader_entry_target_arg;
+ } else if (streq(argv[0], "set-oneshot")) {
+ variable = EFI_LOADER_VARIABLE(LoaderEntryOneShot);
+ arg_parser = parse_loader_entry_target_arg;
+ } else if (streq(argv[0], "set-timeout")) {
+ variable = EFI_LOADER_VARIABLE(LoaderConfigTimeout);
+ arg_parser = parse_timeout;
+ } else if (streq(argv[0], "set-timeout-oneshot")) {
+ variable = EFI_LOADER_VARIABLE(LoaderConfigTimeoutOneShot);
+ arg_parser = parse_timeout;
+ } else
+ assert_not_reached();
+
+ if (isempty(argv[1])) {
+ r = efi_set_variable(variable, NULL, 0);
+ if (r < 0 && r != -ENOENT)
+ return log_error_errno(r, "Failed to remove EFI variable '%s': %m", variable);
+ } else {
+ _cleanup_free_ char16_t *value = NULL;
+ size_t value_size = 0;
+
+ r = arg_parser(argv[1], &value, &value_size);
+ if (r < 0)
+ return r;
+ r = efi_set_variable(variable, value, value_size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update EFI variable '%s': %m", variable);
+ }
+
+ return 0;
+}
+
+static int verb_random_seed(int argc, char *argv[], void *userdata) {
+ int r;
+
+ r = find_esp_and_warn(arg_root, arg_esp_path, false, &arg_esp_path, NULL, NULL, NULL, NULL, NULL);
+ if (r == -ENOKEY) {
+ /* find_esp_and_warn() doesn't warn about ENOKEY, so let's do that on our own */
+ if (!arg_graceful)
+ return log_error_errno(r, "Unable to find ESP.");
+
+ log_notice("No ESP found, not initializing random seed.");
+ return 0;
+ }
+ if (r < 0)
+ return r;
+
+ r = install_random_seed(arg_esp_path);
+ if (r < 0)
+ return r;
+
+ (void) sync_everything();
+ return 0;
+}
+
+static int verb_systemd_efi_options(int argc, char *argv[], void *userdata) {
+ int r;
+
+ if (argc == 1) {
+ _cleanup_free_ char *line = NULL, *new = NULL;
+
+ r = systemd_efi_options_variable(&line);
+ if (r == -ENODATA)
+ log_debug("No SystemdOptions EFI variable present in cache.");
+ else if (r < 0)
+ return log_error_errno(r, "Failed to read SystemdOptions EFI variable from cache: %m");
+ else
+ puts(line);
+
+ r = systemd_efi_options_efivarfs_if_newer(&new);
+ if (r == -ENODATA) {
+ if (line)
+ log_notice("Note: SystemdOptions EFI variable has been removed since boot.");
+ } else if (r < 0)
+ log_warning_errno(r, "Failed to check SystemdOptions EFI variable in efivarfs, ignoring: %m");
+ else if (new && !streq_ptr(line, new))
+ log_notice("Note: SystemdOptions EFI variable has been modified since boot. New value: %s",
+ new);
+ } else {
+ r = efi_set_variable_string(EFI_SYSTEMD_VARIABLE(SystemdOptions), argv[1]);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set SystemdOptions EFI variable: %m");
+ }
+
+ return 0;
+}
+
+static int verb_reboot_to_firmware(int argc, char *argv[], void *userdata) {
+ int r;
+
+ if (argc < 2) {
+ r = efi_get_reboot_to_firmware();
+ if (r > 0) {
+ puts("active");
+ return EXIT_SUCCESS; /* success */
+ }
+ if (r == 0) {
+ puts("supported");
+ return 1; /* recognizable error #1 */
+ }
+ if (r == -EOPNOTSUPP) {
+ puts("not supported");
+ return 2; /* recognizable error #2 */
+ }
+
+ log_error_errno(r, "Failed to query reboot-to-firmware state: %m");
+ return 3; /* other kind of error */
+ } else {
+ r = parse_boolean(argv[1]);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse argument: %s", argv[1]);
+
+ r = efi_set_reboot_to_firmware(r);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set reboot-to-firmware option: %m");
+
+ return 0;
+ }
+}
+
+static int bootctl_main(int argc, char *argv[]) {
+ static const Verb verbs[] = {
+ { "help", VERB_ANY, VERB_ANY, 0, help },
+ { "status", VERB_ANY, 1, VERB_DEFAULT, verb_status },
+ { "install", VERB_ANY, 1, 0, verb_install },
+ { "update", VERB_ANY, 1, 0, verb_install },
+ { "remove", VERB_ANY, 1, 0, verb_remove },
+ { "is-installed", VERB_ANY, 1, 0, verb_is_installed },
+ { "list", VERB_ANY, 1, 0, verb_list },
+ { "set-default", 2, 2, 0, verb_set_efivar },
+ { "set-oneshot", 2, 2, 0, verb_set_efivar },
+ { "set-timeout", 2, 2, 0, verb_set_efivar },
+ { "set-timeout-oneshot", 2, 2, 0, verb_set_efivar },
+ { "random-seed", VERB_ANY, 1, 0, verb_random_seed },
+ { "systemd-efi-options", VERB_ANY, 2, 0, verb_systemd_efi_options },
+ { "reboot-to-firmware", VERB_ANY, 2, 0, verb_reboot_to_firmware },
+ {}
+ };
+
+ return dispatch_verb(argc, argv, verbs, NULL);
+}
+
+static int run(int argc, char *argv[]) {
+ _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
+ _cleanup_(umount_and_rmdir_and_freep) char *unlink_dir = NULL;
+ int r;
+
+ log_parse_environment();
+ log_open();
+
+ /* If we run in a container, automatically turn off EFI file system access */
+ if (detect_container() > 0)
+ arg_touch_variables = false;
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ /* Open up and mount the image */
+ if (arg_image) {
+ assert(!arg_root);
+
+ r = mount_image_privately_interactively(
+ arg_image,
+ DISSECT_IMAGE_GENERIC_ROOT |
+ DISSECT_IMAGE_RELAX_VAR_CHECK,
+ &unlink_dir,
+ &loop_device);
+ if (r < 0)
+ return r;
+
+ arg_root = strdup(unlink_dir);
+ if (!arg_root)
+ return log_oom();
+ }
+
+ return bootctl_main(argc, argv);
+}
+
+DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run);
diff --git a/src/boot/efi/assert.c b/src/boot/efi/assert.c
new file mode 100644
index 0000000..bb16d2b
--- /dev/null
+++ b/src/boot/efi/assert.c
@@ -0,0 +1,12 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <efi.h>
+#include <efilib.h>
+
+#include "util.h"
+
+void efi_assert(const char *expr, const char *file, unsigned line, const char *function) {
+ log_error_stall(L"systemd-boot assertion '%a' failed at %a:%u, function %a(). Halting.", expr, file, line, function);
+ for (;;)
+ BS->Stall(60 * 1000 * 1000);
+}
diff --git a/src/boot/efi/bcd.c b/src/boot/efi/bcd.c
new file mode 100644
index 0000000..7200012
--- /dev/null
+++ b/src/boot/efi/bcd.c
@@ -0,0 +1,306 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdalign.h>
+
+#include "bcd.h"
+#include "efi-string.h"
+
+enum {
+ SIG_BASE_BLOCK = 1718052210, /* regf */
+ SIG_KEY = 27502, /* nk */
+ SIG_SUBKEY_FAST = 26220, /* lf */
+ SIG_KEY_VALUE = 27510, /* vk */
+};
+
+enum {
+ REG_SZ = 1,
+ REG_MULTI_SZ = 7,
+};
+
+/* These structs contain a lot more members than we care for. They have all
+ * been squashed into _padN for our convenience. */
+
+typedef struct {
+ uint32_t sig;
+ uint32_t primary_seqnum;
+ uint32_t secondary_seqnum;
+ uint64_t _pad1;
+ uint32_t version_major;
+ uint32_t version_minor;
+ uint32_t type;
+ uint32_t _pad2;
+ uint32_t root_cell_offset;
+ uint64_t _pad3[507];
+} _packed_ BaseBlock;
+assert_cc(sizeof(BaseBlock) == 4096);
+assert_cc(offsetof(BaseBlock, sig) == 0);
+assert_cc(offsetof(BaseBlock, primary_seqnum) == 4);
+assert_cc(offsetof(BaseBlock, secondary_seqnum) == 8);
+assert_cc(offsetof(BaseBlock, version_major) == 20);
+assert_cc(offsetof(BaseBlock, version_minor) == 24);
+assert_cc(offsetof(BaseBlock, type) == 28);
+assert_cc(offsetof(BaseBlock, root_cell_offset) == 36);
+
+/* All offsets are relative to the base block and technically point to a hive
+ * cell struct. But for our usecase we don't need to bother about that one,
+ * so skip over the cell_size uint32_t. */
+#define HIVE_CELL_OFFSET (sizeof(BaseBlock) + 4)
+
+typedef struct {
+ uint16_t sig;
+ uint16_t _pad1[13];
+ uint32_t subkeys_offset;
+ uint32_t _pad2;
+ uint32_t n_key_values;
+ uint32_t key_values_offset;
+ uint32_t _pad3[7];
+ uint16_t key_name_len;
+ uint16_t _pad4;
+ char key_name[];
+} _packed_ Key;
+assert_cc(offsetof(Key, sig) == 0);
+assert_cc(offsetof(Key, subkeys_offset) == 28);
+assert_cc(offsetof(Key, n_key_values) == 36);
+assert_cc(offsetof(Key, key_values_offset) == 40);
+assert_cc(offsetof(Key, key_name_len) == 72);
+assert_cc(offsetof(Key, key_name) == 76);
+
+typedef struct {
+ uint16_t sig;
+ uint16_t n_entries;
+ struct SubkeyFastEntry {
+ uint32_t key_offset;
+ char name_hint[4];
+ } _packed_ entries[];
+} _packed_ SubkeyFast;
+assert_cc(offsetof(SubkeyFast, sig) == 0);
+assert_cc(offsetof(SubkeyFast, n_entries) == 2);
+assert_cc(offsetof(SubkeyFast, entries) == 4);
+
+typedef struct {
+ uint16_t sig;
+ uint16_t name_len;
+ uint32_t data_size;
+ uint32_t data_offset;
+ uint32_t data_type;
+ uint32_t _pad;
+ char name[];
+} _packed_ KeyValue;
+assert_cc(offsetof(KeyValue, sig) == 0);
+assert_cc(offsetof(KeyValue, name_len) == 2);
+assert_cc(offsetof(KeyValue, data_size) == 4);
+assert_cc(offsetof(KeyValue, data_offset) == 8);
+assert_cc(offsetof(KeyValue, data_type) == 12);
+assert_cc(offsetof(KeyValue, name) == 20);
+
+#define BAD_OFFSET(offset, len, max) \
+ ((uint64_t) (offset) + (len) >= (max))
+
+#define BAD_STRUCT(type, offset, max) \
+ ((uint64_t) (offset) + sizeof(type) >= (max))
+
+#define BAD_ARRAY(type, array, offset, array_len, max) \
+ ((uint64_t) (offset) + offsetof(type, array) + \
+ sizeof((type){}.array[0]) * (uint64_t) (array_len) >= (max))
+
+static const Key *get_key(const uint8_t *bcd, uint32_t bcd_len, uint32_t offset, const char *name);
+
+static const Key *get_subkey(const uint8_t *bcd, uint32_t bcd_len, uint32_t offset, const char *name) {
+ assert(bcd);
+ assert(name);
+
+ if (BAD_STRUCT(SubkeyFast, offset, bcd_len))
+ return NULL;
+
+ const SubkeyFast *subkey = (const SubkeyFast *) (bcd + offset);
+ if (subkey->sig != SIG_SUBKEY_FAST)
+ return NULL;
+
+ if (BAD_ARRAY(SubkeyFast, entries, offset, subkey->n_entries, bcd_len))
+ return NULL;
+
+ for (uint16_t i = 0; i < subkey->n_entries; i++) {
+ if (!strncaseeq8(name, subkey->entries[i].name_hint, sizeof(subkey->entries[i].name_hint)))
+ continue;
+
+ const Key *key = get_key(bcd, bcd_len, subkey->entries[i].key_offset, name);
+ if (key)
+ return key;
+ }
+
+ return NULL;
+}
+
+/* We use NUL as registry path separators for convenience. To start from the root, begin
+ * name with a NUL. Name must end with two NUL. The lookup depth is not restricted, so
+ * name must be properly validated before calling get_key(). */
+static const Key *get_key(const uint8_t *bcd, uint32_t bcd_len, uint32_t offset, const char *name) {
+ assert(bcd);
+ assert(name);
+
+ if (BAD_STRUCT(Key, offset, bcd_len))
+ return NULL;
+
+ const Key *key = (const Key *) (bcd + offset);
+ if (key->sig != SIG_KEY)
+ return NULL;
+
+ if (BAD_ARRAY(Key, key_name, offset, key->key_name_len, bcd_len))
+ return NULL;
+
+ if (*name) {
+ if (strncaseeq8(name, key->key_name, key->key_name_len) && strlen8(name) == key->key_name_len)
+ name += key->key_name_len;
+ else
+ return NULL;
+ }
+
+ name++;
+ return *name ? get_subkey(bcd, bcd_len, key->subkeys_offset, name) : key;
+}
+
+static const KeyValue *get_key_value(const uint8_t *bcd, uint32_t bcd_len, const Key *key, const char *name) {
+ assert(bcd);
+ assert(key);
+ assert(name);
+
+ if (key->n_key_values == 0)
+ return NULL;
+
+ if (BAD_OFFSET(key->key_values_offset, sizeof(uint32_t) * (uint64_t) key->n_key_values, bcd_len) ||
+ (uintptr_t) (bcd + key->key_values_offset) % alignof(uint32_t) != 0)
+ return NULL;
+
+ const uint32_t *key_value_list = (const uint32_t *) (bcd + key->key_values_offset);
+ for (uint32_t i = 0; i < key->n_key_values; i++) {
+ uint32_t offset = *(key_value_list + i);
+
+ if (BAD_STRUCT(KeyValue, offset, bcd_len))
+ continue;
+
+ const KeyValue *kv = (const KeyValue *) (bcd + offset);
+ if (kv->sig != SIG_KEY_VALUE)
+ continue;
+
+ if (BAD_ARRAY(KeyValue, name, offset, kv->name_len, bcd_len))
+ continue;
+
+ /* If most significant bit is set, data is stored in data_offset itself, but
+ * we are only interested in UTF16 strings. The only strings that could fit
+ * would have just one char in it, so let's not bother with this. */
+ if (FLAGS_SET(kv->data_size, UINT32_C(1) << 31))
+ continue;
+
+ if (BAD_OFFSET(kv->data_offset, kv->data_size, bcd_len))
+ continue;
+
+ if (strncaseeq8(name, kv->name, kv->name_len) && strlen8(name) == kv->name_len)
+ return kv;
+ }
+
+ return NULL;
+}
+
+/* The BCD store is really just a regular windows registry hive with a rather cryptic internal
+ * key structure. On a running system it gets mounted to HKEY_LOCAL_MACHINE\BCD00000000.
+ *
+ * Of interest to us are the these two keys:
+ * - \Objects\{bootmgr}\Elements\24000001
+ * This key is the "displayorder" property and contains a value of type REG_MULTI_SZ
+ * with the name "Element" that holds a {GUID} list (UTF16, NUL-separated).
+ * - \Objects\{GUID}\Elements\12000004
+ * This key is the "description" property and contains a value of type REG_SZ with the
+ * name "Element" that holds a NUL-terminated UTF16 string.
+ *
+ * The GUIDs and properties are as reported by "bcdedit.exe /v".
+ *
+ * To get a title for the BCD store we first look at the displayorder property of {bootmgr}
+ * (it always has the GUID 9dea862c-5cdd-4e70-acc1-f32b344d4795). If it contains more than
+ * one GUID, the BCD is multi-boot and we stop looking. Otherwise we take that GUID, look it
+ * up, and return its description property. */
+char16_t *get_bcd_title(uint8_t *bcd, size_t bcd_len) {
+ assert(bcd);
+
+ if (HIVE_CELL_OFFSET >= bcd_len)
+ return NULL;
+
+ BaseBlock *base_block = (BaseBlock *) bcd;
+ if (base_block->sig != SIG_BASE_BLOCK ||
+ base_block->version_major != 1 ||
+ base_block->version_minor != 3 ||
+ base_block->type != 0 ||
+ base_block->primary_seqnum != base_block->secondary_seqnum)
+ return NULL;
+
+ bcd += HIVE_CELL_OFFSET;
+ bcd_len -= HIVE_CELL_OFFSET;
+
+ const Key *objects_key = get_key(bcd, bcd_len, base_block->root_cell_offset, "\0Objects\0");
+ if (!objects_key)
+ return NULL;
+
+ const Key *displayorder_key = get_subkey(
+ bcd,
+ bcd_len,
+ objects_key->subkeys_offset,
+ "{9dea862c-5cdd-4e70-acc1-f32b344d4795}\0Elements\00024000001\0");
+ if (!displayorder_key)
+ return NULL;
+
+ const KeyValue *displayorder_value = get_key_value(bcd, bcd_len, displayorder_key, "Element");
+ if (!displayorder_value)
+ return NULL;
+
+ char order_guid[sizeof("{00000000-0000-0000-0000-000000000000}\0")];
+ if (displayorder_value->data_type != REG_MULTI_SZ ||
+ displayorder_value->data_size != sizeof(char16_t[sizeof(order_guid)]) ||
+ (uintptr_t) (bcd + displayorder_value->data_offset) % alignof(char16_t) != 0)
+ /* BCD is multi-boot. */
+ return NULL;
+
+ /* Keys are stored as ASCII in registry hives if the data fits (and GUIDS always should). */
+ char16_t *order_guid_utf16 = (char16_t *) (bcd + displayorder_value->data_offset);
+ for (size_t i = 0; i < sizeof(order_guid) - 2; i++) {
+ char16_t c = order_guid_utf16[i];
+ switch (c) {
+ case '-':
+ case '{':
+ case '}':
+ case '0' ... '9':
+ case 'a' ... 'f':
+ case 'A' ... 'F':
+ order_guid[i] = c;
+ break;
+ default:
+ /* Not a valid GUID. */
+ return NULL;
+ }
+ }
+ /* Our functions expect the lookup key to be double-derminated. */
+ order_guid[sizeof(order_guid) - 2] = '\0';
+ order_guid[sizeof(order_guid) - 1] = '\0';
+
+ const Key *default_key = get_subkey(bcd, bcd_len, objects_key->subkeys_offset, order_guid);
+ if (!default_key)
+ return NULL;
+
+ const Key *description_key = get_subkey(
+ bcd, bcd_len, default_key->subkeys_offset, "Elements\00012000004\0");
+ if (!description_key)
+ return NULL;
+
+ const KeyValue *description_value = get_key_value(bcd, bcd_len, description_key, "Element");
+ if (!description_value)
+ return NULL;
+
+ if (description_value->data_type != REG_SZ ||
+ description_value->data_size < sizeof(char16_t) ||
+ description_value->data_size % sizeof(char16_t) != 0 ||
+ (uintptr_t) (bcd + description_value->data_offset) % alignof(char16_t))
+ return NULL;
+
+ /* The data should already be NUL-terminated. */
+ char16_t *title = (char16_t *) (bcd + description_value->data_offset);
+ title[description_value->data_size / sizeof(char16_t) - 1] = '\0';
+ return title;
+}
diff --git a/src/boot/efi/bcd.h b/src/boot/efi/bcd.h
new file mode 100644
index 0000000..c27af55
--- /dev/null
+++ b/src/boot/efi/bcd.h
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <stdint.h>
+#include <uchar.h>
+
+char16_t *get_bcd_title(uint8_t *bcd, size_t bcd_len);
diff --git a/src/boot/efi/boot.c b/src/boot/efi/boot.c
new file mode 100644
index 0000000..d7b94bc
--- /dev/null
+++ b/src/boot/efi/boot.c
@@ -0,0 +1,2778 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <efi.h>
+#include <efigpt.h>
+#include <efilib.h>
+
+#include "bcd.h"
+#include "bootspec-fundamental.h"
+#include "console.h"
+#include "devicetree.h"
+#include "disk.h"
+#include "drivers.h"
+#include "efivars-fundamental.h"
+#include "graphics.h"
+#include "initrd.h"
+#include "linux.h"
+#include "measure.h"
+#include "pe.h"
+#include "vmm.h"
+#include "random-seed.h"
+#include "secure-boot.h"
+#include "shim.h"
+#include "ticks.h"
+#include "util.h"
+#include "xbootldr.h"
+
+#ifndef GNU_EFI_USE_MS_ABI
+ /* We do not use uefi_call_wrapper() in systemd-boot. As such, we rely on the
+ * compiler to do the calling convention conversion for us. This is check is
+ * to make sure the -DGNU_EFI_USE_MS_ABI was passed to the comiler. */
+ #error systemd-boot requires compilation with GNU_EFI_USE_MS_ABI defined.
+#endif
+
+#define TEXT_ATTR_SWAP(c) EFI_TEXT_ATTR(((c) & 0b11110000) >> 4, (c) & 0b1111)
+
+/* Magic string for recognizing our own binaries */
+_used_ _section_(".sdmagic") static const char magic[] =
+ "#### LoaderInfo: systemd-boot " GIT_VERSION " ####";
+
+/* Makes systemd-boot available from \EFI\Linux\ for testing purposes. */
+_used_ _section_(".osrel") static const char osrel[] =
+ "ID=systemd-boot\n"
+ "VERSION=\"" GIT_VERSION "\"\n"
+ "NAME=\"systemd-boot " GIT_VERSION "\"\n";
+
+enum loader_type {
+ LOADER_UNDEFINED,
+ LOADER_AUTO,
+ LOADER_EFI,
+ LOADER_LINUX, /* Boot loader spec type #1 entries */
+ LOADER_UNIFIED_LINUX, /* Boot loader spec type #2 entries */
+ LOADER_SECURE_BOOT_KEYS,
+};
+
+typedef struct {
+ char16_t *id; /* The unique identifier for this entry (typically the filename of the file defining the entry) */
+ char16_t *title_show; /* The string to actually display (this is made unique before showing) */
+ char16_t *title; /* The raw (human readable) title string of the entry (not necessarily unique) */
+ char16_t *sort_key; /* The string to use as primary sort key, usually ID= from os-release, possibly suffixed */
+ char16_t *version; /* The raw (human readable) version string of the entry */
+ char16_t *machine_id;
+ EFI_HANDLE *device;
+ enum loader_type type;
+ char16_t *loader;
+ char16_t *devicetree;
+ char16_t *options;
+ char16_t **initrd;
+ char16_t key;
+ EFI_STATUS (*call)(void);
+ int tries_done;
+ int tries_left;
+ char16_t *path;
+ char16_t *current_name;
+ char16_t *next_name;
+} ConfigEntry;
+
+typedef struct {
+ ConfigEntry **entries;
+ UINTN entry_count;
+ UINTN idx_default;
+ UINTN idx_default_efivar;
+ uint32_t timeout_sec; /* Actual timeout used (efi_main() override > efivar > config). */
+ uint32_t timeout_sec_config;
+ uint32_t timeout_sec_efivar;
+ char16_t *entry_default_config;
+ char16_t *entry_default_efivar;
+ char16_t *entry_oneshot;
+ char16_t *entry_saved;
+ bool editor;
+ bool auto_entries;
+ bool auto_firmware;
+ bool reboot_for_bitlocker;
+ secure_boot_enroll secure_boot_enroll;
+ bool force_menu;
+ bool use_saved_entry;
+ bool use_saved_entry_efivar;
+ bool beep;
+ int64_t console_mode;
+ int64_t console_mode_efivar;
+ RandomSeedMode random_seed_mode;
+} Config;
+
+/* These values have been chosen so that the transitions the user sees could
+ * employ unsigned over-/underflow like this:
+ * efivar unset ↔ force menu ↔ no timeout/skip menu ↔ 1 s ↔ 2 s ↔ … */
+enum {
+ TIMEOUT_MIN = 1,
+ TIMEOUT_MAX = UINT32_MAX - 2U,
+ TIMEOUT_UNSET = UINT32_MAX - 1U,
+ TIMEOUT_MENU_FORCE = UINT32_MAX,
+ TIMEOUT_MENU_HIDDEN = 0,
+ TIMEOUT_TYPE_MAX = UINT32_MAX,
+};
+
+enum {
+ IDX_MAX = INT16_MAX,
+ IDX_INVALID,
+};
+
+static void cursor_left(UINTN *cursor, UINTN *first) {
+ assert(cursor);
+ assert(first);
+
+ if ((*cursor) > 0)
+ (*cursor)--;
+ else if ((*first) > 0)
+ (*first)--;
+}
+
+static void cursor_right(
+ UINTN *cursor,
+ UINTN *first,
+ UINTN x_max,
+ UINTN len) {
+
+ assert(cursor);
+ assert(first);
+
+ if ((*cursor)+1 < x_max)
+ (*cursor)++;
+ else if ((*first) + (*cursor) < len)
+ (*first)++;
+}
+
+static bool line_edit(
+ char16_t **line_in,
+ UINTN x_max,
+ UINTN y_pos) {
+
+ _cleanup_free_ char16_t *line = NULL, *print = NULL;
+ UINTN size, len, first = 0, cursor = 0, clear = 0;
+
+ assert(line_in);
+
+ len = strlen16(*line_in);
+ size = len + 1024;
+ line = xnew(char16_t, size);
+ print = xnew(char16_t, x_max + 1);
+ strcpy16(line, strempty(*line_in));
+
+ for (;;) {
+ EFI_STATUS err;
+ uint64_t key;
+ UINTN j;
+ UINTN cursor_color = TEXT_ATTR_SWAP(COLOR_EDIT);
+
+ j = MIN(len - first, x_max);
+ memcpy(print, line + first, j * sizeof(char16_t));
+ while (clear > 0 && j < x_max) {
+ clear--;
+ print[j++] = ' ';
+ }
+ print[j] = '\0';
+
+ /* See comment at edit_line() call site for why we start at 1. */
+ print_at(1, y_pos, COLOR_EDIT, print);
+
+ if (!print[cursor])
+ print[cursor] = ' ';
+ print[cursor+1] = '\0';
+ do {
+ print_at(cursor + 1, y_pos, cursor_color, print + cursor);
+ cursor_color = TEXT_ATTR_SWAP(cursor_color);
+
+ err = console_key_read(&key, 750 * 1000);
+ if (!IN_SET(err, EFI_SUCCESS, EFI_TIMEOUT, EFI_NOT_READY))
+ return false;
+
+ print_at(cursor + 1, y_pos, COLOR_EDIT, print + cursor);
+ } while (err != EFI_SUCCESS);
+
+ switch (key) {
+ case KEYPRESS(0, SCAN_ESC, 0):
+ case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'c'):
+ case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'g'):
+ case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('c')):
+ case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('g')):
+ return false;
+
+ case KEYPRESS(0, SCAN_HOME, 0):
+ case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'a'):
+ case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('a')):
+ /* beginning-of-line */
+ cursor = 0;
+ first = 0;
+ continue;
+
+ case KEYPRESS(0, SCAN_END, 0):
+ case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'e'):
+ case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('e')):
+ /* end-of-line */
+ cursor = len - first;
+ if (cursor+1 >= x_max) {
+ cursor = x_max-1;
+ first = len - (x_max-1);
+ }
+ continue;
+
+ case KEYPRESS(0, SCAN_DOWN, 0):
+ case KEYPRESS(EFI_ALT_PRESSED, 0, 'f'):
+ case KEYPRESS(EFI_CONTROL_PRESSED, SCAN_RIGHT, 0):
+ /* forward-word */
+ while (line[first + cursor] == ' ')
+ cursor_right(&cursor, &first, x_max, len);
+ while (line[first + cursor] && line[first + cursor] != ' ')
+ cursor_right(&cursor, &first, x_max, len);
+ continue;
+
+ case KEYPRESS(0, SCAN_UP, 0):
+ case KEYPRESS(EFI_ALT_PRESSED, 0, 'b'):
+ case KEYPRESS(EFI_CONTROL_PRESSED, SCAN_LEFT, 0):
+ /* backward-word */
+ if ((first + cursor) > 0 && line[first + cursor-1] == ' ') {
+ cursor_left(&cursor, &first);
+ while ((first + cursor) > 0 && line[first + cursor] == ' ')
+ cursor_left(&cursor, &first);
+ }
+ while ((first + cursor) > 0 && line[first + cursor-1] != ' ')
+ cursor_left(&cursor, &first);
+ continue;
+
+ case KEYPRESS(0, SCAN_RIGHT, 0):
+ case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'f'):
+ case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('f')):
+ /* forward-char */
+ if (first + cursor == len)
+ continue;
+ cursor_right(&cursor, &first, x_max, len);
+ continue;
+
+ case KEYPRESS(0, SCAN_LEFT, 0):
+ case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'b'):
+ case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('b')):
+ /* backward-char */
+ cursor_left(&cursor, &first);
+ continue;
+
+ case KEYPRESS(EFI_CONTROL_PRESSED, SCAN_DELETE, 0):
+ case KEYPRESS(EFI_ALT_PRESSED, 0, 'd'):
+ /* kill-word */
+ clear = 0;
+
+ UINTN k;
+ for (k = first + cursor; k < len && line[k] == ' '; k++)
+ clear++;
+ for (; k < len && line[k] != ' '; k++)
+ clear++;
+
+ for (UINTN i = first + cursor; i + clear < len; i++)
+ line[i] = line[i + clear];
+ len -= clear;
+ line[len] = '\0';
+ continue;
+
+ case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'w'):
+ case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('w')):
+ case KEYPRESS(EFI_ALT_PRESSED, 0, CHAR_BACKSPACE):
+ /* backward-kill-word */
+ clear = 0;
+ if ((first + cursor) > 0 && line[first + cursor-1] == ' ') {
+ cursor_left(&cursor, &first);
+ clear++;
+ while ((first + cursor) > 0 && line[first + cursor] == ' ') {
+ cursor_left(&cursor, &first);
+ clear++;
+ }
+ }
+ while ((first + cursor) > 0 && line[first + cursor-1] != ' ') {
+ cursor_left(&cursor, &first);
+ clear++;
+ }
+
+ for (UINTN i = first + cursor; i + clear < len; i++)
+ line[i] = line[i + clear];
+ len -= clear;
+ line[len] = '\0';
+ continue;
+
+ case KEYPRESS(0, SCAN_DELETE, 0):
+ case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'd'):
+ case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('d')):
+ if (len == 0)
+ continue;
+ if (first + cursor == len)
+ continue;
+ for (UINTN i = first + cursor; i < len; i++)
+ line[i] = line[i+1];
+ clear = 1;
+ len--;
+ continue;
+
+ case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'k'):
+ case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('k')):
+ /* kill-line */
+ line[first + cursor] = '\0';
+ clear = len - (first + cursor);
+ len = first + cursor;
+ continue;
+
+ case KEYPRESS(0, 0, CHAR_LINEFEED):
+ case KEYPRESS(0, 0, CHAR_CARRIAGE_RETURN):
+ case KEYPRESS(0, SCAN_F3, 0): /* EZpad Mini 4s firmware sends malformed events */
+ case KEYPRESS(0, SCAN_F3, CHAR_CARRIAGE_RETURN): /* Teclast X98+ II firmware sends malformed events */
+ if (!streq16(line, *line_in)) {
+ free(*line_in);
+ *line_in = TAKE_PTR(line);
+ }
+ return true;
+
+ case KEYPRESS(0, 0, CHAR_BACKSPACE):
+ if (len == 0)
+ continue;
+ if (first == 0 && cursor == 0)
+ continue;
+ for (UINTN i = first + cursor-1; i < len; i++)
+ line[i] = line[i+1];
+ clear = 1;
+ len--;
+ if (cursor > 0)
+ cursor--;
+ if (cursor > 0 || first == 0)
+ continue;
+ /* show full line if it fits */
+ if (len < x_max) {
+ cursor = first;
+ first = 0;
+ continue;
+ }
+ /* jump left to see what we delete */
+ if (first > 10) {
+ first -= 10;
+ cursor = 10;
+ } else {
+ cursor = first;
+ first = 0;
+ }
+ continue;
+
+ case KEYPRESS(0, 0, ' ') ... KEYPRESS(0, 0, '~'):
+ case KEYPRESS(0, 0, 0x80) ... KEYPRESS(0, 0, 0xffff):
+ if (len+1 == size)
+ continue;
+ for (UINTN i = len; i > first + cursor; i--)
+ line[i] = line[i-1];
+ line[first + cursor] = KEYCHAR(key);
+ len++;
+ line[len] = '\0';
+ if (cursor+1 < x_max)
+ cursor++;
+ else if (first + cursor < len)
+ first++;
+ continue;
+ }
+ }
+}
+
+static UINTN entry_lookup_key(Config *config, UINTN start, char16_t key) {
+ assert(config);
+
+ if (key == 0)
+ return IDX_INVALID;
+
+ /* select entry by number key */
+ if (key >= '1' && key <= '9') {
+ UINTN i = key - '0';
+ if (i > config->entry_count)
+ i = config->entry_count;
+ return i-1;
+ }
+
+ /* find matching key in config entries */
+ for (UINTN i = start; i < config->entry_count; i++)
+ if (config->entries[i]->key == key)
+ return i;
+
+ for (UINTN i = 0; i < start; i++)
+ if (config->entries[i]->key == key)
+ return i;
+
+ return IDX_INVALID;
+}
+
+static char16_t *update_timeout_efivar(uint32_t *t, bool inc) {
+ assert(t);
+
+ switch (*t) {
+ case TIMEOUT_MAX:
+ *t = inc ? TIMEOUT_MAX : (*t - 1);
+ break;
+ case TIMEOUT_UNSET:
+ *t = inc ? TIMEOUT_MENU_FORCE : TIMEOUT_UNSET;
+ break;
+ case TIMEOUT_MENU_FORCE:
+ *t = inc ? TIMEOUT_MENU_HIDDEN : TIMEOUT_UNSET;
+ break;
+ case TIMEOUT_MENU_HIDDEN:
+ *t = inc ? TIMEOUT_MIN : TIMEOUT_MENU_FORCE;
+ break;
+ default:
+ *t += inc ? 1 : -1;
+ }
+
+ switch (*t) {
+ case TIMEOUT_UNSET:
+ return xstrdup16(u"Menu timeout defined by configuration file.");
+ case TIMEOUT_MENU_FORCE:
+ return xstrdup16(u"Timeout disabled, menu will always be shown.");
+ case TIMEOUT_MENU_HIDDEN:
+ return xstrdup16(u"Menu disabled. Hold down key at bootup to show menu.");
+ default:
+ return xpool_print(L"Menu timeout set to %u s.", *t);
+ }
+}
+
+static bool unicode_supported(void) {
+ static int cache = -1;
+
+ if (cache < 0)
+ /* Basic unicode box drawing support is mandated by the spec, but it does
+ * not hurt to make sure it works. */
+ cache = ST->ConOut->TestString(ST->ConOut, (char16_t *) L"─") == EFI_SUCCESS;
+
+ return cache;
+}
+
+static void ps_string(const char16_t *fmt, const void *value) {
+ assert(fmt);
+ if (value)
+ Print(fmt, value);
+}
+
+static void ps_bool(const char16_t *fmt, bool value) {
+ assert(fmt);
+ Print(fmt, yes_no(value));
+}
+
+static bool ps_continue(void) {
+ if (unicode_supported())
+ Print(L"\n─── Press any key to continue, ESC or q to quit. ───\n\n");
+ else
+ Print(L"\n--- Press any key to continue, ESC or q to quit. ---\n\n");
+
+ uint64_t key;
+ return console_key_read(&key, UINT64_MAX) == EFI_SUCCESS &&
+ !IN_SET(key, KEYPRESS(0, SCAN_ESC, 0), KEYPRESS(0, 0, 'q'), KEYPRESS(0, 0, 'Q'));
+}
+
+static void print_status(Config *config, char16_t *loaded_image_path) {
+ UINTN x_max, y_max;
+ uint32_t screen_width = 0, screen_height = 0;
+ SecureBootMode secure;
+ _cleanup_free_ char16_t *device_part_uuid = NULL;
+
+ assert(config);
+
+ clear_screen(COLOR_NORMAL);
+ console_query_mode(&x_max, &y_max);
+ query_screen_resolution(&screen_width, &screen_height);
+
+ secure = secure_boot_mode();
+ (void) efivar_get(LOADER_GUID, L"LoaderDevicePartUUID", &device_part_uuid);
+
+ /* We employ some unusual indentation here for readability. */
+
+ ps_string(L" systemd-boot version: %a\n", GIT_VERSION);
+ ps_string(L" loaded image: %s\n", loaded_image_path);
+ ps_string(L" loader partition UUID: %s\n", device_part_uuid);
+ ps_string(L" architecture: %a\n", EFI_MACHINE_TYPE_NAME);
+ Print(L" UEFI specification: %u.%02u\n", ST->Hdr.Revision >> 16, ST->Hdr.Revision & 0xffff);
+ ps_string(L" firmware vendor: %s\n", ST->FirmwareVendor);
+ Print(L" firmware version: %u.%02u\n", ST->FirmwareRevision >> 16, ST->FirmwareRevision & 0xffff);
+ Print(L" OS indications: %lu\n", get_os_indications_supported());
+ Print(L" secure boot: %s (%s)\n", yes_no(IN_SET(secure, SECURE_BOOT_USER, SECURE_BOOT_DEPLOYED)), secure_boot_mode_to_string(secure));
+ ps_bool(L" shim: %s\n", shim_loaded());
+ ps_bool(L" TPM: %s\n", tpm_present());
+ Print(L" console mode: %d/%ld (%" PRIuN L"x%" PRIuN L" @%ux%u)\n", ST->ConOut->Mode->Mode, ST->ConOut->Mode->MaxMode - INT64_C(1), x_max, y_max, screen_width, screen_height);
+
+ if (!ps_continue())
+ return;
+
+ switch (config->timeout_sec_config) {
+ case TIMEOUT_UNSET:
+ break;
+ case TIMEOUT_MENU_FORCE:
+ Print(L" timeout (config): menu-force\n"); break;
+ case TIMEOUT_MENU_HIDDEN:
+ Print(L" timeout (config): menu-hidden\n"); break;
+ default:
+ Print(L" timeout (config): %u s\n", config->timeout_sec_config);
+ }
+
+ switch (config->timeout_sec_efivar) {
+ case TIMEOUT_UNSET:
+ break;
+ case TIMEOUT_MENU_FORCE:
+ Print(L" timeout (EFI var): menu-force\n"); break;
+ case TIMEOUT_MENU_HIDDEN:
+ Print(L" timeout (EFI var): menu-hidden\n"); break;
+ default:
+ Print(L" timeout (EFI var): %u s\n", config->timeout_sec_efivar);
+ }
+
+ ps_string(L" default (config): %s\n", config->entry_default_config);
+ ps_string(L" default (EFI var): %s\n", config->entry_default_efivar);
+ ps_string(L" default (one-shot): %s\n", config->entry_oneshot);
+ ps_string(L" saved entry: %s\n", config->entry_saved);
+ ps_bool(L" editor: %s\n", config->editor);
+ ps_bool(L" auto-entries: %s\n", config->auto_entries);
+ ps_bool(L" auto-firmware: %s\n", config->auto_firmware);
+ ps_bool(L" beep: %s\n", config->beep);
+ ps_bool(L" reboot-for-bitlocker: %s\n", config->reboot_for_bitlocker);
+ ps_string(L" random-seed-mode: %s\n", random_seed_modes_table[config->random_seed_mode]);
+
+ switch (config->secure_boot_enroll) {
+ case ENROLL_OFF:
+ Print(L" secure-boot-enroll: off\n"); break;
+ case ENROLL_MANUAL:
+ Print(L" secure-boot-enroll: manual\n"); break;
+ case ENROLL_FORCE:
+ Print(L" secure-boot-enroll: force\n"); break;
+ default:
+ assert_not_reached();
+ }
+
+ switch (config->console_mode) {
+ case CONSOLE_MODE_AUTO:
+ Print(L" console-mode (config): %s\n", L"auto"); break;
+ case CONSOLE_MODE_KEEP:
+ Print(L" console-mode (config): %s\n", L"keep"); break;
+ case CONSOLE_MODE_FIRMWARE_MAX:
+ Print(L" console-mode (config): %s\n", L"max"); break;
+ default:
+ Print(L" console-mode (config): %ld\n", config->console_mode); break;
+ }
+
+ /* EFI var console mode is always a concrete value or unset. */
+ if (config->console_mode_efivar != CONSOLE_MODE_KEEP)
+ Print(L"console-mode (EFI var): %ld\n", config->console_mode_efivar);
+
+ if (!ps_continue())
+ return;
+
+ for (UINTN i = 0; i < config->entry_count; i++) {
+ ConfigEntry *entry = config->entries[i];
+
+ _cleanup_free_ char16_t *dp = NULL;
+ if (entry->device)
+ (void) device_path_to_str(DevicePathFromHandle(entry->device), &dp);
+
+ Print(L" config entry: %" PRIuN L"/%" PRIuN L"\n", i + 1, config->entry_count);
+ ps_string(L" id: %s\n", entry->id);
+ ps_string(L" title: %s\n", entry->title);
+ ps_string(L" title show: %s\n", streq16(entry->title, entry->title_show) ? NULL : entry->title_show);
+ ps_string(L" sort key: %s\n", entry->sort_key);
+ ps_string(L" version: %s\n", entry->version);
+ ps_string(L" machine-id: %s\n", entry->machine_id);
+ ps_string(L" device: %s\n", dp);
+ ps_string(L" loader: %s\n", entry->loader);
+ STRV_FOREACH(initrd, entry->initrd)
+ Print(L" initrd: %s\n", *initrd);
+ ps_string(L" devicetree: %s\n", entry->devicetree);
+ ps_string(L" options: %s\n", entry->options);
+ ps_bool(L" internal call: %s\n", !!entry->call);
+
+ ps_bool(L"counting boots: %s\n", entry->tries_left >= 0);
+ if (entry->tries_left >= 0) {
+ Print(L" tries: %u left, %u done\n", entry->tries_left, entry->tries_done);
+ Print(L" current path: %s\\%s\n", entry->path, entry->current_name);
+ Print(L" next path: %s\\%s\n", entry->path, entry->next_name);
+ }
+
+ if (!ps_continue())
+ return;
+ }
+}
+
+static EFI_STATUS reboot_into_firmware(void) {
+ uint64_t osind = 0;
+ EFI_STATUS err;
+
+ if (!FLAGS_SET(get_os_indications_supported(), EFI_OS_INDICATIONS_BOOT_TO_FW_UI))
+ return log_error_status_stall(EFI_UNSUPPORTED, L"Reboot to firmware interface not supported.");
+
+ (void) efivar_get_uint64_le(EFI_GLOBAL_GUID, L"OsIndications", &osind);
+ osind |= EFI_OS_INDICATIONS_BOOT_TO_FW_UI;
+
+ err = efivar_set_uint64_le(EFI_GLOBAL_GUID, L"OsIndications", osind, EFI_VARIABLE_NON_VOLATILE);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Error setting OsIndications: %r", err);
+
+ RT->ResetSystem(EfiResetCold, EFI_SUCCESS, 0, NULL);
+ assert_not_reached();
+}
+
+static bool menu_run(
+ Config *config,
+ ConfigEntry **chosen_entry,
+ char16_t *loaded_image_path) {
+
+ assert(config);
+ assert(chosen_entry);
+
+ EFI_STATUS err;
+ UINTN visible_max = 0;
+ UINTN idx_highlight = config->idx_default;
+ UINTN idx_highlight_prev = 0;
+ UINTN idx, idx_first = 0, idx_last = 0;
+ bool new_mode = true, clear = true;
+ bool refresh = true, highlight = false;
+ UINTN x_start = 0, y_start = 0, y_status = 0;
+ UINTN x_max, y_max;
+ _cleanup_(strv_freep) char16_t **lines = NULL;
+ _cleanup_free_ char16_t *clearline = NULL, *separator = NULL, *status = NULL;
+ uint32_t timeout_efivar_saved = config->timeout_sec_efivar;
+ uint32_t timeout_remain = config->timeout_sec == TIMEOUT_MENU_FORCE ? 0 : config->timeout_sec;
+ bool exit = false, run = true, firmware_setup = false;
+ int64_t console_mode_initial = ST->ConOut->Mode->Mode, console_mode_efivar_saved = config->console_mode_efivar;
+ UINTN default_efivar_saved = config->idx_default_efivar;
+
+ graphics_mode(false);
+ ST->ConIn->Reset(ST->ConIn, false);
+ ST->ConOut->EnableCursor(ST->ConOut, false);
+
+ /* draw a single character to make ClearScreen work on some firmware */
+ Print(L" ");
+
+ err = console_set_mode(config->console_mode_efivar != CONSOLE_MODE_KEEP ?
+ config->console_mode_efivar : config->console_mode);
+ if (err != EFI_SUCCESS) {
+ clear_screen(COLOR_NORMAL);
+ log_error_stall(L"Error switching console mode: %r", err);
+ }
+
+ UINTN line_width = 0, entry_padding = 3;
+ while (!exit) {
+ uint64_t key;
+
+ if (new_mode) {
+ console_query_mode(&x_max, &y_max);
+
+ /* account for padding+status */
+ visible_max = y_max - 2;
+
+ /* Drawing entries starts at idx_first until idx_last. We want to make
+ * sure that idx_highlight is centered, but not if we are close to the
+ * beginning/end of the entry list. Otherwise we would have a half-empty
+ * screen. */
+ if (config->entry_count <= visible_max || idx_highlight <= visible_max / 2)
+ idx_first = 0;
+ else if (idx_highlight >= config->entry_count - (visible_max / 2))
+ idx_first = config->entry_count - visible_max;
+ else
+ idx_first = idx_highlight - (visible_max / 2);
+ idx_last = idx_first + visible_max - 1;
+
+ /* length of the longest entry */
+ line_width = 0;
+ for (UINTN i = 0; i < config->entry_count; i++)
+ line_width = MAX(line_width, strlen16(config->entries[i]->title_show));
+ line_width = MIN(line_width + 2 * entry_padding, x_max);
+
+ /* offsets to center the entries on the screen */
+ x_start = (x_max - (line_width)) / 2;
+ if (config->entry_count < visible_max)
+ y_start = ((visible_max - config->entry_count) / 2) + 1;
+ else
+ y_start = 0;
+
+ /* Put status line after the entry list, but give it some breathing room. */
+ y_status = MIN(y_start + MIN(visible_max, config->entry_count) + 1, y_max - 1);
+
+ lines = strv_free(lines);
+ clearline = mfree(clearline);
+ separator = mfree(separator);
+
+ /* menu entries title lines */
+ lines = xnew(char16_t *, config->entry_count + 1);
+
+ for (UINTN i = 0; i < config->entry_count; i++) {
+ UINTN j, padding;
+
+ lines[i] = xnew(char16_t, line_width + 1);
+ padding = (line_width - MIN(strlen16(config->entries[i]->title_show), line_width)) / 2;
+
+ for (j = 0; j < padding; j++)
+ lines[i][j] = ' ';
+
+ for (UINTN k = 0; config->entries[i]->title_show[k] != '\0' && j < line_width; j++, k++)
+ lines[i][j] = config->entries[i]->title_show[k];
+
+ for (; j < line_width; j++)
+ lines[i][j] = ' ';
+ lines[i][line_width] = '\0';
+ }
+ lines[config->entry_count] = NULL;
+
+ clearline = xnew(char16_t, x_max + 1);
+ separator = xnew(char16_t, x_max + 1);
+ for (UINTN i = 0; i < x_max; i++) {
+ clearline[i] = ' ';
+ separator[i] = unicode_supported() ? L'─' : L'-';
+ }
+ clearline[x_max] = 0;
+ separator[x_max] = 0;
+
+ new_mode = false;
+ clear = true;
+ }
+
+ if (clear) {
+ clear_screen(COLOR_NORMAL);
+ clear = false;
+ refresh = true;
+ }
+
+ if (refresh) {
+ for (UINTN i = idx_first; i <= idx_last && i < config->entry_count; i++) {
+ print_at(x_start, y_start + i - idx_first,
+ i == idx_highlight ? COLOR_HIGHLIGHT : COLOR_ENTRY,
+ lines[i]);
+ if (i == config->idx_default_efivar)
+ print_at(x_start,
+ y_start + i - idx_first,
+ i == idx_highlight ? COLOR_HIGHLIGHT : COLOR_ENTRY,
+ unicode_supported() ? L" ►" : L"=>");
+ }
+ refresh = false;
+ } else if (highlight) {
+ print_at(x_start, y_start + idx_highlight_prev - idx_first, COLOR_ENTRY, lines[idx_highlight_prev]);
+ print_at(x_start, y_start + idx_highlight - idx_first, COLOR_HIGHLIGHT, lines[idx_highlight]);
+ if (idx_highlight_prev == config->idx_default_efivar)
+ print_at(x_start,
+ y_start + idx_highlight_prev - idx_first,
+ COLOR_ENTRY,
+ unicode_supported() ? L" ►" : L"=>");
+ if (idx_highlight == config->idx_default_efivar)
+ print_at(x_start,
+ y_start + idx_highlight - idx_first,
+ COLOR_HIGHLIGHT,
+ unicode_supported() ? L" ►" : L"=>");
+ highlight = false;
+ }
+
+ if (timeout_remain > 0) {
+ free(status);
+ status = xpool_print(L"Boot in %u s.", timeout_remain);
+ }
+
+ if (status) {
+ /* If we draw the last char of the last line, the screen will scroll and break our
+ * input. Therefore, draw one less character then we could for the status message.
+ * Note that the same does not apply for the separator line as it will never be drawn
+ * on the last line. */
+ UINTN len = strnlen16(status, x_max - 1);
+ UINTN x = (x_max - len) / 2;
+ status[len] = '\0';
+ print_at(0, y_status, COLOR_NORMAL, clearline + x_max - x);
+ ST->ConOut->OutputString(ST->ConOut, status);
+ ST->ConOut->OutputString(ST->ConOut, clearline + 1 + x + len);
+
+ len = MIN(MAX(len, line_width) + 2 * entry_padding, x_max);
+ x = (x_max - len) / 2;
+ print_at(x, y_status - 1, COLOR_NORMAL, separator + x_max - len);
+ } else {
+ print_at(0, y_status - 1, COLOR_NORMAL, clearline);
+ print_at(0, y_status, COLOR_NORMAL, clearline + 1); /* See comment above. */
+ }
+
+ /* Beep several times so that the selected entry can be distinguished. */
+ if (config->beep)
+ beep(idx_highlight + 1);
+
+ err = console_key_read(&key, timeout_remain > 0 ? 1000 * 1000 : UINT64_MAX);
+ if (err == EFI_NOT_READY)
+ /* No input device returned a key, try again. This
+ * normally should not happen. */
+ continue;
+ if (err == EFI_TIMEOUT) {
+ assert(timeout_remain > 0);
+ timeout_remain--;
+ if (timeout_remain == 0) {
+ exit = true;
+ break;
+ }
+
+ /* update status */
+ continue;
+ }
+ if (err != EFI_SUCCESS) {
+ exit = true;
+ break;
+ }
+
+ timeout_remain = 0;
+
+ /* clear status after keystroke */
+ status = mfree(status);
+
+ idx_highlight_prev = idx_highlight;
+
+ if (firmware_setup) {
+ firmware_setup = false;
+ if (key == KEYPRESS(0, 0, CHAR_CARRIAGE_RETURN))
+ reboot_into_firmware();
+ continue;
+ }
+
+ switch (key) {
+ case KEYPRESS(0, SCAN_UP, 0):
+ case KEYPRESS(0, 0, 'k'):
+ case KEYPRESS(0, 0, 'K'):
+ if (idx_highlight > 0)
+ idx_highlight--;
+ break;
+
+ case KEYPRESS(0, SCAN_DOWN, 0):
+ case KEYPRESS(0, 0, 'j'):
+ case KEYPRESS(0, 0, 'J'):
+ if (idx_highlight < config->entry_count-1)
+ idx_highlight++;
+ break;
+
+ case KEYPRESS(0, SCAN_HOME, 0):
+ case KEYPRESS(EFI_ALT_PRESSED, 0, '<'):
+ if (idx_highlight > 0) {
+ refresh = true;
+ idx_highlight = 0;
+ }
+ break;
+
+ case KEYPRESS(0, SCAN_END, 0):
+ case KEYPRESS(EFI_ALT_PRESSED, 0, '>'):
+ if (idx_highlight < config->entry_count-1) {
+ refresh = true;
+ idx_highlight = config->entry_count-1;
+ }
+ break;
+
+ case KEYPRESS(0, SCAN_PAGE_UP, 0):
+ if (idx_highlight > visible_max)
+ idx_highlight -= visible_max;
+ else
+ idx_highlight = 0;
+ break;
+
+ case KEYPRESS(0, SCAN_PAGE_DOWN, 0):
+ idx_highlight += visible_max;
+ if (idx_highlight > config->entry_count-1)
+ idx_highlight = config->entry_count-1;
+ break;
+
+ case KEYPRESS(0, 0, CHAR_LINEFEED):
+ case KEYPRESS(0, 0, CHAR_CARRIAGE_RETURN):
+ case KEYPRESS(0, SCAN_F3, 0): /* EZpad Mini 4s firmware sends malformed events */
+ case KEYPRESS(0, SCAN_F3, CHAR_CARRIAGE_RETURN): /* Teclast X98+ II firmware sends malformed events */
+ case KEYPRESS(0, SCAN_RIGHT, 0):
+ exit = true;
+ break;
+
+ case KEYPRESS(0, SCAN_F1, 0):
+ case KEYPRESS(0, 0, 'h'):
+ case KEYPRESS(0, 0, 'H'):
+ case KEYPRESS(0, 0, '?'):
+ /* This must stay below 80 characters! Q/v/Ctrl+l/f deliberately not advertised. */
+ status = xstrdup16(u"(d)efault (t/T)timeout (e)dit (r/R)resolution (p)rint (h)elp");
+ break;
+
+ case KEYPRESS(0, 0, 'Q'):
+ exit = true;
+ run = false;
+ break;
+
+ case KEYPRESS(0, 0, 'd'):
+ case KEYPRESS(0, 0, 'D'):
+ if (config->idx_default_efivar != idx_highlight) {
+ free(config->entry_default_efivar);
+ config->entry_default_efivar = xstrdup16(config->entries[idx_highlight]->id);
+ config->idx_default_efivar = idx_highlight;
+ status = xstrdup16(u"Default boot entry selected.");
+ } else {
+ config->entry_default_efivar = mfree(config->entry_default_efivar);
+ config->idx_default_efivar = IDX_INVALID;
+ status = xstrdup16(u"Default boot entry cleared.");
+ }
+ config->use_saved_entry_efivar = false;
+ refresh = true;
+ break;
+
+ case KEYPRESS(0, 0, '-'):
+ case KEYPRESS(0, 0, 'T'):
+ status = update_timeout_efivar(&config->timeout_sec_efivar, false);
+ break;
+
+ case KEYPRESS(0, 0, '+'):
+ case KEYPRESS(0, 0, 't'):
+ status = update_timeout_efivar(&config->timeout_sec_efivar, true);
+ break;
+
+ case KEYPRESS(0, 0, 'e'):
+ case KEYPRESS(0, 0, 'E'):
+ /* only the options of configured entries can be edited */
+ if (!config->editor ||
+ !IN_SET(config->entries[idx_highlight]->type, LOADER_EFI, LOADER_LINUX, LOADER_UNIFIED_LINUX)) {
+ status = xstrdup16(u"Entry does not support editing the command line.");
+ break;
+ }
+
+ /* Unified kernels that are signed as a whole will not accept command line options
+ * when secure boot is enabled unless there is none embedded in the image. Do not try
+ * to pretend we can edit it to only have it be ignored. */
+ if (config->entries[idx_highlight]->type == LOADER_UNIFIED_LINUX &&
+ secure_boot_enabled() &&
+ config->entries[idx_highlight]->options) {
+ status = xstrdup16(u"Entry not editable in SecureBoot mode.");
+ break;
+ }
+
+ /* The edit line may end up on the last line of the screen. And even though we're
+ * not telling the firmware to advance the line, it still does in this one case,
+ * causing a scroll to happen that screws with our beautiful boot loader output.
+ * Since we cannot paint the last character of the edit line, we simply start
+ * at x-offset 1 for symmetry. */
+ print_at(1, y_status, COLOR_EDIT, clearline + 2);
+ exit = line_edit(&config->entries[idx_highlight]->options, x_max - 2, y_status);
+ print_at(1, y_status, COLOR_NORMAL, clearline + 2);
+ break;
+
+ case KEYPRESS(0, 0, 'v'):
+ status = xpool_print(
+ L"systemd-boot " GIT_VERSION L" (" EFI_MACHINE_TYPE_NAME L"), "
+ L"UEFI Specification %u.%02u, Vendor %s %u.%02u",
+ ST->Hdr.Revision >> 16,
+ ST->Hdr.Revision & 0xffff,
+ ST->FirmwareVendor,
+ ST->FirmwareRevision >> 16,
+ ST->FirmwareRevision & 0xffff);
+ break;
+
+ case KEYPRESS(0, 0, 'p'):
+ case KEYPRESS(0, 0, 'P'):
+ print_status(config, loaded_image_path);
+ clear = true;
+ break;
+
+ case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'l'):
+ case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('l')):
+ clear = true;
+ break;
+
+ case KEYPRESS(0, 0, 'r'):
+ err = console_set_mode(CONSOLE_MODE_NEXT);
+ if (err != EFI_SUCCESS)
+ status = xpool_print(L"Error changing console mode: %r", err);
+ else {
+ config->console_mode_efivar = ST->ConOut->Mode->Mode;
+ status = xpool_print(L"Console mode changed to %ld.", config->console_mode_efivar);
+ }
+ new_mode = true;
+ break;
+
+ case KEYPRESS(0, 0, 'R'):
+ config->console_mode_efivar = CONSOLE_MODE_KEEP;
+ err = console_set_mode(config->console_mode == CONSOLE_MODE_KEEP ?
+ console_mode_initial : config->console_mode);
+ if (err != EFI_SUCCESS)
+ status = xpool_print(L"Error resetting console mode: %r", err);
+ else
+ status = xpool_print(L"Console mode reset to %s default.",
+ config->console_mode == CONSOLE_MODE_KEEP ? L"firmware" : L"configuration file");
+ new_mode = true;
+ break;
+
+ case KEYPRESS(0, 0, 'f'):
+ case KEYPRESS(0, 0, 'F'):
+ case KEYPRESS(0, SCAN_F2, 0): /* Most vendors. */
+ case KEYPRESS(0, SCAN_F10, 0): /* HP and Lenovo. */
+ case KEYPRESS(0, SCAN_DELETE, 0): /* Same as F2. */
+ case KEYPRESS(0, SCAN_ESC, 0): /* HP. */
+ if (FLAGS_SET(get_os_indications_supported(), EFI_OS_INDICATIONS_BOOT_TO_FW_UI)) {
+ firmware_setup = true;
+ /* Let's make sure the user really wants to do this. */
+ status = xpool_print(L"Press Enter to reboot into firmware interface.");
+ } else
+ status = xpool_print(L"Reboot into firmware interface not supported.");
+ break;
+
+ default:
+ /* jump with a hotkey directly to a matching entry */
+ idx = entry_lookup_key(config, idx_highlight+1, KEYCHAR(key));
+ if (idx == IDX_INVALID)
+ break;
+ idx_highlight = idx;
+ refresh = true;
+ }
+
+ if (idx_highlight > idx_last) {
+ idx_last = idx_highlight;
+ idx_first = 1 + idx_highlight - visible_max;
+ refresh = true;
+ } else if (idx_highlight < idx_first) {
+ idx_first = idx_highlight;
+ idx_last = idx_highlight + visible_max-1;
+ refresh = true;
+ }
+
+ if (!refresh && idx_highlight != idx_highlight_prev)
+ highlight = true;
+ }
+
+ *chosen_entry = config->entries[idx_highlight];
+
+ /* Update EFI vars after we left the menu to reduce NVRAM writes. */
+
+ if (default_efivar_saved != config->idx_default_efivar)
+ efivar_set(LOADER_GUID, L"LoaderEntryDefault", config->entry_default_efivar, EFI_VARIABLE_NON_VOLATILE);
+
+ if (console_mode_efivar_saved != config->console_mode_efivar) {
+ if (config->console_mode_efivar == CONSOLE_MODE_KEEP)
+ efivar_set(LOADER_GUID, L"LoaderConfigConsoleMode", NULL, EFI_VARIABLE_NON_VOLATILE);
+ else
+ efivar_set_uint_string(LOADER_GUID, L"LoaderConfigConsoleMode",
+ config->console_mode_efivar, EFI_VARIABLE_NON_VOLATILE);
+ }
+
+ if (timeout_efivar_saved != config->timeout_sec_efivar) {
+ switch (config->timeout_sec_efivar) {
+ case TIMEOUT_UNSET:
+ efivar_set(LOADER_GUID, L"LoaderConfigTimeout", NULL, EFI_VARIABLE_NON_VOLATILE);
+ break;
+ case TIMEOUT_MENU_FORCE:
+ efivar_set(LOADER_GUID, u"LoaderConfigTimeout", u"menu-force", EFI_VARIABLE_NON_VOLATILE);
+ break;
+ case TIMEOUT_MENU_HIDDEN:
+ efivar_set(LOADER_GUID, u"LoaderConfigTimeout", u"menu-hidden", EFI_VARIABLE_NON_VOLATILE);
+ break;
+ default:
+ efivar_set_uint_string(LOADER_GUID, L"LoaderConfigTimeout",
+ config->timeout_sec_efivar, EFI_VARIABLE_NON_VOLATILE);
+ }
+ }
+
+ clear_screen(COLOR_NORMAL);
+ return run;
+}
+
+static void config_add_entry(Config *config, ConfigEntry *entry) {
+ assert(config);
+ assert(entry);
+
+ /* This is just for paranoia. */
+ assert(config->entry_count < IDX_MAX);
+
+ if ((config->entry_count & 15) == 0) {
+ config->entries = xrealloc(
+ config->entries,
+ sizeof(void *) * config->entry_count,
+ sizeof(void *) * (config->entry_count + 16));
+ }
+ config->entries[config->entry_count++] = entry;
+}
+
+static void config_entry_free(ConfigEntry *entry) {
+ if (!entry)
+ return;
+
+ free(entry->id);
+ free(entry->title_show);
+ free(entry->title);
+ free(entry->sort_key);
+ free(entry->version);
+ free(entry->machine_id);
+ free(entry->loader);
+ free(entry->devicetree);
+ free(entry->options);
+ strv_free(entry->initrd);
+ free(entry->path);
+ free(entry->current_name);
+ free(entry->next_name);
+ free(entry);
+}
+
+static inline void config_entry_freep(ConfigEntry **entry) {
+ config_entry_free(*entry);
+}
+
+static char *line_get_key_value(
+ char *content,
+ const char *sep,
+ UINTN *pos,
+ char **key_ret,
+ char **value_ret) {
+
+ char *line, *value;
+ UINTN linelen;
+
+ assert(content);
+ assert(sep);
+ assert(pos);
+ assert(key_ret);
+ assert(value_ret);
+
+ for (;;) {
+ line = content + *pos;
+ if (*line == '\0')
+ return NULL;
+
+ linelen = 0;
+ while (line[linelen] && !strchr8("\n\r", line[linelen]))
+ linelen++;
+
+ /* move pos to next line */
+ *pos += linelen;
+ if (content[*pos])
+ (*pos)++;
+
+ /* empty line */
+ if (linelen == 0)
+ continue;
+
+ /* terminate line */
+ line[linelen] = '\0';
+
+ /* remove leading whitespace */
+ while (linelen > 0 && strchr8(" \t", *line)) {
+ line++;
+ linelen--;
+ }
+
+ /* remove trailing whitespace */
+ while (linelen > 0 && strchr8(" \t", line[linelen - 1]))
+ linelen--;
+ line[linelen] = '\0';
+
+ if (*line == '#')
+ continue;
+
+ /* split key/value */
+ value = line;
+ while (*value && !strchr8(sep, *value))
+ value++;
+ if (*value == '\0')
+ continue;
+ *value = '\0';
+ value++;
+ while (*value && strchr8(sep, *value))
+ value++;
+
+ /* unquote */
+ if (value[0] == '"' && line[linelen - 1] == '"') {
+ value++;
+ line[linelen - 1] = '\0';
+ }
+
+ *key_ret = line;
+ *value_ret = value;
+ return line;
+ }
+}
+
+static void config_defaults_load_from_file(Config *config, char *content) {
+ char *line;
+ UINTN pos = 0;
+ char *key, *value;
+ EFI_STATUS err;
+
+ assert(config);
+ assert(content);
+
+ while ((line = line_get_key_value(content, " \t", &pos, &key, &value))) {
+ if (streq8(key, "timeout")) {
+ if (streq8( value, "menu-force"))
+ config->timeout_sec_config = TIMEOUT_MENU_FORCE;
+ else if (streq8(value, "menu-hidden"))
+ config->timeout_sec_config = TIMEOUT_MENU_HIDDEN;
+ else {
+ uint64_t u;
+ if (!parse_number8(value, &u, NULL) || u > TIMEOUT_TYPE_MAX) {
+ log_error_stall(L"Error parsing 'timeout' config option: %a", value);
+ continue;
+ }
+ config->timeout_sec_config = u;
+ }
+ config->timeout_sec = config->timeout_sec_config;
+ continue;
+ }
+
+ if (streq8(key, "default")) {
+ if (value[0] == '@' && !strcaseeq8(value, "@saved")) {
+ log_error_stall(L"Unsupported special entry identifier: %a", value);
+ continue;
+ }
+ free(config->entry_default_config);
+ config->entry_default_config = xstr8_to_16(value);
+ continue;
+ }
+
+ if (streq8(key, "editor")) {
+ err = parse_boolean(value, &config->editor);
+ if (err != EFI_SUCCESS)
+ log_error_stall(L"Error parsing 'editor' config option: %a", value);
+ continue;
+ }
+
+ if (streq8(key, "auto-entries")) {
+ err = parse_boolean(value, &config->auto_entries);
+ if (err != EFI_SUCCESS)
+ log_error_stall(L"Error parsing 'auto-entries' config option: %a", value);
+ continue;
+ }
+
+ if (streq8(key, "auto-firmware")) {
+ err = parse_boolean(value, &config->auto_firmware);
+ if (err != EFI_SUCCESS)
+ log_error_stall(L"Error parsing 'auto-firmware' config option: %a", value);
+ continue;
+ }
+
+ if (streq8(key, "beep")) {
+ err = parse_boolean(value, &config->beep);
+ if (err != EFI_SUCCESS)
+ log_error_stall(L"Error parsing 'beep' config option: %a", value);
+ continue;
+ }
+
+ if (streq8(key, "reboot-for-bitlocker")) {
+ err = parse_boolean(value, &config->reboot_for_bitlocker);
+ if (err != EFI_SUCCESS)
+ log_error_stall(L"Error parsing 'reboot-for-bitlocker' config option: %a", value);
+ }
+
+ if (streq8(key, "secure-boot-enroll")) {
+ if (streq8(value, "manual"))
+ config->secure_boot_enroll = ENROLL_MANUAL;
+ else if (streq8(value, "force"))
+ config->secure_boot_enroll = ENROLL_FORCE;
+ else if (streq8(value, "off"))
+ config->secure_boot_enroll = ENROLL_OFF;
+ else
+ log_error_stall(L"Error parsing 'secure-boot-enroll' config option: %a", value);
+ continue;
+ }
+
+ if (streq8(key, "console-mode")) {
+ if (streq8(value, "auto"))
+ config->console_mode = CONSOLE_MODE_AUTO;
+ else if (streq8(value, "max"))
+ config->console_mode = CONSOLE_MODE_FIRMWARE_MAX;
+ else if (streq8(value, "keep"))
+ config->console_mode = CONSOLE_MODE_KEEP;
+ else {
+ uint64_t u;
+ if (!parse_number8(value, &u, NULL) || u > CONSOLE_MODE_RANGE_MAX) {
+ log_error_stall(L"Error parsing 'console-mode' config option: %a", value);
+ continue;
+ }
+ config->console_mode = u;
+ }
+ continue;
+ }
+
+ if (streq8(key, "random-seed-mode")) {
+ if (streq8(value, "off"))
+ config->random_seed_mode = RANDOM_SEED_OFF;
+ else if (streq8(value, "with-system-token"))
+ config->random_seed_mode = RANDOM_SEED_WITH_SYSTEM_TOKEN;
+ else if (streq8(value, "always"))
+ config->random_seed_mode = RANDOM_SEED_ALWAYS;
+ else {
+ bool on;
+
+ err = parse_boolean(value, &on);
+ if (err != EFI_SUCCESS) {
+ log_error_stall(L"Error parsing 'random-seed-mode' config option: %a", value);
+ continue;
+ }
+
+ config->random_seed_mode = on ? RANDOM_SEED_ALWAYS : RANDOM_SEED_OFF;
+ }
+ continue;
+ }
+ }
+}
+
+static void config_entry_parse_tries(
+ ConfigEntry *entry,
+ const char16_t *path,
+ const char16_t *file,
+ const char16_t *suffix) {
+
+ assert(entry);
+ assert(path);
+ assert(file);
+ assert(suffix);
+
+ /*
+ * Parses a suffix of two counters (one going down, one going up) in the form "+LEFT-DONE" from the end of the
+ * filename (but before the .efi/.conf suffix), where the "-DONE" part is optional and may be left out (in
+ * which case that counter as assumed to be zero, i.e. the missing part is synonymous to "-0").
+ *
+ * Names we grok, and the series they result in:
+ *
+ * foobar+3.efi → foobar+2-1.efi → foobar+1-2.efi → foobar+0-3.efi → STOP!
+ * foobar+4-0.efi → foobar+3-1.efi → foobar+2-2.efi → foobar+1-3.efi → foobar+0-4.efi → STOP!
+ */
+
+ const char16_t *counter = NULL;
+ for (;;) {
+ char16_t *plus = strchr16(counter ?: file, '+');
+ if (plus) {
+ /* We want the last "+". */
+ counter = plus + 1;
+ continue;
+ }
+ if (counter)
+ break;
+
+ /* No boot counter found. */
+ return;
+ }
+
+ uint64_t tries_left, tries_done = 0;
+ size_t prefix_len = counter - file;
+
+ if (!parse_number16(counter, &tries_left, &counter) || tries_left > INT_MAX)
+ return;
+
+ /* Parse done counter only if present. */
+ if (*counter == '-' && (!parse_number16(counter + 1, &tries_done, &counter) || tries_done > INT_MAX))
+ return;
+
+ /* Boot counter in the middle of the name? */
+ if (!streq16(counter, suffix))
+ return;
+
+ entry->tries_left = tries_left;
+ entry->tries_done = tries_done;
+ entry->path = xstrdup16(path);
+ entry->current_name = xstrdup16(file);
+ entry->next_name = xpool_print(
+ L"%.*s%u-%u%s",
+ prefix_len,
+ file,
+ LESS_BY(tries_left, 1u),
+ MIN(tries_done + 1, (uint64_t) INT_MAX),
+ suffix);
+}
+
+static EFI_STATUS config_entry_bump_counters(ConfigEntry *entry) {
+ _cleanup_free_ char16_t* old_path = NULL, *new_path = NULL;
+ _cleanup_(file_closep) EFI_FILE *handle = NULL;
+ _cleanup_free_ EFI_FILE_INFO *file_info = NULL;
+ UINTN file_info_size;
+ EFI_STATUS err;
+
+ assert(entry);
+
+ if (entry->tries_left < 0)
+ return EFI_SUCCESS;
+
+ if (!entry->path || !entry->current_name || !entry->next_name)
+ return EFI_SUCCESS;
+
+ _cleanup_(file_closep) EFI_FILE *root = NULL;
+ err = open_volume(entry->device, &root);
+ if (err != EFI_SUCCESS) {
+ log_error_stall(L"Error opening entry root path: %r", err);
+ return err;
+ }
+
+ old_path = xpool_print(L"%s\\%s", entry->path, entry->current_name);
+
+ err = root->Open(root, &handle, old_path, EFI_FILE_MODE_READ|EFI_FILE_MODE_WRITE, 0ULL);
+ if (err != EFI_SUCCESS) {
+ log_error_stall(L"Error opening boot entry: %r", err);
+ return err;
+ }
+
+ err = get_file_info_harder(handle, &file_info, &file_info_size);
+ if (err != EFI_SUCCESS) {
+ log_error_stall(L"Error getting boot entry file info: %r", err);
+ return err;
+ }
+
+ /* And rename the file */
+ strcpy16(file_info->FileName, entry->next_name);
+ err = handle->SetInfo(handle, &GenericFileInfo, file_info_size, file_info);
+ if (err != EFI_SUCCESS) {
+ log_error_stall(L"Failed to rename '%s' to '%s', ignoring: %r", old_path, entry->next_name, err);
+ return err;
+ }
+
+ /* Flush everything to disk, just in case… */
+ err = handle->Flush(handle);
+ if (err != EFI_SUCCESS) {
+ log_error_stall(L"Error flushing boot entry file info: %r", err);
+ return err;
+ }
+
+ /* Let's tell the OS that we renamed this file, so that it knows what to rename to the counter-less name on
+ * success */
+ new_path = xpool_print(L"%s\\%s", entry->path, entry->next_name);
+ efivar_set(LOADER_GUID, L"LoaderBootCountPath", new_path, 0);
+
+ /* If the file we just renamed is the loader path, then let's update that. */
+ if (streq16(entry->loader, old_path)) {
+ free(entry->loader);
+ entry->loader = TAKE_PTR(new_path);
+ }
+
+ return EFI_SUCCESS;
+}
+
+static void config_entry_add_type1(
+ Config *config,
+ EFI_HANDLE *device,
+ EFI_FILE *root_dir,
+ const char16_t *path,
+ const char16_t *file,
+ char *content,
+ const char16_t *loaded_image_path) {
+
+ _cleanup_(config_entry_freep) ConfigEntry *entry = NULL;
+ char *line;
+ UINTN pos = 0, n_initrd = 0;
+ char *key, *value;
+ EFI_STATUS err;
+
+ assert(config);
+ assert(device);
+ assert(root_dir);
+ assert(path);
+ assert(file);
+ assert(content);
+
+ entry = xnew(ConfigEntry, 1);
+ *entry = (ConfigEntry) {
+ .tries_done = -1,
+ .tries_left = -1,
+ };
+
+ while ((line = line_get_key_value(content, " \t", &pos, &key, &value))) {
+ if (streq8(key, "title")) {
+ free(entry->title);
+ entry->title = xstr8_to_16(value);
+ continue;
+ }
+
+ if (streq8(key, "sort-key")) {
+ free(entry->sort_key);
+ entry->sort_key = xstr8_to_16(value);
+ continue;
+ }
+
+ if (streq8(key, "version")) {
+ free(entry->version);
+ entry->version = xstr8_to_16(value);
+ continue;
+ }
+
+ if (streq8(key, "machine-id")) {
+ free(entry->machine_id);
+ entry->machine_id = xstr8_to_16(value);
+ continue;
+ }
+
+ if (streq8(key, "linux")) {
+ free(entry->loader);
+ entry->type = LOADER_LINUX;
+ entry->loader = xstr8_to_path(value);
+ entry->key = 'l';
+ continue;
+ }
+
+ if (streq8(key, "efi")) {
+ entry->type = LOADER_EFI;
+ free(entry->loader);
+ entry->loader = xstr8_to_path(value);
+
+ /* do not add an entry for ourselves */
+ if (strcaseeq16(entry->loader, loaded_image_path)) {
+ entry->type = LOADER_UNDEFINED;
+ break;
+ }
+ continue;
+ }
+
+ if (streq8(key, "architecture")) {
+ /* do not add an entry for an EFI image of architecture not matching with that of the image */
+ if (!streq8(value, EFI_MACHINE_TYPE_NAME)) {
+ entry->type = LOADER_UNDEFINED;
+ break;
+ }
+ continue;
+ }
+
+ if (streq8(key, "devicetree")) {
+ free(entry->devicetree);
+ entry->devicetree = xstr8_to_path(value);
+ continue;
+ }
+
+ if (streq8(key, "initrd")) {
+ entry->initrd = xrealloc(
+ entry->initrd,
+ n_initrd == 0 ? 0 : (n_initrd + 1) * sizeof(uint16_t *),
+ (n_initrd + 2) * sizeof(uint16_t *));
+ entry->initrd[n_initrd++] = xstr8_to_path(value);
+ entry->initrd[n_initrd] = NULL;
+ continue;
+ }
+
+ if (streq8(key, "options")) {
+ _cleanup_free_ char16_t *new = NULL;
+
+ new = xstr8_to_16(value);
+ if (entry->options) {
+ char16_t *s = xpool_print(L"%s %s", entry->options, new);
+ free(entry->options);
+ entry->options = s;
+ } else
+ entry->options = TAKE_PTR(new);
+
+ continue;
+ }
+ }
+
+ if (entry->type == LOADER_UNDEFINED)
+ return;
+
+ /* check existence */
+ _cleanup_(file_closep) EFI_FILE *handle = NULL;
+ err = root_dir->Open(root_dir, &handle, entry->loader, EFI_FILE_MODE_READ, 0ULL);
+ if (err != EFI_SUCCESS)
+ return;
+
+ entry->device = device;
+ entry->id = xstrdup16(file);
+ strtolower16(entry->id);
+
+ config_add_entry(config, entry);
+
+ config_entry_parse_tries(entry, path, file, L".conf");
+ TAKE_PTR(entry);
+}
+
+static EFI_STATUS efivar_get_timeout(const char16_t *var, uint32_t *ret_value) {
+ _cleanup_free_ char16_t *value = NULL;
+ EFI_STATUS err;
+
+ assert(var);
+ assert(ret_value);
+
+ err = efivar_get(LOADER_GUID, var, &value);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ if (streq16(value, u"menu-force")) {
+ *ret_value = TIMEOUT_MENU_FORCE;
+ return EFI_SUCCESS;
+ }
+ if (streq16(value, u"menu-hidden")) {
+ *ret_value = TIMEOUT_MENU_HIDDEN;
+ return EFI_SUCCESS;
+ }
+
+ uint64_t timeout;
+ if (!parse_number16(value, &timeout, NULL))
+ return EFI_INVALID_PARAMETER;
+
+ *ret_value = MIN(timeout, TIMEOUT_TYPE_MAX);
+ return EFI_SUCCESS;
+}
+
+static void config_load_defaults(Config *config, EFI_FILE *root_dir) {
+ _cleanup_free_ char *content = NULL;
+ UINTN value = 0; /* avoid false maybe-uninitialized warning */
+ EFI_STATUS err;
+
+ assert(root_dir);
+
+ *config = (Config) {
+ .editor = true,
+ .auto_entries = true,
+ .auto_firmware = true,
+ .reboot_for_bitlocker = false,
+ .secure_boot_enroll = ENROLL_MANUAL,
+ .random_seed_mode = RANDOM_SEED_WITH_SYSTEM_TOKEN,
+ .idx_default_efivar = IDX_INVALID,
+ .console_mode = CONSOLE_MODE_KEEP,
+ .console_mode_efivar = CONSOLE_MODE_KEEP,
+ .timeout_sec_config = TIMEOUT_UNSET,
+ .timeout_sec_efivar = TIMEOUT_UNSET,
+ };
+
+ err = file_read(root_dir, L"\\loader\\loader.conf", 0, 0, &content, NULL);
+ if (err == EFI_SUCCESS)
+ config_defaults_load_from_file(config, content);
+
+ err = efivar_get_timeout(u"LoaderConfigTimeout", &config->timeout_sec_efivar);
+ if (err == EFI_SUCCESS)
+ config->timeout_sec = config->timeout_sec_efivar;
+ else if (err != EFI_NOT_FOUND)
+ log_error_stall(u"Error reading LoaderConfigTimeout EFI variable: %r", err);
+
+ err = efivar_get_timeout(u"LoaderConfigTimeoutOneShot", &config->timeout_sec);
+ if (err == EFI_SUCCESS) {
+ /* Unset variable now, after all it's "one shot". */
+ (void) efivar_set(LOADER_GUID, L"LoaderConfigTimeoutOneShot", NULL, EFI_VARIABLE_NON_VOLATILE);
+
+ config->force_menu = true; /* force the menu when this is set */
+ } else if (err != EFI_NOT_FOUND)
+ log_error_stall(u"Error reading LoaderConfigTimeoutOneShot EFI variable: %r", err);
+
+ err = efivar_get_uint_string(LOADER_GUID, L"LoaderConfigConsoleMode", &value);
+ if (err == EFI_SUCCESS)
+ config->console_mode_efivar = value;
+
+ err = efivar_get(LOADER_GUID, L"LoaderEntryOneShot", &config->entry_oneshot);
+ if (err == EFI_SUCCESS)
+ /* Unset variable now, after all it's "one shot". */
+ (void) efivar_set(LOADER_GUID, L"LoaderEntryOneShot", NULL, EFI_VARIABLE_NON_VOLATILE);
+
+ (void) efivar_get(LOADER_GUID, L"LoaderEntryDefault", &config->entry_default_efivar);
+
+ strtolower16(config->entry_default_config);
+ strtolower16(config->entry_default_efivar);
+ strtolower16(config->entry_oneshot);
+ strtolower16(config->entry_saved);
+
+ config->use_saved_entry = streq16(config->entry_default_config, L"@saved");
+ config->use_saved_entry_efivar = streq16(config->entry_default_efivar, L"@saved");
+ if (config->use_saved_entry || config->use_saved_entry_efivar)
+ (void) efivar_get(LOADER_GUID, L"LoaderEntryLastBooted", &config->entry_saved);
+}
+
+static void config_load_entries(
+ Config *config,
+ EFI_HANDLE *device,
+ EFI_FILE *root_dir,
+ const char16_t *loaded_image_path) {
+
+ _cleanup_(file_closep) EFI_FILE *entries_dir = NULL;
+ _cleanup_free_ EFI_FILE_INFO *f = NULL;
+ UINTN f_size = 0;
+ EFI_STATUS err;
+
+ assert(config);
+ assert(device);
+ assert(root_dir);
+
+ /* Adds Boot Loader Type #1 entries (i.e. /loader/entries/….conf) */
+
+ err = open_directory(root_dir, L"\\loader\\entries", &entries_dir);
+ if (err != EFI_SUCCESS)
+ return;
+
+ for (;;) {
+ _cleanup_free_ char *content = NULL;
+
+ err = readdir_harder(entries_dir, &f, &f_size);
+ if (err != EFI_SUCCESS || !f)
+ break;
+
+ if (f->FileName[0] == '.')
+ continue;
+ if (FLAGS_SET(f->Attribute, EFI_FILE_DIRECTORY))
+ continue;
+
+ if (!endswith_no_case(f->FileName, L".conf"))
+ continue;
+ if (startswith(f->FileName, L"auto-"))
+ continue;
+
+ err = file_read(entries_dir, f->FileName, 0, 0, &content, NULL);
+ if (err == EFI_SUCCESS)
+ config_entry_add_type1(config, device, root_dir, L"\\loader\\entries", f->FileName, content, loaded_image_path);
+ }
+}
+
+static int config_entry_compare(const ConfigEntry *a, const ConfigEntry *b) {
+ int r;
+
+ assert(a);
+ assert(b);
+
+ /* Order entries that have no tries left to the end of the list */
+ r = CMP(a->tries_left == 0, b->tries_left == 0);
+ if (r != 0)
+ return r;
+
+ /* If there's a sort key defined for *both* entries, then we do new-style ordering, i.e. by
+ * sort-key/machine-id/version, with a final fallback to id. If there's no sort key for either, we do
+ * old-style ordering, i.e. by id only. If one has sort key and the other does not, we put new-style
+ * before old-style. */
+ r = CMP(!a->sort_key, !b->sort_key);
+ if (r != 0) /* one is old-style, one new-style */
+ return r;
+
+ if (a->sort_key && b->sort_key) {
+ r = strcmp16(a->sort_key, b->sort_key);
+ if (r != 0)
+ return r;
+
+ /* If multiple installations of the same OS are around, group by machine ID */
+ r = strcmp16(a->machine_id, b->machine_id);
+ if (r != 0)
+ return r;
+
+ /* If the sort key was defined, then order by version now (downwards, putting the newest first) */
+ r = -strverscmp_improved(a->version, b->version);
+ if (r != 0)
+ return r;
+ }
+
+ /* Now order by ID. The version is likely part of the ID, thus note that this will generatelly put
+ * the newer versions earlier. Specifying a sort key explicitly is preferable, because it gives an
+ * explicit sort order. */
+ r = -strverscmp_improved(a->id, b->id);
+ if (r != 0)
+ return r;
+
+ if (a->tries_left < 0 || b->tries_left < 0)
+ return 0;
+
+ /* If both items have boot counting, and otherwise are identical, put the entry with more tries left first */
+ r = -CMP(a->tries_left, b->tries_left);
+ if (r != 0)
+ return r;
+
+ /* If they have the same number of tries left, then let the one win which was tried fewer times so far */
+ return CMP(a->tries_done, b->tries_done);
+}
+
+static UINTN config_entry_find(Config *config, const char16_t *pattern) {
+ assert(config);
+
+ /* We expect pattern and entry IDs to be already case folded. */
+
+ if (!pattern)
+ return IDX_INVALID;
+
+ for (UINTN i = 0; i < config->entry_count; i++)
+ if (efi_fnmatch(pattern, config->entries[i]->id))
+ return i;
+
+ return IDX_INVALID;
+}
+
+static void config_default_entry_select(Config *config) {
+ UINTN i;
+
+ assert(config);
+
+ i = config_entry_find(config, config->entry_oneshot);
+ if (i != IDX_INVALID) {
+ config->idx_default = i;
+ return;
+ }
+
+ i = config_entry_find(config, config->use_saved_entry_efivar ? config->entry_saved : config->entry_default_efivar);
+ if (i != IDX_INVALID) {
+ config->idx_default = i;
+ config->idx_default_efivar = i;
+ return;
+ }
+
+ if (config->use_saved_entry)
+ /* No need to do the same thing twice. */
+ i = config->use_saved_entry_efivar ? IDX_INVALID : config_entry_find(config, config->entry_saved);
+ else
+ i = config_entry_find(config, config->entry_default_config);
+ if (i != IDX_INVALID) {
+ config->idx_default = i;
+ return;
+ }
+
+ /* select the first suitable entry */
+ for (i = 0; i < config->entry_count; i++) {
+ if (config->entries[i]->type == LOADER_AUTO || config->entries[i]->call)
+ continue;
+ config->idx_default = i;
+ return;
+ }
+
+ /* If no configured entry to select from was found, enable the menu. */
+ config->idx_default = 0;
+ if (config->timeout_sec == 0)
+ config->timeout_sec = 10;
+}
+
+static bool entries_unique(ConfigEntry **entries, bool *unique, UINTN entry_count) {
+ bool is_unique = true;
+
+ assert(entries);
+ assert(unique);
+
+ for (UINTN i = 0; i < entry_count; i++)
+ for (UINTN k = i + 1; k < entry_count; k++) {
+ if (!streq16(entries[i]->title_show, entries[k]->title_show))
+ continue;
+
+ is_unique = unique[i] = unique[k] = false;
+ }
+
+ return is_unique;
+}
+
+/* generate a unique title, avoiding non-distinguishable menu entries */
+static void config_title_generate(Config *config) {
+ assert(config);
+
+ bool unique[config->entry_count];
+
+ /* set title */
+ for (UINTN i = 0; i < config->entry_count; i++) {
+ assert(!config->entries[i]->title_show);
+ unique[i] = true;
+ config->entries[i]->title_show = xstrdup16(config->entries[i]->title ?: config->entries[i]->id);
+ }
+
+ if (entries_unique(config->entries, unique, config->entry_count))
+ return;
+
+ /* add version to non-unique titles */
+ for (UINTN i = 0; i < config->entry_count; i++) {
+ if (unique[i])
+ continue;
+
+ unique[i] = true;
+
+ if (!config->entries[i]->version)
+ continue;
+
+ _cleanup_free_ char16_t *t = config->entries[i]->title_show;
+ config->entries[i]->title_show = xpool_print(L"%s (%s)", t, config->entries[i]->version);
+ }
+
+ if (entries_unique(config->entries, unique, config->entry_count))
+ return;
+
+ /* add machine-id to non-unique titles */
+ for (UINTN i = 0; i < config->entry_count; i++) {
+ if (unique[i])
+ continue;
+
+ unique[i] = true;
+
+ if (!config->entries[i]->machine_id)
+ continue;
+
+ _cleanup_free_ char16_t *t = config->entries[i]->title_show;
+ config->entries[i]->title_show = xpool_print(
+ L"%s (%.*s)",
+ t,
+ strnlen16(config->entries[i]->machine_id, 8),
+ config->entries[i]->machine_id);
+ }
+
+ if (entries_unique(config->entries, unique, config->entry_count))
+ return;
+
+ /* add file name to non-unique titles */
+ for (UINTN i = 0; i < config->entry_count; i++) {
+ if (unique[i])
+ continue;
+
+ _cleanup_free_ char16_t *t = config->entries[i]->title_show;
+ config->entries[i]->title_show = xpool_print(L"%s (%s)", t, config->entries[i]->id);
+ }
+}
+
+static bool is_sd_boot(EFI_FILE *root_dir, const char16_t *loader_path) {
+ EFI_STATUS err;
+ static const char * const sections[] = {
+ ".sdmagic",
+ NULL
+ };
+ UINTN offset = 0, size = 0, read;
+ _cleanup_free_ char *content = NULL;
+
+ assert(root_dir);
+ assert(loader_path);
+
+ err = pe_file_locate_sections(root_dir, loader_path, sections, &offset, &size);
+ if (err != EFI_SUCCESS || size != sizeof(magic))
+ return false;
+
+ err = file_read(root_dir, loader_path, offset, size, &content, &read);
+ if (err != EFI_SUCCESS || size != read)
+ return false;
+
+ return memcmp(content, magic, sizeof(magic)) == 0;
+}
+
+static ConfigEntry *config_entry_add_loader_auto(
+ Config *config,
+ EFI_HANDLE *device,
+ EFI_FILE *root_dir,
+ const char16_t *loaded_image_path,
+ const char16_t *id,
+ char16_t key,
+ const char16_t *title,
+ const char16_t *loader) {
+
+ assert(config);
+ assert(device);
+ assert(root_dir);
+ assert(id);
+ assert(title);
+
+ if (!config->auto_entries)
+ return NULL;
+
+ if (!loader) {
+ loader = L"\\EFI\\BOOT\\BOOT" EFI_MACHINE_TYPE_NAME ".efi";
+
+ /* We are trying to add the default EFI loader here,
+ * but we do not want to do that if that would be us.
+ *
+ * If the default loader is not us, it might be shim. It would
+ * chainload GRUBX64.EFI in that case, which might be us.*/
+ if (strcaseeq16(loader, loaded_image_path) ||
+ is_sd_boot(root_dir, loader) ||
+ is_sd_boot(root_dir, L"\\EFI\\BOOT\\GRUB" EFI_MACHINE_TYPE_NAME L".EFI"))
+ return NULL;
+ }
+
+ /* check existence */
+ _cleanup_(file_closep) EFI_FILE *handle = NULL;
+ EFI_STATUS err = root_dir->Open(root_dir, &handle, (char16_t *) loader, EFI_FILE_MODE_READ, 0ULL);
+ if (err != EFI_SUCCESS)
+ return NULL;
+
+ ConfigEntry *entry = xnew(ConfigEntry, 1);
+ *entry = (ConfigEntry) {
+ .id = xstrdup16(id),
+ .type = LOADER_AUTO,
+ .title = xstrdup16(title),
+ .device = device,
+ .loader = xstrdup16(loader),
+ .key = key,
+ .tries_done = -1,
+ .tries_left = -1,
+ };
+
+ config_add_entry(config, entry);
+ return entry;
+}
+
+static void config_entry_add_osx(Config *config) {
+ EFI_STATUS err;
+ UINTN n_handles = 0;
+ _cleanup_free_ EFI_HANDLE *handles = NULL;
+
+ assert(config);
+
+ if (!config->auto_entries)
+ return;
+
+ err = BS->LocateHandleBuffer(ByProtocol, &FileSystemProtocol, NULL, &n_handles, &handles);
+ if (err != EFI_SUCCESS)
+ return;
+
+ for (UINTN i = 0; i < n_handles; i++) {
+ _cleanup_(file_closep) EFI_FILE *root = NULL;
+
+ if (open_volume(handles[i], &root) != EFI_SUCCESS)
+ continue;
+
+ if (config_entry_add_loader_auto(
+ config,
+ handles[i],
+ root,
+ NULL,
+ L"auto-osx",
+ 'a',
+ L"macOS",
+ L"\\System\\Library\\CoreServices\\boot.efi"))
+ break;
+ }
+}
+
+static EFI_STATUS boot_windows_bitlocker(void) {
+ _cleanup_free_ EFI_HANDLE *handles = NULL;
+ UINTN n_handles;
+ EFI_STATUS err;
+
+ // FIXME: Experimental for now. Should be generalized, and become a per-entry option that can be
+ // enabled independently of BitLocker, and without a BootXXXX entry pre-existing.
+
+ /* BitLocker key cannot be sealed without a TPM present. */
+ if (!tpm_present())
+ return EFI_NOT_FOUND;
+
+ err = BS->LocateHandleBuffer(ByProtocol, &BlockIoProtocol, NULL, &n_handles, &handles);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ /* Look for BitLocker magic string on all block drives. */
+ bool found = false;
+ for (UINTN i = 0; i < n_handles; i++) {
+ EFI_BLOCK_IO_PROTOCOL *block_io;
+ err = BS->HandleProtocol(handles[i], &BlockIoProtocol, (void **) &block_io);
+ if (err != EFI_SUCCESS || block_io->Media->BlockSize < 512 || block_io->Media->BlockSize > 4096)
+ continue;
+
+ char buf[4096];
+ err = block_io->ReadBlocks(block_io, block_io->Media->MediaId, 0, sizeof(buf), buf);
+ if (err != EFI_SUCCESS)
+ continue;
+
+ if (memcmp(buf + 3, "-FVE-FS-", STRLEN("-FVE-FS-")) == 0) {
+ found = true;
+ break;
+ }
+ }
+
+ /* If no BitLocker drive was found, we can just chainload bootmgfw.efi directly. */
+ if (!found)
+ return EFI_NOT_FOUND;
+
+ _cleanup_free_ uint16_t *boot_order = NULL;
+ UINTN boot_order_size;
+
+ /* There can be gaps in Boot#### entries. Instead of iterating over the full
+ * EFI var list or uint16_t namespace, just look for "Windows Boot Manager" in BootOrder. */
+ err = efivar_get_raw(EFI_GLOBAL_GUID, L"BootOrder", (char **) &boot_order, &boot_order_size);
+ if (err != EFI_SUCCESS || boot_order_size % sizeof(uint16_t) != 0)
+ return err;
+
+ for (UINTN i = 0; i < boot_order_size / sizeof(uint16_t); i++) {
+ _cleanup_free_ char *buf = NULL;
+ char16_t name[sizeof(L"Boot0000")];
+ UINTN buf_size;
+
+ SPrint(name, sizeof(name), L"Boot%04x", (uint32_t) boot_order[i]);
+ err = efivar_get_raw(EFI_GLOBAL_GUID, name, &buf, &buf_size);
+ if (err != EFI_SUCCESS)
+ continue;
+
+ /* Boot#### are EFI_LOAD_OPTION. But we really are only interested
+ * for the description, which is at this offset. */
+ UINTN offset = sizeof(uint32_t) + sizeof(uint16_t);
+ if (buf_size < offset + sizeof(char16_t))
+ continue;
+
+ if (streq16((char16_t *) (buf + offset), L"Windows Boot Manager")) {
+ err = efivar_set_raw(
+ EFI_GLOBAL_GUID,
+ L"BootNext",
+ boot_order + i,
+ sizeof(boot_order[i]),
+ EFI_VARIABLE_NON_VOLATILE);
+ if (err != EFI_SUCCESS)
+ return err;
+ RT->ResetSystem(EfiResetWarm, EFI_SUCCESS, 0, NULL);
+ assert_not_reached();
+ }
+ }
+
+ return EFI_NOT_FOUND;
+}
+
+static void config_entry_add_windows(Config *config, EFI_HANDLE *device, EFI_FILE *root_dir) {
+#if defined(__i386__) || defined(__x86_64__) || defined(__arm__) || defined(__aarch64__)
+ _cleanup_free_ char *bcd = NULL;
+ char16_t *title = NULL;
+ EFI_STATUS err;
+ UINTN len;
+
+ assert(config);
+ assert(device);
+ assert(root_dir);
+
+ if (!config->auto_entries)
+ return;
+
+ /* Try to find a better title. */
+ err = file_read(root_dir, L"\\EFI\\Microsoft\\Boot\\BCD", 0, 100*1024, &bcd, &len);
+ if (err == EFI_SUCCESS)
+ title = get_bcd_title((uint8_t *) bcd, len);
+
+ ConfigEntry *e = config_entry_add_loader_auto(config, device, root_dir, NULL,
+ L"auto-windows", 'w', title ?: L"Windows Boot Manager",
+ L"\\EFI\\Microsoft\\Boot\\bootmgfw.efi");
+
+ if (config->reboot_for_bitlocker)
+ e->call = boot_windows_bitlocker;
+#endif
+}
+
+static void config_entry_add_unified(
+ Config *config,
+ EFI_HANDLE *device,
+ EFI_FILE *root_dir) {
+
+ _cleanup_(file_closep) EFI_FILE *linux_dir = NULL;
+ _cleanup_free_ EFI_FILE_INFO *f = NULL;
+ UINTN f_size = 0;
+ EFI_STATUS err;
+
+ /* Adds Boot Loader Type #2 entries (i.e. /EFI/Linux/….efi) */
+
+ assert(config);
+ assert(device);
+ assert(root_dir);
+
+ err = open_directory(root_dir, L"\\EFI\\Linux", &linux_dir);
+ if (err != EFI_SUCCESS)
+ return;
+
+ for (;;) {
+ enum {
+ SECTION_CMDLINE,
+ SECTION_OSREL,
+ _SECTION_MAX,
+ };
+
+ static const char * const sections[_SECTION_MAX + 1] = {
+ [SECTION_CMDLINE] = ".cmdline",
+ [SECTION_OSREL] = ".osrel",
+ NULL,
+ };
+
+ _cleanup_free_ char16_t *os_pretty_name = NULL, *os_image_id = NULL, *os_name = NULL, *os_id = NULL,
+ *os_image_version = NULL, *os_version = NULL, *os_version_id = NULL, *os_build_id = NULL;
+ const char16_t *good_name, *good_version, *good_sort_key;
+ _cleanup_free_ char *content = NULL;
+ UINTN offs[_SECTION_MAX] = {};
+ UINTN szs[_SECTION_MAX] = {};
+ char *line;
+ UINTN pos = 0;
+ char *key, *value;
+
+ err = readdir_harder(linux_dir, &f, &f_size);
+ if (err != EFI_SUCCESS || !f)
+ break;
+
+ if (f->FileName[0] == '.')
+ continue;
+ if (FLAGS_SET(f->Attribute, EFI_FILE_DIRECTORY))
+ continue;
+ if (!endswith_no_case(f->FileName, L".efi"))
+ continue;
+ if (startswith(f->FileName, L"auto-"))
+ continue;
+
+ /* look for .osrel and .cmdline sections in the .efi binary */
+ err = pe_file_locate_sections(linux_dir, f->FileName, sections, offs, szs);
+ if (err != EFI_SUCCESS || szs[SECTION_OSREL] == 0)
+ continue;
+
+ err = file_read(linux_dir, f->FileName, offs[SECTION_OSREL], szs[SECTION_OSREL], &content, NULL);
+ if (err != EFI_SUCCESS)
+ continue;
+
+ /* read properties from the embedded os-release file */
+ while ((line = line_get_key_value(content, "=", &pos, &key, &value))) {
+ if (streq8(key, "PRETTY_NAME")) {
+ free(os_pretty_name);
+ os_pretty_name = xstr8_to_16(value);
+ continue;
+ }
+
+ if (streq8(key, "IMAGE_ID")) {
+ free(os_image_id);
+ os_image_id = xstr8_to_16(value);
+ continue;
+ }
+
+ if (streq8(key, "NAME")) {
+ free(os_name);
+ os_name = xstr8_to_16(value);
+ continue;
+ }
+
+ if (streq8(key, "ID")) {
+ free(os_id);
+ os_id = xstr8_to_16(value);
+ continue;
+ }
+
+ if (streq8(key, "IMAGE_VERSION")) {
+ free(os_image_version);
+ os_image_version = xstr8_to_16(value);
+ continue;
+ }
+
+ if (streq8(key, "VERSION")) {
+ free(os_version);
+ os_version = xstr8_to_16(value);
+ continue;
+ }
+
+ if (streq8(key, "VERSION_ID")) {
+ free(os_version_id);
+ os_version_id = xstr8_to_16(value);
+ continue;
+ }
+
+ if (streq8(key, "BUILD_ID")) {
+ free(os_build_id);
+ os_build_id = xstr8_to_16(value);
+ continue;
+ }
+ }
+
+ if (!bootspec_pick_name_version_sort_key(
+ os_pretty_name,
+ os_image_id,
+ os_name,
+ os_id,
+ os_image_version,
+ os_version,
+ os_version_id,
+ os_build_id,
+ &good_name,
+ &good_version,
+ &good_sort_key))
+ continue;
+
+ ConfigEntry *entry = xnew(ConfigEntry, 1);
+ *entry = (ConfigEntry) {
+ .id = xstrdup16(f->FileName),
+ .type = LOADER_UNIFIED_LINUX,
+ .title = xstrdup16(good_name),
+ .version = xstrdup16(good_version),
+ .device = device,
+ .loader = xpool_print(L"\\EFI\\Linux\\%s", f->FileName),
+ .sort_key = xstrdup16(good_sort_key),
+ .key = 'l',
+ .tries_done = -1,
+ .tries_left = -1,
+ };
+
+ strtolower16(entry->id);
+ config_add_entry(config, entry);
+ config_entry_parse_tries(entry, L"\\EFI\\Linux", f->FileName, L".efi");
+
+ if (szs[SECTION_CMDLINE] == 0)
+ continue;
+
+ content = mfree(content);
+
+ /* read the embedded cmdline file */
+ size_t cmdline_len;
+ err = file_read(linux_dir, f->FileName, offs[SECTION_CMDLINE], szs[SECTION_CMDLINE], &content, &cmdline_len);
+ if (err == EFI_SUCCESS) {
+ entry->options = xstrn8_to_16(content, cmdline_len);
+ mangle_stub_cmdline(entry->options);
+ }
+ }
+}
+
+static void config_load_xbootldr(
+ Config *config,
+ EFI_HANDLE *device) {
+
+ _cleanup_(file_closep) EFI_FILE *root_dir = NULL;
+ EFI_HANDLE new_device = NULL; /* avoid false maybe-uninitialized warning */
+ EFI_STATUS err;
+
+ assert(config);
+ assert(device);
+
+ err = xbootldr_open(device, &new_device, &root_dir);
+ if (err != EFI_SUCCESS)
+ return;
+
+ config_entry_add_unified(config, new_device, root_dir);
+ config_load_entries(config, new_device, root_dir, NULL);
+}
+
+static EFI_STATUS initrd_prepare(
+ EFI_FILE *root,
+ const ConfigEntry *entry,
+ char16_t **ret_options,
+ void **ret_initrd,
+ UINTN *ret_initrd_size) {
+
+ assert(root);
+ assert(entry);
+ assert(ret_options);
+ assert(ret_initrd);
+ assert(ret_initrd_size);
+
+ if (entry->type != LOADER_LINUX || !entry->initrd) {
+ ret_options = NULL;
+ ret_initrd = NULL;
+ ret_initrd_size = 0;
+ return EFI_SUCCESS;
+ }
+
+ /* Note that order of initrds matters. The kernel will only look for microcode updates in the very
+ * first one it sees. */
+
+ /* Add initrd= to options for older kernels that do not support LINUX_INITRD_MEDIA. Should be dropped
+ * if linux_x86.c is dropped. */
+ _cleanup_free_ char16_t *options = NULL;
+
+ EFI_STATUS err;
+ UINTN size = 0;
+ _cleanup_free_ uint8_t *initrd = NULL;
+
+ STRV_FOREACH(i, entry->initrd) {
+ _cleanup_free_ char16_t *o = options;
+ if (o)
+ options = xpool_print(L"%s initrd=%s", o, *i);
+ else
+ options = xpool_print(L"initrd=%s", *i);
+
+ _cleanup_(file_closep) EFI_FILE *handle = NULL;
+ err = root->Open(root, &handle, *i, EFI_FILE_MODE_READ, 0);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ _cleanup_free_ EFI_FILE_INFO *info = NULL;
+ err = get_file_info_harder(handle, &info, NULL);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ if (info->FileSize == 0) /* Automatically skip over empty files */
+ continue;
+
+ UINTN new_size, read_size = info->FileSize;
+ if (__builtin_add_overflow(size, read_size, &new_size))
+ return EFI_OUT_OF_RESOURCES;
+ initrd = xrealloc(initrd, size, new_size);
+
+ err = chunked_read(handle, &read_size, initrd + size);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ /* Make sure the actual read size is what we expected. */
+ assert(size + read_size == new_size);
+ size = new_size;
+ }
+
+ if (entry->options) {
+ _cleanup_free_ char16_t *o = options;
+ options = xpool_print(L"%s %s", o, entry->options);
+ }
+
+ *ret_options = TAKE_PTR(options);
+ *ret_initrd = TAKE_PTR(initrd);
+ *ret_initrd_size = size;
+ return EFI_SUCCESS;
+}
+
+static EFI_STATUS image_start(
+ EFI_HANDLE parent_image,
+ const ConfigEntry *entry) {
+
+ _cleanup_(devicetree_cleanup) struct devicetree_state dtstate = {};
+ _cleanup_(unload_imagep) EFI_HANDLE image = NULL;
+ _cleanup_free_ EFI_DEVICE_PATH *path = NULL;
+ EFI_STATUS err;
+
+ assert(entry);
+
+ /* If this loader entry has a special way to boot, try that first. */
+ if (entry->call)
+ (void) entry->call();
+
+ _cleanup_(file_closep) EFI_FILE *image_root = NULL;
+ err = open_volume(entry->device, &image_root);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Error opening root path: %r", err);
+
+ err = make_file_device_path(entry->device, entry->loader, &path);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Error making file device path: %r", err);
+
+ UINTN initrd_size = 0;
+ _cleanup_free_ void *initrd = NULL;
+ _cleanup_free_ char16_t *options_initrd = NULL;
+ err = initrd_prepare(image_root, entry, &options_initrd, &initrd, &initrd_size);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Error preparing initrd: %r", err);
+
+ err = shim_load_image(parent_image, path, &image);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Error loading %s: %r", entry->loader, err);
+
+ /* DTBs are loaded by the kernel before ExitBootServices, and they can be used to map and assign
+ * arbitrary memory ranges, so skip it when secure boot is enabled as the DTB here is unverified. */
+ if (entry->devicetree && !secure_boot_enabled()) {
+ err = devicetree_install(&dtstate, image_root, entry->devicetree);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Error loading %s: %r", entry->devicetree, err);
+ }
+
+ _cleanup_(cleanup_initrd) EFI_HANDLE initrd_handle = NULL;
+ err = initrd_register(initrd, initrd_size, &initrd_handle);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Error registering initrd: %r", err);
+
+ EFI_LOADED_IMAGE_PROTOCOL *loaded_image;
+ err = BS->HandleProtocol(image, &LoadedImageProtocol, (void **) &loaded_image);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Error getting LoadedImageProtocol handle: %r", err);
+
+ char16_t *options = options_initrd ?: entry->options;
+ if (options) {
+ loaded_image->LoadOptions = options;
+ loaded_image->LoadOptionsSize = strsize16(options);
+
+ /* Try to log any options to the TPM, especially to catch manually edited options */
+ (void) tpm_log_load_options(options, NULL);
+ }
+
+ efivar_set_time_usec(LOADER_GUID, L"LoaderTimeExecUSec", 0);
+ err = BS->StartImage(image, NULL, NULL);
+ graphics_mode(false);
+ if (err == EFI_SUCCESS)
+ return EFI_SUCCESS;
+
+ /* Try calling the kernel compat entry point if one exists. */
+ if (err == EFI_UNSUPPORTED && entry->type == LOADER_LINUX) {
+ uint32_t compat_address;
+
+ err = pe_kernel_info(loaded_image->ImageBase, &compat_address);
+ if (err != EFI_SUCCESS) {
+ if (err != EFI_UNSUPPORTED)
+ return log_error_status_stall(err, L"Error finding kernel compat entry address: %r", err);
+ } else if (compat_address > 0) {
+ EFI_IMAGE_ENTRY_POINT kernel_entry =
+ (EFI_IMAGE_ENTRY_POINT) ((uint8_t *) loaded_image->ImageBase + compat_address);
+
+ err = kernel_entry(image, ST);
+ graphics_mode(false);
+ if (err == EFI_SUCCESS)
+ return EFI_SUCCESS;
+ } else
+ err = EFI_UNSUPPORTED;
+ }
+
+ return log_error_status_stall(err, L"Failed to execute %s (%s): %r", entry->title_show, entry->loader, err);
+}
+
+static void config_free(Config *config) {
+ assert(config);
+ for (UINTN i = 0; i < config->entry_count; i++)
+ config_entry_free(config->entries[i]);
+ free(config->entries);
+ free(config->entry_default_config);
+ free(config->entry_default_efivar);
+ free(config->entry_oneshot);
+ free(config->entry_saved);
+}
+
+static void config_write_entries_to_variable(Config *config) {
+ _cleanup_free_ char *buffer = NULL;
+ UINTN sz = 0;
+ char *p;
+
+ assert(config);
+
+ for (UINTN i = 0; i < config->entry_count; i++)
+ sz += strsize16(config->entries[i]->id);
+
+ p = buffer = xmalloc(sz);
+
+ for (UINTN i = 0; i < config->entry_count; i++)
+ p = mempcpy(p, config->entries[i]->id, strsize16(config->entries[i]->id));
+
+ assert(p == buffer + sz);
+
+ /* Store the full list of discovered entries. */
+ (void) efivar_set_raw(LOADER_GUID, L"LoaderEntries", buffer, sz, 0);
+}
+
+static void save_selected_entry(const Config *config, const ConfigEntry *entry) {
+ assert(config);
+ assert(entry);
+ assert(entry->loader || !entry->call);
+
+ /* Always export the selected boot entry to the system in a volatile var. */
+ (void) efivar_set(LOADER_GUID, L"LoaderEntrySelected", entry->id, 0);
+
+ /* Do not save or delete if this was a oneshot boot. */
+ if (streq16(config->entry_oneshot, entry->id))
+ return;
+
+ if (config->use_saved_entry_efivar || (!config->entry_default_efivar && config->use_saved_entry)) {
+ /* Avoid unnecessary NVRAM writes. */
+ if (streq16(config->entry_saved, entry->id))
+ return;
+
+ (void) efivar_set(LOADER_GUID, L"LoaderEntryLastBooted", entry->id, EFI_VARIABLE_NON_VOLATILE);
+ } else
+ /* Delete the non-volatile var if not needed. */
+ (void) efivar_set(LOADER_GUID, L"LoaderEntryLastBooted", NULL, EFI_VARIABLE_NON_VOLATILE);
+}
+
+static EFI_STATUS secure_boot_discover_keys(Config *config, EFI_FILE *root_dir) {
+ EFI_STATUS err;
+ _cleanup_(file_closep) EFI_FILE *keys_basedir = NULL;
+
+ if (secure_boot_mode() != SECURE_BOOT_SETUP)
+ return EFI_SUCCESS;
+
+ /* the lack of a 'keys' directory is not fatal and is silently ignored */
+ err = open_directory(root_dir, u"\\loader\\keys", &keys_basedir);
+ if (err == EFI_NOT_FOUND)
+ return EFI_SUCCESS;
+ if (err != EFI_SUCCESS)
+ return err;
+
+ for (;;) {
+ _cleanup_free_ EFI_FILE_INFO *dirent = NULL;
+ size_t dirent_size = 0;
+ ConfigEntry *entry = NULL;
+
+ err = readdir_harder(keys_basedir, &dirent, &dirent_size);
+ if (err != EFI_SUCCESS || !dirent)
+ return err;
+
+ if (dirent->FileName[0] == '.')
+ continue;
+
+ if (!FLAGS_SET(dirent->Attribute, EFI_FILE_DIRECTORY))
+ continue;
+
+ entry = xnew(ConfigEntry, 1);
+ *entry = (ConfigEntry) {
+ .id = xpool_print(L"secure-boot-keys-%s", dirent->FileName),
+ .title = xpool_print(L"Enroll Secure Boot keys: %s", dirent->FileName),
+ .path = xpool_print(L"\\loader\\keys\\%s", dirent->FileName),
+ .type = LOADER_SECURE_BOOT_KEYS,
+ .tries_done = -1,
+ .tries_left = -1,
+ };
+ config_add_entry(config, entry);
+
+ if (config->secure_boot_enroll == ENROLL_FORCE && strcaseeq16(dirent->FileName, u"auto"))
+ /* if we auto enroll successfully this call does not return, if it fails we still
+ * want to add other potential entries to the menu */
+ secure_boot_enroll_at(root_dir, entry->path);
+ }
+
+ return EFI_SUCCESS;
+}
+
+static void export_variables(
+ EFI_LOADED_IMAGE_PROTOCOL *loaded_image,
+ const char16_t *loaded_image_path,
+ uint64_t init_usec) {
+
+ static const uint64_t loader_features =
+ EFI_LOADER_FEATURE_CONFIG_TIMEOUT |
+ EFI_LOADER_FEATURE_CONFIG_TIMEOUT_ONE_SHOT |
+ EFI_LOADER_FEATURE_ENTRY_DEFAULT |
+ EFI_LOADER_FEATURE_ENTRY_ONESHOT |
+ EFI_LOADER_FEATURE_BOOT_COUNTING |
+ EFI_LOADER_FEATURE_XBOOTLDR |
+ EFI_LOADER_FEATURE_RANDOM_SEED |
+ EFI_LOADER_FEATURE_LOAD_DRIVER |
+ EFI_LOADER_FEATURE_SORT_KEY |
+ EFI_LOADER_FEATURE_SAVED_ENTRY |
+ EFI_LOADER_FEATURE_DEVICETREE |
+ 0;
+
+ _cleanup_free_ char16_t *infostr = NULL, *typestr = NULL;
+ char16_t uuid[37];
+
+ assert(loaded_image);
+
+ efivar_set_time_usec(LOADER_GUID, L"LoaderTimeInitUSec", init_usec);
+ efivar_set(LOADER_GUID, L"LoaderInfo", L"systemd-boot " GIT_VERSION, 0);
+
+ infostr = xpool_print(L"%s %u.%02u", ST->FirmwareVendor, ST->FirmwareRevision >> 16, ST->FirmwareRevision & 0xffff);
+ efivar_set(LOADER_GUID, L"LoaderFirmwareInfo", infostr, 0);
+
+ typestr = xpool_print(L"UEFI %u.%02u", ST->Hdr.Revision >> 16, ST->Hdr.Revision & 0xffff);
+ efivar_set(LOADER_GUID, L"LoaderFirmwareType", typestr, 0);
+
+ (void) efivar_set_uint64_le(LOADER_GUID, L"LoaderFeatures", loader_features, 0);
+
+ /* the filesystem path to this image, to prevent adding ourselves to the menu */
+ efivar_set(LOADER_GUID, L"LoaderImageIdentifier", loaded_image_path, 0);
+
+ /* export the device path this image is started from */
+ if (disk_get_part_uuid(loaded_image->DeviceHandle, uuid) == EFI_SUCCESS)
+ efivar_set(LOADER_GUID, L"LoaderDevicePartUUID", uuid, 0);
+}
+
+static void config_load_all_entries(
+ Config *config,
+ EFI_LOADED_IMAGE_PROTOCOL *loaded_image,
+ const char16_t *loaded_image_path,
+ EFI_FILE *root_dir) {
+
+ assert(config);
+ assert(loaded_image);
+ assert(root_dir);
+
+ config_load_defaults(config, root_dir);
+
+ /* scan /EFI/Linux/ directory */
+ config_entry_add_unified(config, loaded_image->DeviceHandle, root_dir);
+
+ /* scan /loader/entries/\*.conf files */
+ config_load_entries(config, loaded_image->DeviceHandle, root_dir, loaded_image_path);
+
+ /* Similar, but on any XBOOTLDR partition */
+ config_load_xbootldr(config, loaded_image->DeviceHandle);
+
+ /* sort entries after version number */
+ sort_pointer_array((void **) config->entries, config->entry_count, (compare_pointer_func_t) config_entry_compare);
+
+ /* if we find some well-known loaders, add them to the end of the list */
+ config_entry_add_osx(config);
+ config_entry_add_windows(config, loaded_image->DeviceHandle, root_dir);
+ config_entry_add_loader_auto(config, loaded_image->DeviceHandle, root_dir, NULL,
+ L"auto-efi-shell", 's', L"EFI Shell", L"\\shell" EFI_MACHINE_TYPE_NAME ".efi");
+ config_entry_add_loader_auto(config, loaded_image->DeviceHandle, root_dir, loaded_image_path,
+ L"auto-efi-default", '\0', L"EFI Default Loader", NULL);
+
+ if (config->auto_firmware && FLAGS_SET(get_os_indications_supported(), EFI_OS_INDICATIONS_BOOT_TO_FW_UI)) {
+ ConfigEntry *entry = xnew(ConfigEntry, 1);
+ *entry = (ConfigEntry) {
+ .id = xstrdup16(u"auto-reboot-to-firmware-setup"),
+ .title = xstrdup16(u"Reboot Into Firmware Interface"),
+ .call = reboot_into_firmware,
+ .tries_done = -1,
+ .tries_left = -1,
+ };
+ config_add_entry(config, entry);
+ }
+
+ /* find if secure boot signing keys exist and autoload them if necessary
+ otherwise creates menu entries so that the user can load them manually
+ if the secure-boot-enroll variable is set to no (the default), we do not
+ even search for keys on the ESP */
+ if (config->secure_boot_enroll != ENROLL_OFF)
+ secure_boot_discover_keys(config, root_dir);
+
+ if (config->entry_count == 0)
+ return;
+
+ config_write_entries_to_variable(config);
+
+ config_title_generate(config);
+
+ /* select entry by configured pattern or EFI LoaderDefaultEntry= variable */
+ config_default_entry_select(config);
+}
+
+EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *sys_table) {
+ EFI_LOADED_IMAGE_PROTOCOL *loaded_image;
+ _cleanup_(file_closep) EFI_FILE *root_dir = NULL;
+ _cleanup_(config_free) Config config = {};
+ _cleanup_free_ char16_t *loaded_image_path = NULL;
+ EFI_STATUS err;
+ uint64_t init_usec;
+ bool menu = false;
+
+ InitializeLib(image, sys_table);
+ init_usec = time_usec();
+ debug_hook(L"systemd-boot");
+ /* Uncomment the next line if you need to wait for debugger. */
+ // debug_break();
+
+ err = BS->OpenProtocol(image,
+ &LoadedImageProtocol,
+ (void **)&loaded_image,
+ image,
+ NULL,
+ EFI_OPEN_PROTOCOL_GET_PROTOCOL);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Error getting a LoadedImageProtocol handle: %r", err);
+
+ (void) device_path_to_str(loaded_image->FilePath, &loaded_image_path);
+
+ export_variables(loaded_image, loaded_image_path, init_usec);
+
+ /* The firmware may skip initializing some devices for the sake of a faster boot. This is especially
+ * true for fastboot enabled firmwares. But this means that things we use like input devices or the
+ * xbootldr partition may not be available yet. Reconnect all drivers should hopefully make the
+ * firmware initialize everything we need. */
+ if (is_direct_boot(loaded_image->DeviceHandle))
+ (void) reconnect_all_drivers();
+
+ err = open_volume(loaded_image->DeviceHandle, &root_dir);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Unable to open root directory: %r", err);
+
+ (void) load_drivers(image, loaded_image, root_dir);
+
+ config_load_all_entries(&config, loaded_image, loaded_image_path, root_dir);
+
+ if (config.entry_count == 0) {
+ log_error_stall(L"No loader found. Configuration files in \\loader\\entries\\*.conf are needed.");
+ goto out;
+ }
+
+ /* select entry or show menu when key is pressed or timeout is set */
+ if (config.force_menu || config.timeout_sec > 0)
+ menu = true;
+ else {
+ uint64_t key;
+
+ /* Block up to 100ms to give firmware time to get input working. */
+ err = console_key_read(&key, 100 * 1000);
+ if (err == EFI_SUCCESS) {
+ /* find matching key in config entries */
+ UINTN idx = entry_lookup_key(&config, config.idx_default, KEYCHAR(key));
+ if (idx != IDX_INVALID)
+ config.idx_default = idx;
+ else
+ menu = true;
+ }
+ }
+
+ for (;;) {
+ ConfigEntry *entry;
+
+ entry = config.entries[config.idx_default];
+ if (menu) {
+ efivar_set_time_usec(LOADER_GUID, L"LoaderTimeMenuUSec", 0);
+ if (!menu_run(&config, &entry, loaded_image_path))
+ break;
+ }
+
+ /* if auto enrollment is activated, we try to load keys for the given entry. */
+ if (entry->type == LOADER_SECURE_BOOT_KEYS && config.secure_boot_enroll != ENROLL_OFF) {
+ err = secure_boot_enroll_at(root_dir, entry->path);
+ if (err != EFI_SUCCESS)
+ return err;
+ continue;
+ }
+
+ /* Run special entry like "reboot" now. Those that have a loader
+ * will be handled by image_start() instead. */
+ if (entry->call && !entry->loader) {
+ entry->call();
+ continue;
+ }
+
+ (void) config_entry_bump_counters(entry);
+ save_selected_entry(&config, entry);
+
+ /* Optionally, read a random seed off the ESP and pass it to the OS */
+ (void) process_random_seed(root_dir, config.random_seed_mode);
+
+ err = image_start(image, entry);
+ if (err != EFI_SUCCESS)
+ goto out;
+
+ menu = true;
+ config.timeout_sec = 0;
+ }
+ err = EFI_SUCCESS;
+out:
+ BS->CloseProtocol(image, &LoadedImageProtocol, image, NULL);
+ return err;
+}
diff --git a/src/boot/efi/console.c b/src/boot/efi/console.c
new file mode 100644
index 0000000..14c0008
--- /dev/null
+++ b/src/boot/efi/console.c
@@ -0,0 +1,309 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <efi.h>
+#include <efilib.h>
+
+#include "console.h"
+#include "util.h"
+
+#define SYSTEM_FONT_WIDTH 8
+#define SYSTEM_FONT_HEIGHT 19
+#define HORIZONTAL_MAX_OK 1920
+#define VERTICAL_MAX_OK 1080
+#define VIEWPORT_RATIO 10
+
+static inline void event_closep(EFI_EVENT *event) {
+ if (!*event)
+ return;
+
+ BS->CloseEvent(*event);
+}
+
+/*
+ * Reading input from the console sounds like an easy task to do, but thanks to broken
+ * firmware it is actually a nightmare.
+ *
+ * There is a SimpleTextInput and SimpleTextInputEx API for this. Ideally we want to use
+ * TextInputEx, because that gives us Ctrl/Alt/Shift key state information. Unfortunately,
+ * it is not always available and sometimes just non-functional.
+ *
+ * On some firmware, calling ReadKeyStroke or ReadKeyStrokeEx on the default console input
+ * device will just freeze no matter what (even though it *reported* being ready).
+ * Also, multiple input protocols can be backed by the same device, but they can be out of
+ * sync. Falling back on a different protocol can end up with double input.
+ *
+ * Therefore, we will preferably use TextInputEx for ConIn if that is available. Additionally,
+ * we look for the first TextInputEx device the firmware gives us as a fallback option. It
+ * will replace ConInEx permanently if it ever reports a key press.
+ * Lastly, a timer event allows us to provide a input timeout without having to call into
+ * any input functions that can freeze on us or using a busy/stall loop. */
+EFI_STATUS console_key_read(uint64_t *key, uint64_t timeout_usec) {
+ static EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *conInEx = NULL, *extraInEx = NULL;
+ static bool checked = false;
+ UINTN index;
+ EFI_STATUS err;
+ _cleanup_(event_closep) EFI_EVENT timer = NULL;
+
+ assert(key);
+
+ if (!checked) {
+ /* Get the *first* TextInputEx device.*/
+ err = BS->LocateProtocol(&SimpleTextInputExProtocol, NULL, (void **) &extraInEx);
+ if (err != EFI_SUCCESS || BS->CheckEvent(extraInEx->WaitForKeyEx) == EFI_INVALID_PARAMETER)
+ /* If WaitForKeyEx fails here, the firmware pretends it talks this
+ * protocol, but it really doesn't. */
+ extraInEx = NULL;
+
+ /* Get the TextInputEx version of ST->ConIn. */
+ err = BS->HandleProtocol(ST->ConsoleInHandle, &SimpleTextInputExProtocol, (void **) &conInEx);
+ if (err != EFI_SUCCESS || BS->CheckEvent(conInEx->WaitForKeyEx) == EFI_INVALID_PARAMETER)
+ conInEx = NULL;
+
+ if (conInEx == extraInEx)
+ extraInEx = NULL;
+
+ checked = true;
+ }
+
+ err = BS->CreateEvent(EVT_TIMER, 0, NULL, NULL, &timer);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Error creating timer event: %r", err);
+
+ EFI_EVENT events[] = {
+ timer,
+ conInEx ? conInEx->WaitForKeyEx : ST->ConIn->WaitForKey,
+ extraInEx ? extraInEx->WaitForKeyEx : NULL,
+ };
+ UINTN n_events = extraInEx ? 3 : 2;
+
+ /* Watchdog rearming loop in case the user never provides us with input or some
+ * broken firmware never returns from WaitForEvent. */
+ for (;;) {
+ uint64_t watchdog_timeout_sec = 5 * 60,
+ watchdog_ping_usec = watchdog_timeout_sec / 2 * 1000 * 1000;
+
+ /* SetTimer expects 100ns units for some reason. */
+ err = BS->SetTimer(
+ timer,
+ TimerRelative,
+ MIN(timeout_usec, watchdog_ping_usec) * 10);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Error arming timer event: %r", err);
+
+ (void) BS->SetWatchdogTimer(watchdog_timeout_sec, 0x10000, 0, NULL);
+ err = BS->WaitForEvent(n_events, events, &index);
+ (void) BS->SetWatchdogTimer(watchdog_timeout_sec, 0x10000, 0, NULL);
+
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Error waiting for events: %r", err);
+
+ /* We have keyboard input, process it after this loop. */
+ if (timer != events[index])
+ break;
+
+ /* The EFI timer fired instead. If this was a watchdog timeout, loop again. */
+ if (timeout_usec == UINT64_MAX)
+ continue;
+ else if (timeout_usec > watchdog_ping_usec) {
+ timeout_usec -= watchdog_ping_usec;
+ continue;
+ }
+
+ /* The caller requested a timeout? They shall have one! */
+ return EFI_TIMEOUT;
+ }
+
+ /* If the extra input device we found returns something, always use that instead
+ * to work around broken firmware freezing on ConIn/ConInEx. */
+ if (extraInEx && BS->CheckEvent(extraInEx->WaitForKeyEx) == EFI_SUCCESS) {
+ conInEx = extraInEx;
+ extraInEx = NULL;
+ }
+
+ /* Do not fall back to ConIn if we have a ConIn that supports TextInputEx.
+ * The two may be out of sync on some firmware, giving us double input. */
+ if (conInEx) {
+ EFI_KEY_DATA keydata;
+ uint32_t shift = 0;
+
+ err = conInEx->ReadKeyStrokeEx(conInEx, &keydata);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ if (FLAGS_SET(keydata.KeyState.KeyShiftState, EFI_SHIFT_STATE_VALID)) {
+ /* Do not distinguish between left and right keys (set both flags). */
+ if (keydata.KeyState.KeyShiftState & EFI_CONTROL_PRESSED)
+ shift |= EFI_CONTROL_PRESSED;
+ if (keydata.KeyState.KeyShiftState & EFI_ALT_PRESSED)
+ shift |= EFI_ALT_PRESSED;
+ if (keydata.KeyState.KeyShiftState & EFI_LOGO_PRESSED)
+ shift |= EFI_LOGO_PRESSED;
+
+ /* Shift is not supposed to be reported for keys that can be represented as uppercase
+ * unicode chars (Shift+f is reported as F instead). Some firmware does it anyway, so
+ * filter those out. */
+ if ((keydata.KeyState.KeyShiftState & EFI_SHIFT_PRESSED) &&
+ keydata.Key.UnicodeChar == 0)
+ shift |= EFI_SHIFT_PRESSED;
+ }
+
+ /* 32 bit modifier keys + 16 bit scan code + 16 bit unicode */
+ *key = KEYPRESS(shift, keydata.Key.ScanCode, keydata.Key.UnicodeChar);
+ return EFI_SUCCESS;
+ } else if (BS->CheckEvent(ST->ConIn->WaitForKey) == EFI_SUCCESS) {
+ EFI_INPUT_KEY k;
+
+ err = ST->ConIn->ReadKeyStroke(ST->ConIn, &k);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ *key = KEYPRESS(0, k.ScanCode, k.UnicodeChar);
+ return EFI_SUCCESS;
+ }
+
+ return EFI_NOT_READY;
+}
+
+static EFI_STATUS change_mode(int64_t mode) {
+ EFI_STATUS err;
+ int32_t old_mode;
+
+ /* SetMode expects a UINTN, so make sure these values are sane. */
+ mode = CLAMP(mode, CONSOLE_MODE_RANGE_MIN, CONSOLE_MODE_RANGE_MAX);
+ old_mode = MAX(CONSOLE_MODE_RANGE_MIN, ST->ConOut->Mode->Mode);
+
+ err = ST->ConOut->SetMode(ST->ConOut, mode);
+ if (err == EFI_SUCCESS)
+ return EFI_SUCCESS;
+
+ /* Something went wrong. Output is probably borked, so try to revert to previous mode. */
+ if (ST->ConOut->SetMode(ST->ConOut, old_mode) == EFI_SUCCESS)
+ return err;
+
+ /* Maybe the device is on fire? */
+ ST->ConOut->Reset(ST->ConOut, true);
+ ST->ConOut->SetMode(ST->ConOut, CONSOLE_MODE_RANGE_MIN);
+ return err;
+}
+
+EFI_STATUS query_screen_resolution(uint32_t *ret_w, uint32_t *ret_h) {
+ EFI_STATUS err;
+ EFI_GRAPHICS_OUTPUT_PROTOCOL *go;
+
+ err = BS->LocateProtocol(&GraphicsOutputProtocol, NULL, (void **) &go);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ if (!go->Mode || !go->Mode->Info)
+ return EFI_DEVICE_ERROR;
+
+ *ret_w = go->Mode->Info->HorizontalResolution;
+ *ret_h = go->Mode->Info->VerticalResolution;
+ return EFI_SUCCESS;
+}
+
+static int64_t get_auto_mode(void) {
+ uint32_t screen_width, screen_height;
+
+ if (query_screen_resolution(&screen_width, &screen_height) == EFI_SUCCESS) {
+ bool keep = false;
+
+ /* Start verifying if we are in a resolution larger than Full HD
+ * (1920x1080). If we're not, assume we're in a good mode and do not
+ * try to change it. */
+ if (screen_width <= HORIZONTAL_MAX_OK && screen_height <= VERTICAL_MAX_OK)
+ keep = true;
+ /* For larger resolutions, calculate the ratio of the total screen
+ * area to the text viewport area. If it's less than 10 times bigger,
+ * then assume the text is readable and keep the text mode. */
+ else {
+ uint64_t text_area;
+ UINTN x_max, y_max;
+ uint64_t screen_area = (uint64_t)screen_width * (uint64_t)screen_height;
+
+ console_query_mode(&x_max, &y_max);
+ text_area = SYSTEM_FONT_WIDTH * SYSTEM_FONT_HEIGHT * (uint64_t)x_max * (uint64_t)y_max;
+
+ if (text_area != 0 && screen_area/text_area < VIEWPORT_RATIO)
+ keep = true;
+ }
+
+ if (keep)
+ return ST->ConOut->Mode->Mode;
+ }
+
+ /* If we reached here, then we have a high resolution screen and the text
+ * viewport is less than 10% the screen area, so the firmware developer
+ * screwed up. Try to switch to a better mode. Mode number 2 is first non
+ * standard mode, which is provided by the device manufacturer, so it should
+ * be a good mode.
+ * Note: MaxMode is the number of modes, not the last mode. */
+ if (ST->ConOut->Mode->MaxMode > CONSOLE_MODE_FIRMWARE_FIRST)
+ return CONSOLE_MODE_FIRMWARE_FIRST;
+
+ /* Try again with mode different than zero (assume user requests
+ * auto mode due to some problem with mode zero). */
+ if (ST->ConOut->Mode->MaxMode > CONSOLE_MODE_80_50)
+ return CONSOLE_MODE_80_50;
+
+ return CONSOLE_MODE_80_25;
+}
+
+EFI_STATUS console_set_mode(int64_t mode) {
+ switch (mode) {
+ case CONSOLE_MODE_KEEP:
+ /* If the firmware indicates the current mode is invalid, change it anyway. */
+ if (ST->ConOut->Mode->Mode < CONSOLE_MODE_RANGE_MIN)
+ return change_mode(CONSOLE_MODE_RANGE_MIN);
+ return EFI_SUCCESS;
+
+ case CONSOLE_MODE_NEXT:
+ if (ST->ConOut->Mode->MaxMode <= CONSOLE_MODE_RANGE_MIN)
+ return EFI_UNSUPPORTED;
+
+ mode = MAX(CONSOLE_MODE_RANGE_MIN, ST->ConOut->Mode->Mode);
+ do {
+ mode = (mode + 1) % ST->ConOut->Mode->MaxMode;
+ if (change_mode(mode) == EFI_SUCCESS)
+ break;
+ /* If this mode is broken/unsupported, try the next.
+ * If mode is 0, we wrapped around and should stop. */
+ } while (mode > CONSOLE_MODE_RANGE_MIN);
+
+ return EFI_SUCCESS;
+
+ case CONSOLE_MODE_AUTO:
+ return change_mode(get_auto_mode());
+
+ case CONSOLE_MODE_FIRMWARE_MAX:
+ /* Note: MaxMode is the number of modes, not the last mode. */
+ return change_mode(ST->ConOut->Mode->MaxMode - 1LL);
+
+ default:
+ return change_mode(mode);
+ }
+}
+
+EFI_STATUS console_query_mode(UINTN *x_max, UINTN *y_max) {
+ EFI_STATUS err;
+
+ assert(x_max);
+ assert(y_max);
+
+ err = ST->ConOut->QueryMode(ST->ConOut, ST->ConOut->Mode->Mode, x_max, y_max);
+ if (err != EFI_SUCCESS) {
+ /* Fallback values mandated by UEFI spec. */
+ switch (ST->ConOut->Mode->Mode) {
+ case CONSOLE_MODE_80_50:
+ *x_max = 80;
+ *y_max = 50;
+ break;
+ case CONSOLE_MODE_80_25:
+ default:
+ *x_max = 80;
+ *y_max = 25;
+ }
+ }
+
+ return err;
+}
diff --git a/src/boot/efi/console.h b/src/boot/efi/console.h
new file mode 100644
index 0000000..673a8ee
--- /dev/null
+++ b/src/boot/efi/console.h
@@ -0,0 +1,37 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "missing_efi.h"
+
+enum {
+ EFI_SHIFT_PRESSED = EFI_RIGHT_SHIFT_PRESSED|EFI_LEFT_SHIFT_PRESSED,
+ EFI_CONTROL_PRESSED = EFI_RIGHT_CONTROL_PRESSED|EFI_LEFT_CONTROL_PRESSED,
+ EFI_ALT_PRESSED = EFI_RIGHT_ALT_PRESSED|EFI_LEFT_ALT_PRESSED,
+ EFI_LOGO_PRESSED = EFI_RIGHT_LOGO_PRESSED|EFI_LEFT_LOGO_PRESSED,
+};
+
+#define KEYPRESS(keys, scan, uni) ((((uint64_t)keys) << 32) | (((uint64_t)scan) << 16) | (uni))
+#define KEYCHAR(k) ((char16_t)(k))
+#define CHAR_CTRL(c) ((c) - 'a' + 1)
+
+enum {
+ /* Console mode is a int32_t in EFI. We use int64_t to make room for our special values. */
+ CONSOLE_MODE_RANGE_MIN = 0,
+ CONSOLE_MODE_RANGE_MAX = INT32_MAX, /* This is just the theoretical limit. */
+ CONSOLE_MODE_INVALID = -1, /* UEFI uses -1 if the device is not in a valid text mode. */
+
+ CONSOLE_MODE_80_25 = 0, /* 80x25 is required by UEFI spec. */
+ CONSOLE_MODE_80_50 = 1, /* 80x50 may be supported. */
+ CONSOLE_MODE_FIRMWARE_FIRST = 2, /* First custom mode, if supported. */
+
+ /* These are our own mode values that map to concrete values at runtime. */
+ CONSOLE_MODE_KEEP = CONSOLE_MODE_RANGE_MAX + 1LL,
+ CONSOLE_MODE_NEXT,
+ CONSOLE_MODE_AUTO,
+ CONSOLE_MODE_FIRMWARE_MAX, /* 'max' in config. */
+};
+
+EFI_STATUS console_key_read(uint64_t *key, uint64_t timeout_usec);
+EFI_STATUS console_set_mode(int64_t mode);
+EFI_STATUS console_query_mode(UINTN *x_max, UINTN *y_max);
+EFI_STATUS query_screen_resolution(uint32_t *ret_width, uint32_t *ret_height);
diff --git a/src/boot/efi/cpio.c b/src/boot/efi/cpio.c
new file mode 100644
index 0000000..79b5d43
--- /dev/null
+++ b/src/boot/efi/cpio.c
@@ -0,0 +1,568 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "cpio.h"
+#include "measure.h"
+#include "util.h"
+
+static char *write_cpio_word(char *p, uint32_t v) {
+ static const char hex[] = "0123456789abcdef";
+
+ assert(p);
+
+ /* Writes a CPIO header 8 character hex value */
+
+ for (UINTN i = 0; i < 8; i++)
+ p[7-i] = hex[(v >> (4 * i)) & 0xF];
+
+ return p + 8;
+}
+
+static char *mangle_filename(char *p, const char16_t *f) {
+ char* w;
+
+ assert(p);
+ assert(f);
+
+ /* Basically converts UTF-16 to plain ASCII (note that we filtered non-ASCII filenames beforehand, so
+ * this operation is always safe) */
+
+ for (w = p; *f != 0; f++) {
+ assert(*f <= 0x7fu);
+
+ *(w++) = *f;
+ }
+
+ *(w++) = 0;
+ return w;
+}
+
+static char *pad4(char *p, const char *start) {
+ assert(p);
+ assert(start);
+ assert(p >= start);
+
+ /* Appends NUL bytes to 'p', until the address is divisible by 4, when taken relative to 'start' */
+
+ while ((p - start) % 4 != 0)
+ *(p++) = 0;
+
+ return p;
+}
+
+static EFI_STATUS pack_cpio_one(
+ const char16_t *fname,
+ const void *contents,
+ UINTN contents_size,
+ const char *target_dir_prefix,
+ uint32_t access_mode,
+ uint32_t *inode_counter,
+ void **cpio_buffer,
+ UINTN *cpio_buffer_size) {
+
+ UINTN l, target_dir_prefix_size, fname_size, q;
+ char *a;
+
+ assert(fname);
+ assert(contents_size || contents_size == 0);
+ assert(target_dir_prefix);
+ assert(inode_counter);
+ assert(cpio_buffer);
+ assert(cpio_buffer_size);
+
+ /* Serializes one file in the cpio format understood by the kernel initrd logic.
+ *
+ * See: https://docs.kernel.org/driver-api/early-userspace/buffer-format.html */
+
+ if (contents_size > UINT32_MAX) /* cpio cannot deal with > 32bit file sizes */
+ return EFI_LOAD_ERROR;
+
+ if (*inode_counter == UINT32_MAX) /* more than 2^32-1 inodes? yikes. cpio doesn't support that either */
+ return EFI_OUT_OF_RESOURCES;
+
+ l = 6 + 13*8 + 1 + 1; /* Fixed CPIO header size, slash separator, and NUL byte after the file name*/
+
+ target_dir_prefix_size = strlen8(target_dir_prefix);
+ if (l > UINTN_MAX - target_dir_prefix_size)
+ return EFI_OUT_OF_RESOURCES;
+ l += target_dir_prefix_size;
+
+ fname_size = strlen16(fname);
+ if (l > UINTN_MAX - fname_size)
+ return EFI_OUT_OF_RESOURCES;
+ l += fname_size; /* append space for file name */
+
+ /* CPIO can't deal with fnames longer than 2^32-1 */
+ if (target_dir_prefix_size + fname_size >= UINT32_MAX)
+ return EFI_OUT_OF_RESOURCES;
+
+ /* Align the whole header to 4 byte size */
+ l = ALIGN4(l);
+ if (l == UINTN_MAX) /* overflow check */
+ return EFI_OUT_OF_RESOURCES;
+
+ /* Align the contents to 4 byte size */
+ q = ALIGN4(contents_size);
+ if (q == UINTN_MAX) /* overflow check */
+ return EFI_OUT_OF_RESOURCES;
+
+ if (l > UINTN_MAX - q) /* overflow check */
+ return EFI_OUT_OF_RESOURCES;
+ l += q; /* Add contents to header */
+
+ if (*cpio_buffer_size > UINTN_MAX - l) /* overflow check */
+ return EFI_OUT_OF_RESOURCES;
+ a = xrealloc(*cpio_buffer, *cpio_buffer_size, *cpio_buffer_size + l);
+
+ *cpio_buffer = a;
+ a = (char *) *cpio_buffer + *cpio_buffer_size;
+
+ a = mempcpy(a, "070701", 6); /* magic ID */
+
+ a = write_cpio_word(a, (*inode_counter)++); /* inode */
+ a = write_cpio_word(a, access_mode | 0100000 /* = S_IFREG */); /* mode */
+ a = write_cpio_word(a, 0); /* uid */
+ a = write_cpio_word(a, 0); /* gid */
+ a = write_cpio_word(a, 1); /* nlink */
+
+ /* Note: we don't make any attempt to propagate the mtime here, for two reasons: it's a mess given
+ * that FAT usually is assumed to operate with timezoned timestamps, while UNIX does not. More
+ * importantly though: the modifications times would hamper our goals of providing stable
+ * measurements for the same boots. After all we extend the initrds we generate here into TPM2
+ * PCRs. */
+ a = write_cpio_word(a, 0); /* mtime */
+ a = write_cpio_word(a, contents_size); /* size */
+ a = write_cpio_word(a, 0); /* major(dev) */
+ a = write_cpio_word(a, 0); /* minor(dev) */
+ a = write_cpio_word(a, 0); /* major(rdev) */
+ a = write_cpio_word(a, 0); /* minor(rdev) */
+ a = write_cpio_word(a, target_dir_prefix_size + fname_size + 2); /* fname size */
+ a = write_cpio_word(a, 0); /* "crc" */
+
+ a = mempcpy(a, target_dir_prefix, target_dir_prefix_size);
+ *(a++) = '/';
+ a = mangle_filename(a, fname);
+
+ /* Pad to next multiple of 4 */
+ a = pad4(a, *cpio_buffer);
+
+ a = mempcpy(a, contents, contents_size);
+
+ /* Pad to next multiple of 4 */
+ a = pad4(a, *cpio_buffer);
+
+ assert(a == (char *) *cpio_buffer + *cpio_buffer_size + l);
+ *cpio_buffer_size += l;
+
+ return EFI_SUCCESS;
+}
+
+static EFI_STATUS pack_cpio_dir(
+ const char *path,
+ uint32_t access_mode,
+ uint32_t *inode_counter,
+ void **cpio_buffer,
+ UINTN *cpio_buffer_size) {
+
+ UINTN l, path_size;
+ char *a;
+
+ assert(path);
+ assert(inode_counter);
+ assert(cpio_buffer);
+ assert(cpio_buffer_size);
+
+ /* Serializes one directory inode in cpio format. Note that cpio archives must first create the dirs
+ * they want to place files in. */
+
+ if (*inode_counter == UINT32_MAX)
+ return EFI_OUT_OF_RESOURCES;
+
+ l = 6 + 13*8 + 1; /* Fixed CPIO header size, and NUL byte after the file name*/
+
+ path_size = strlen8(path);
+ if (l > UINTN_MAX - path_size)
+ return EFI_OUT_OF_RESOURCES;
+ l += path_size;
+
+ /* Align the whole header to 4 byte size */
+ l = ALIGN4(l);
+ if (l == UINTN_MAX) /* overflow check */
+ return EFI_OUT_OF_RESOURCES;
+
+ if (*cpio_buffer_size > UINTN_MAX - l) /* overflow check */
+ return EFI_OUT_OF_RESOURCES;
+
+ *cpio_buffer = a = xrealloc(*cpio_buffer, *cpio_buffer_size, *cpio_buffer_size + l);
+ a = (char *) *cpio_buffer + *cpio_buffer_size;
+
+ a = mempcpy(a, "070701", 6); /* magic ID */
+
+ a = write_cpio_word(a, (*inode_counter)++); /* inode */
+ a = write_cpio_word(a, access_mode | 0040000 /* = S_IFDIR */); /* mode */
+ a = write_cpio_word(a, 0); /* uid */
+ a = write_cpio_word(a, 0); /* gid */
+ a = write_cpio_word(a, 1); /* nlink */
+ a = write_cpio_word(a, 0); /* mtime */
+ a = write_cpio_word(a, 0); /* size */
+ a = write_cpio_word(a, 0); /* major(dev) */
+ a = write_cpio_word(a, 0); /* minor(dev) */
+ a = write_cpio_word(a, 0); /* major(rdev) */
+ a = write_cpio_word(a, 0); /* minor(rdev) */
+ a = write_cpio_word(a, path_size + 1); /* fname size */
+ a = write_cpio_word(a, 0); /* "crc" */
+
+ a = mempcpy(a, path, path_size + 1);
+
+ /* Pad to next multiple of 4 */
+ a = pad4(a, *cpio_buffer);
+
+ assert(a == (char *) *cpio_buffer + *cpio_buffer_size + l);
+
+ *cpio_buffer_size += l;
+ return EFI_SUCCESS;
+}
+
+static EFI_STATUS pack_cpio_prefix(
+ const char *path,
+ uint32_t dir_mode,
+ uint32_t *inode_counter,
+ void **cpio_buffer,
+ UINTN *cpio_buffer_size) {
+
+ EFI_STATUS err;
+
+ assert(path);
+ assert(inode_counter);
+ assert(cpio_buffer);
+ assert(cpio_buffer_size);
+
+ /* Serializes directory inodes of all prefix paths of the specified path in cpio format. Note that
+ * (similar to mkdir -p behaviour) all leading paths are created with 0555 access mode, only the
+ * final dir is created with the specified directory access mode. */
+
+ for (const char *p = path;;) {
+ const char *e;
+
+ e = strchr8(p, '/');
+ if (!e)
+ break;
+
+ if (e > p) {
+ _cleanup_free_ char *t = NULL;
+
+ t = xstrndup8(path, e - path);
+ if (!t)
+ return EFI_OUT_OF_RESOURCES;
+
+ err = pack_cpio_dir(t, 0555, inode_counter, cpio_buffer, cpio_buffer_size);
+ if (err != EFI_SUCCESS)
+ return err;
+ }
+
+ p = e + 1;
+ }
+
+ return pack_cpio_dir(path, dir_mode, inode_counter, cpio_buffer, cpio_buffer_size);
+}
+
+static EFI_STATUS pack_cpio_trailer(
+ void **cpio_buffer,
+ UINTN *cpio_buffer_size) {
+
+ static const char trailer[] =
+ "070701"
+ "00000000"
+ "00000000"
+ "00000000"
+ "00000000"
+ "00000001"
+ "00000000"
+ "00000000"
+ "00000000"
+ "00000000"
+ "00000000"
+ "00000000"
+ "0000000B"
+ "00000000"
+ "TRAILER!!!\0\0\0"; /* There's a fourth NUL byte appended here, because this is a string */
+
+ /* Generates the cpio trailer record that indicates the end of our initrd cpio archive */
+
+ assert(cpio_buffer);
+ assert(cpio_buffer_size);
+ assert_cc(sizeof(trailer) % 4 == 0);
+
+ *cpio_buffer = xrealloc(*cpio_buffer, *cpio_buffer_size, *cpio_buffer_size + sizeof(trailer));
+ memcpy((uint8_t*) *cpio_buffer + *cpio_buffer_size, trailer, sizeof(trailer));
+ *cpio_buffer_size += sizeof(trailer);
+
+ return EFI_SUCCESS;
+}
+
+static EFI_STATUS measure_cpio(
+ void *buffer,
+ UINTN buffer_size,
+ const uint32_t tpm_pcr[],
+ UINTN n_tpm_pcr,
+ const char16_t *tpm_description,
+ bool *ret_measured) {
+
+ int measured = -1;
+ EFI_STATUS err;
+
+ assert(buffer || buffer_size == 0);
+ assert(tpm_pcr || n_tpm_pcr == 0);
+
+ for (UINTN i = 0; i < n_tpm_pcr; i++) {
+ bool m;
+
+ if (tpm_pcr[i] == UINT32_MAX) /* Disabled */
+ continue;
+
+ err = tpm_log_event(
+ tpm_pcr[i],
+ POINTER_TO_PHYSICAL_ADDRESS(buffer),
+ buffer_size,
+ tpm_description,
+ &m);
+ if (err != EFI_SUCCESS) {
+ log_error_stall(L"Unable to add initrd TPM measurement for PCR %u (%s), ignoring: %r", tpm_pcr[i], tpm_description, err);
+ measured = false;
+ continue;
+ }
+
+ if (measured != false)
+ measured = m;
+ }
+
+ if (ret_measured)
+ *ret_measured = measured > 0;
+
+ return EFI_SUCCESS;
+}
+
+static char16_t *get_dropin_dir(const EFI_DEVICE_PATH *file_path) {
+ if (!file_path)
+ return NULL;
+
+ /* A device path is allowed to have more than one file path node. If that is the case they are
+ * supposed to be concatenated. Unfortunately, the device path to text protocol simply converts the
+ * nodes individually and then combines those with the usual '/' for device path nodes. But this does
+ * not create a legal EFI file path that the file protocol can use. */
+
+ /* Make sure we really only got file paths. */
+ for (const EFI_DEVICE_PATH *node = file_path; !IsDevicePathEnd(node); node = NextDevicePathNode(node))
+ if (DevicePathType(node) != MEDIA_DEVICE_PATH || DevicePathSubType(node) != MEDIA_FILEPATH_DP)
+ return NULL;
+
+ _cleanup_free_ char16_t *file_path_str = NULL;
+ if (device_path_to_str(file_path, &file_path_str) != EFI_SUCCESS)
+ return NULL;
+
+ convert_efi_path(file_path_str);
+ return xpool_print(u"%s.extra.d", file_path_str);
+}
+
+EFI_STATUS pack_cpio(
+ EFI_LOADED_IMAGE_PROTOCOL *loaded_image,
+ const char16_t *dropin_dir,
+ const char16_t *match_suffix,
+ const char *target_dir_prefix,
+ uint32_t dir_mode,
+ uint32_t access_mode,
+ const uint32_t tpm_pcr[],
+ UINTN n_tpm_pcr,
+ const char16_t *tpm_description,
+ void **ret_buffer,
+ UINTN *ret_buffer_size,
+ bool *ret_measured) {
+
+ _cleanup_(file_closep) EFI_FILE *root = NULL, *extra_dir = NULL;
+ UINTN dirent_size = 0, buffer_size = 0, n_items = 0, n_allocated = 0;
+ _cleanup_free_ char16_t *rel_dropin_dir = NULL;
+ _cleanup_free_ EFI_FILE_INFO *dirent = NULL;
+ _cleanup_(strv_freep) char16_t **items = NULL;
+ _cleanup_free_ void *buffer = NULL;
+ uint32_t inode = 1; /* inode counter, so that each item gets a new inode */
+ EFI_STATUS err;
+
+ assert(loaded_image);
+ assert(target_dir_prefix);
+ assert(tpm_pcr || n_tpm_pcr == 0);
+ assert(ret_buffer);
+ assert(ret_buffer_size);
+
+ if (!loaded_image->DeviceHandle)
+ goto nothing;
+
+ err = open_volume(loaded_image->DeviceHandle, &root);
+ if (err == EFI_UNSUPPORTED)
+ /* Error will be unsupported if the bootloader doesn't implement the file system protocol on
+ * its file handles. */
+ goto nothing;
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(
+ err, L"Unable to open root directory: %r", err);
+
+ if (!dropin_dir)
+ dropin_dir = rel_dropin_dir = get_dropin_dir(loaded_image->FilePath);
+
+ err = open_directory(root, dropin_dir, &extra_dir);
+ if (err == EFI_NOT_FOUND)
+ /* No extra subdir, that's totally OK */
+ goto nothing;
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Failed to open extra directory of loaded image: %r", err);
+
+ for (;;) {
+ _cleanup_free_ char16_t *d = NULL;
+
+ err = readdir_harder(extra_dir, &dirent, &dirent_size);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Failed to read extra directory of loaded image: %r", err);
+ if (!dirent) /* End of directory */
+ break;
+
+ if (dirent->FileName[0] == '.')
+ continue;
+ if (FLAGS_SET(dirent->Attribute, EFI_FILE_DIRECTORY))
+ continue;
+ if (match_suffix && !endswith_no_case(dirent->FileName, match_suffix))
+ continue;
+ if (!is_ascii(dirent->FileName))
+ continue;
+ if (strlen16(dirent->FileName) > 255) /* Max filename size on Linux */
+ continue;
+
+ d = xstrdup16(dirent->FileName);
+
+ if (n_items+2 > n_allocated) {
+ UINTN m;
+
+ /* We allocate 16 entries at a time, as a matter of optimization */
+ if (n_items > (UINTN_MAX / sizeof(uint16_t)) - 16) /* Overflow check, just in case */
+ return log_oom();
+
+ m = n_items + 16;
+ items = xrealloc(items, n_allocated * sizeof(uint16_t *), m * sizeof(uint16_t *));
+ n_allocated = m;
+ }
+
+ items[n_items++] = TAKE_PTR(d);
+ items[n_items] = NULL; /* Let's always NUL terminate, to make freeing via strv_free() easy */
+ }
+
+ if (n_items == 0)
+ /* Empty directory */
+ goto nothing;
+
+ /* Now, sort the files we found, to make this uniform and stable (and to ensure the TPM measurements
+ * are not dependent on read order) */
+ sort_pointer_array((void**) items, n_items, (compare_pointer_func_t) strcmp16);
+
+ /* Generate the leading directory inodes right before adding the first files, to the
+ * archive. Otherwise the cpio archive cannot be unpacked, since the leading dirs won't exist. */
+ err = pack_cpio_prefix(target_dir_prefix, dir_mode, &inode, &buffer, &buffer_size);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Failed to pack cpio prefix: %r", err);
+
+ for (UINTN i = 0; i < n_items; i++) {
+ _cleanup_free_ char *content = NULL;
+ UINTN contentsize = 0; /* avoid false maybe-uninitialized warning */
+
+ err = file_read(extra_dir, items[i], 0, 0, &content, &contentsize);
+ if (err != EFI_SUCCESS) {
+ log_error_status_stall(err, L"Failed to read %s, ignoring: %r", items[i], err);
+ continue;
+ }
+
+ err = pack_cpio_one(
+ items[i],
+ content, contentsize,
+ target_dir_prefix,
+ access_mode,
+ &inode,
+ &buffer, &buffer_size);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Failed to pack cpio file %s: %r", dirent->FileName, err);
+ }
+
+ err = pack_cpio_trailer(&buffer, &buffer_size);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Failed to pack cpio trailer: %r");
+
+ err = measure_cpio(buffer, buffer_size, tpm_pcr, n_tpm_pcr, tpm_description, ret_measured);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ *ret_buffer = TAKE_PTR(buffer);
+ *ret_buffer_size = buffer_size;
+
+ return EFI_SUCCESS;
+
+nothing:
+ *ret_buffer = NULL;
+ *ret_buffer_size = 0;
+
+ if (ret_measured)
+ *ret_measured = n_tpm_pcr > 0;
+
+ return EFI_SUCCESS;
+}
+
+EFI_STATUS pack_cpio_literal(
+ const void *data,
+ size_t data_size,
+ const char *target_dir_prefix,
+ const char16_t *target_filename,
+ uint32_t dir_mode,
+ uint32_t access_mode,
+ const uint32_t tpm_pcr[],
+ UINTN n_tpm_pcr,
+ const char16_t *tpm_description,
+ void **ret_buffer,
+ UINTN *ret_buffer_size,
+ bool *ret_measured) {
+
+ uint32_t inode = 1; /* inode counter, so that each item gets a new inode */
+ _cleanup_free_ void *buffer = NULL;
+ UINTN buffer_size = 0;
+ EFI_STATUS err;
+
+ assert(data || data_size == 0);
+ assert(target_dir_prefix);
+ assert(target_filename);
+ assert(tpm_pcr || n_tpm_pcr == 0);
+ assert(ret_buffer);
+ assert(ret_buffer_size);
+
+ /* Generate the leading directory inodes right before adding the first files, to the
+ * archive. Otherwise the cpio archive cannot be unpacked, since the leading dirs won't exist. */
+
+ err = pack_cpio_prefix(target_dir_prefix, dir_mode, &inode, &buffer, &buffer_size);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Failed to pack cpio prefix: %r", err);
+
+ err = pack_cpio_one(
+ target_filename,
+ data, data_size,
+ target_dir_prefix,
+ access_mode,
+ &inode,
+ &buffer, &buffer_size);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Failed to pack cpio file %s: %r", target_filename, err);
+
+ err = pack_cpio_trailer(&buffer, &buffer_size);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Failed to pack cpio trailer: %r");
+
+ err = measure_cpio(buffer, buffer_size, tpm_pcr, n_tpm_pcr, tpm_description, ret_measured);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ *ret_buffer = TAKE_PTR(buffer);
+ *ret_buffer_size = buffer_size;
+
+ return EFI_SUCCESS;
+}
diff --git a/src/boot/efi/cpio.h b/src/boot/efi/cpio.h
new file mode 100644
index 0000000..beebef3
--- /dev/null
+++ b/src/boot/efi/cpio.h
@@ -0,0 +1,34 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <efi.h>
+#include <stdbool.h>
+#include <uchar.h>
+
+EFI_STATUS pack_cpio(
+ EFI_LOADED_IMAGE_PROTOCOL *loaded_image,
+ const char16_t *dropin_dir,
+ const char16_t *match_suffix,
+ const char *target_dir_prefix,
+ uint32_t dir_mode,
+ uint32_t access_mode,
+ const uint32_t tpm_pcr[],
+ UINTN n_tpm_pcr,
+ const char16_t *tpm_description,
+ void **ret_buffer,
+ UINTN *ret_buffer_size,
+ bool *ret_measured);
+
+EFI_STATUS pack_cpio_literal(
+ const void *data,
+ size_t data_size,
+ const char *target_dir_prefix,
+ const char16_t *target_filename,
+ uint32_t dir_mode,
+ uint32_t access_mode,
+ const uint32_t tpm_pcr[],
+ UINTN n_tpm_pcr,
+ const char16_t *tpm_description,
+ void **ret_buffer,
+ UINTN *ret_buffer_size,
+ bool *ret_measured);
diff --git a/src/boot/efi/devicetree.c b/src/boot/efi/devicetree.c
new file mode 100644
index 0000000..0312670
--- /dev/null
+++ b/src/boot/efi/devicetree.c
@@ -0,0 +1,154 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <efi.h>
+
+#include "devicetree.h"
+#include "missing_efi.h"
+#include "util.h"
+
+#define FDT_V1_SIZE (7*4)
+
+static void *get_dtb_table(void) {
+ for (UINTN i = 0; i < ST->NumberOfTableEntries; i++)
+ if (memcmp(&EfiDtbTableGuid, &ST->ConfigurationTable[i].VendorGuid, sizeof(EfiDtbTableGuid)) == 0)
+ return ST->ConfigurationTable[i].VendorTable;
+ return NULL;
+}
+
+static EFI_STATUS devicetree_allocate(struct devicetree_state *state, UINTN size) {
+ UINTN pages = DIV_ROUND_UP(size, EFI_PAGE_SIZE);
+ EFI_STATUS err;
+
+ assert(state);
+
+ err = BS->AllocatePages(AllocateAnyPages, EfiACPIReclaimMemory, pages, &state->addr);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ state->pages = pages;
+ return err;
+}
+
+static UINTN devicetree_allocated(const struct devicetree_state *state) {
+ assert(state);
+ return state->pages * EFI_PAGE_SIZE;
+}
+
+static EFI_STATUS devicetree_fixup(struct devicetree_state *state, UINTN len) {
+ EFI_DT_FIXUP_PROTOCOL *fixup;
+ UINTN size;
+ EFI_STATUS err;
+
+ assert(state);
+
+ err = BS->LocateProtocol(&EfiDtFixupProtocol, NULL, (void **) &fixup);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(EFI_SUCCESS,
+ L"Could not locate device tree fixup protocol, skipping.");
+
+ size = devicetree_allocated(state);
+ err = fixup->Fixup(fixup, PHYSICAL_ADDRESS_TO_POINTER(state->addr), &size,
+ EFI_DT_APPLY_FIXUPS | EFI_DT_RESERVE_MEMORY);
+ if (err == EFI_BUFFER_TOO_SMALL) {
+ EFI_PHYSICAL_ADDRESS oldaddr = state->addr;
+ UINTN oldpages = state->pages;
+ void *oldptr = PHYSICAL_ADDRESS_TO_POINTER(state->addr);
+
+ err = devicetree_allocate(state, size);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ memcpy(PHYSICAL_ADDRESS_TO_POINTER(state->addr), oldptr, len);
+ err = BS->FreePages(oldaddr, oldpages);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ size = devicetree_allocated(state);
+ err = fixup->Fixup(fixup, PHYSICAL_ADDRESS_TO_POINTER(state->addr), &size,
+ EFI_DT_APPLY_FIXUPS | EFI_DT_RESERVE_MEMORY);
+ }
+
+ return err;
+}
+
+EFI_STATUS devicetree_install(struct devicetree_state *state, EFI_FILE *root_dir, char16_t *name) {
+ _cleanup_(file_closep) EFI_FILE *handle = NULL;
+ _cleanup_free_ EFI_FILE_INFO *info = NULL;
+ UINTN len;
+ EFI_STATUS err;
+
+ assert(state);
+ assert(root_dir);
+ assert(name);
+
+ state->orig = get_dtb_table();
+ if (!state->orig)
+ return EFI_UNSUPPORTED;
+
+ err = root_dir->Open(root_dir, &handle, name, EFI_FILE_MODE_READ, EFI_FILE_READ_ONLY);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ err = get_file_info_harder(handle, &info, NULL);
+ if (err != EFI_SUCCESS)
+ return err;
+ if (info->FileSize < FDT_V1_SIZE || info->FileSize > 32 * 1024 * 1024)
+ /* 32MB device tree blob doesn't seem right */
+ return EFI_INVALID_PARAMETER;
+
+ len = info->FileSize;
+
+ err = devicetree_allocate(state, len);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ err = handle->Read(handle, &len, PHYSICAL_ADDRESS_TO_POINTER(state->addr));
+ if (err != EFI_SUCCESS)
+ return err;
+
+ err = devicetree_fixup(state, len);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ return BS->InstallConfigurationTable(&EfiDtbTableGuid, PHYSICAL_ADDRESS_TO_POINTER(state->addr));
+}
+
+EFI_STATUS devicetree_install_from_memory(struct devicetree_state *state,
+ const void *dtb_buffer, UINTN dtb_length) {
+
+ EFI_STATUS err;
+
+ assert(state);
+ assert(dtb_buffer && dtb_length > 0);
+
+ state->orig = get_dtb_table();
+ if (!state->orig)
+ return EFI_UNSUPPORTED;
+
+ err = devicetree_allocate(state, dtb_length);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ memcpy(PHYSICAL_ADDRESS_TO_POINTER(state->addr), dtb_buffer, dtb_length);
+
+ err = devicetree_fixup(state, dtb_length);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ return BS->InstallConfigurationTable(&EfiDtbTableGuid, PHYSICAL_ADDRESS_TO_POINTER(state->addr));
+}
+
+void devicetree_cleanup(struct devicetree_state *state) {
+ EFI_STATUS err;
+
+ if (!state->pages)
+ return;
+
+ err = BS->InstallConfigurationTable(&EfiDtbTableGuid, state->orig);
+ /* don't free the current device tree if we can't reinstate the old one */
+ if (err != EFI_SUCCESS)
+ return;
+
+ BS->FreePages(state->addr, state->pages);
+ state->pages = 0;
+}
diff --git a/src/boot/efi/devicetree.h b/src/boot/efi/devicetree.h
new file mode 100644
index 0000000..d512cb5
--- /dev/null
+++ b/src/boot/efi/devicetree.h
@@ -0,0 +1,16 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <efi.h>
+#include <uchar.h>
+
+struct devicetree_state {
+ EFI_PHYSICAL_ADDRESS addr;
+ UINTN pages;
+ void *orig;
+};
+
+EFI_STATUS devicetree_install(struct devicetree_state *state, EFI_FILE *root_dir, char16_t *name);
+EFI_STATUS devicetree_install_from_memory(
+ struct devicetree_state *state, const VOID *dtb_buffer, UINTN dtb_length);
+void devicetree_cleanup(struct devicetree_state *state);
diff --git a/src/boot/efi/disk.c b/src/boot/efi/disk.c
new file mode 100644
index 0000000..5246626
--- /dev/null
+++ b/src/boot/efi/disk.c
@@ -0,0 +1,40 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <efi.h>
+#include <efilib.h>
+
+#include "disk.h"
+#include "util.h"
+
+EFI_STATUS disk_get_part_uuid(EFI_HANDLE *handle, char16_t uuid[static 37]) {
+ EFI_STATUS err;
+ EFI_DEVICE_PATH *dp;
+
+ /* export the device path this image is started from */
+
+ if (!handle)
+ return EFI_NOT_FOUND;
+
+ err = BS->HandleProtocol(handle, &DevicePathProtocol, (void **) &dp);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ for (; !IsDevicePathEnd(dp); dp = NextDevicePathNode(dp)) {
+ if (DevicePathType(dp) != MEDIA_DEVICE_PATH)
+ continue;
+ if (DevicePathSubType(dp) != MEDIA_HARDDRIVE_DP)
+ continue;
+
+ /* The HD device path may be misaligned. */
+ HARDDRIVE_DEVICE_PATH hd;
+ memcpy(&hd, dp, MIN(sizeof(hd), (size_t) DevicePathNodeLength(dp)));
+
+ if (hd.SignatureType != SIGNATURE_TYPE_GUID)
+ continue;
+
+ GuidToString(uuid, (EFI_GUID *) &hd.Signature);
+ return EFI_SUCCESS;
+ }
+
+ return EFI_NOT_FOUND;
+}
diff --git a/src/boot/efi/disk.h b/src/boot/efi/disk.h
new file mode 100644
index 0000000..1a5a187
--- /dev/null
+++ b/src/boot/efi/disk.h
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <efi.h>
+#include <uchar.h>
+
+EFI_STATUS disk_get_part_uuid(EFI_HANDLE *handle, char16_t uuid[static 37]);
diff --git a/src/boot/efi/drivers.c b/src/boot/efi/drivers.c
new file mode 100644
index 0000000..7f2057f
--- /dev/null
+++ b/src/boot/efi/drivers.c
@@ -0,0 +1,117 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <efi.h>
+#include <efilib.h>
+
+#include "drivers.h"
+#include "util.h"
+
+static EFI_STATUS load_one_driver(
+ EFI_HANDLE parent_image,
+ EFI_LOADED_IMAGE_PROTOCOL *loaded_image,
+ const char16_t *fname) {
+
+ _cleanup_(unload_imagep) EFI_HANDLE image = NULL;
+ _cleanup_free_ EFI_DEVICE_PATH *path = NULL;
+ _cleanup_free_ char16_t *spath = NULL;
+ EFI_STATUS err;
+
+ assert(parent_image);
+ assert(loaded_image);
+ assert(fname);
+
+ spath = xpool_print(L"\\EFI\\systemd\\drivers\\%s", fname);
+ err = make_file_device_path(loaded_image->DeviceHandle, spath, &path);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Error making file device path: %r", err);
+
+ err = BS->LoadImage(false, parent_image, path, NULL, 0, &image);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Failed to load image %s: %r", fname, err);
+
+ err = BS->HandleProtocol(image, &LoadedImageProtocol, (void **)&loaded_image);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Failed to find protocol in driver image %s: %r", fname, err);
+
+ if (loaded_image->ImageCodeType != EfiBootServicesCode &&
+ loaded_image->ImageCodeType != EfiRuntimeServicesCode)
+ return log_error_status_stall(EFI_INVALID_PARAMETER, L"Image %s is not a driver, refusing.", fname);
+
+ err = BS->StartImage(image, NULL, NULL);
+ if (err != EFI_SUCCESS) {
+ /* EFI_ABORTED signals an initializing driver. It uses this error code on success
+ * so that it is unloaded after. */
+ if (err != EFI_ABORTED)
+ log_error_stall(L"Failed to start image %s: %r", fname, err);
+ return err;
+ }
+
+ TAKE_PTR(image);
+ return EFI_SUCCESS;
+}
+
+EFI_STATUS reconnect_all_drivers(void) {
+ _cleanup_free_ EFI_HANDLE *handles = NULL;
+ size_t n_handles = 0;
+ EFI_STATUS err;
+
+ /* Reconnects all handles, so that any loaded drivers can take effect. */
+
+ err = BS->LocateHandleBuffer(AllHandles, NULL, NULL, &n_handles, &handles);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Failed to get list of handles: %r", err);
+
+ for (size_t i = 0; i < n_handles; i++)
+ /* Some firmware gives us some bogus handles (or they might become bad due to
+ * reconnecting everything). Security policy may also prevent us from doing so too.
+ * There is nothing we can realistically do on errors anyways, so just ignore them. */
+ (void) BS->ConnectController(handles[i], NULL, NULL, true);
+
+ return EFI_SUCCESS;
+}
+
+EFI_STATUS load_drivers(
+ EFI_HANDLE parent_image,
+ EFI_LOADED_IMAGE_PROTOCOL *loaded_image,
+ EFI_FILE *root_dir) {
+
+ _cleanup_(file_closep) EFI_FILE *drivers_dir = NULL;
+ _cleanup_free_ EFI_FILE_INFO *dirent = NULL;
+ UINTN dirent_size = 0, n_succeeded = 0;
+ EFI_STATUS err;
+
+ err = open_directory(
+ root_dir,
+ L"\\EFI\\systemd\\drivers",
+ &drivers_dir);
+ if (err == EFI_NOT_FOUND)
+ return EFI_SUCCESS;
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Failed to open \\EFI\\systemd\\drivers: %r", err);
+
+ for (;;) {
+ err = readdir_harder(drivers_dir, &dirent, &dirent_size);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Failed to read extra directory of loaded image: %r", err);
+ if (!dirent) /* End of directory */
+ break;
+
+ if (dirent->FileName[0] == '.')
+ continue;
+ if (FLAGS_SET(dirent->Attribute, EFI_FILE_DIRECTORY))
+ continue;
+ if (!endswith_no_case(dirent->FileName, EFI_MACHINE_TYPE_NAME L".efi"))
+ continue;
+
+ err = load_one_driver(parent_image, loaded_image, dirent->FileName);
+ if (err != EFI_SUCCESS)
+ continue;
+
+ n_succeeded++;
+ }
+
+ if (n_succeeded > 0)
+ (void) reconnect_all_drivers();
+
+ return EFI_SUCCESS;
+}
diff --git a/src/boot/efi/drivers.h b/src/boot/efi/drivers.h
new file mode 100644
index 0000000..4ad526e
--- /dev/null
+++ b/src/boot/efi/drivers.h
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <efi.h>
+
+EFI_STATUS reconnect_all_drivers(void);
+EFI_STATUS load_drivers(
+ EFI_HANDLE parent_image,
+ EFI_LOADED_IMAGE_PROTOCOL *loaded_image,
+ EFI_FILE *root_dir);
diff --git a/src/boot/efi/efi-string.c b/src/boot/efi/efi-string.c
new file mode 100644
index 0000000..2ba1567
--- /dev/null
+++ b/src/boot/efi/efi-string.c
@@ -0,0 +1,460 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "efi-string.h"
+
+#ifdef SD_BOOT
+# include "util.h"
+#else
+# include <stdlib.h>
+# include "alloc-util.h"
+# define xnew(t, n) ASSERT_SE_PTR(new(t, n))
+# define xmalloc(n) ASSERT_SE_PTR(malloc(n))
+#endif
+
+/* String functions for both char and char16_t that should behave the same way as their respective
+ * counterpart in userspace. Where it makes sense, these accept NULL and do something sensible whereas
+ * userspace does not allow for this (strlen8(NULL) returns 0 like strlen_ptr(NULL) for example). To make it
+ * easier to tell in code which kind of string they work on, we use 8/16 suffixes. This also makes is easier
+ * to unit test them. */
+
+#define DEFINE_STRNLEN(type, name) \
+ size_t name(const type *s, size_t n) { \
+ if (!s) \
+ return 0; \
+ \
+ size_t len = 0; \
+ while (len < n && *s) { \
+ s++; \
+ len++; \
+ } \
+ \
+ return len; \
+ }
+
+DEFINE_STRNLEN(char, strnlen8);
+DEFINE_STRNLEN(char16_t, strnlen16);
+
+#define TOLOWER(c) \
+ ({ \
+ typeof(c) _c = (c); \
+ (_c >= 'A' && _c <= 'Z') ? _c + ('a' - 'A') : _c; \
+ })
+
+#define DEFINE_STRTOLOWER(type, name) \
+ void name(type *s) { \
+ if (!s) \
+ return; \
+ for (; *s; s++) \
+ *s = TOLOWER(*s); \
+ }
+
+DEFINE_STRTOLOWER(char, strtolower8);
+DEFINE_STRTOLOWER(char16_t, strtolower16);
+
+#define DEFINE_STRNCASECMP(type, name, tolower) \
+ int name(const type *s1, const type *s2, size_t n) { \
+ if (!s1 || !s2) \
+ return CMP(s1, s2); \
+ \
+ while (n > 0) { \
+ type c1 = *s1, c2 = *s2; \
+ if (tolower) { \
+ c1 = TOLOWER(c1); \
+ c2 = TOLOWER(c2); \
+ } \
+ if (!c1 || c1 != c2) \
+ return CMP(c1, c2); \
+ \
+ s1++; \
+ s2++; \
+ n--; \
+ } \
+ \
+ return 0; \
+ }
+
+DEFINE_STRNCASECMP(char, strncmp8, false);
+DEFINE_STRNCASECMP(char16_t, strncmp16, false);
+DEFINE_STRNCASECMP(char, strncasecmp8, true);
+DEFINE_STRNCASECMP(char16_t, strncasecmp16, true);
+
+#define DEFINE_STRCPY(type, name) \
+ type *name(type * restrict dest, const type * restrict src) { \
+ type *ret = ASSERT_PTR(dest); \
+ \
+ if (!src) { \
+ *dest = '\0'; \
+ return ret; \
+ } \
+ \
+ while (*src) { \
+ *dest = *src; \
+ dest++; \
+ src++; \
+ } \
+ \
+ *dest = '\0'; \
+ return ret; \
+ }
+
+DEFINE_STRCPY(char, strcpy8);
+DEFINE_STRCPY(char16_t, strcpy16);
+
+#define DEFINE_STRCHR(type, name) \
+ type *name(const type *s, type c) { \
+ if (!s) \
+ return NULL; \
+ \
+ while (*s) { \
+ if (*s == c) \
+ return (type *) s; \
+ s++; \
+ } \
+ \
+ return NULL; \
+ }
+
+DEFINE_STRCHR(char, strchr8);
+DEFINE_STRCHR(char16_t, strchr16);
+
+#define DEFINE_STRNDUP(type, name, len_func) \
+ type *name(const type *s, size_t n) { \
+ if (!s) \
+ return NULL; \
+ \
+ size_t len = len_func(s, n); \
+ size_t size = len * sizeof(type); \
+ \
+ type *dup = xmalloc(size + sizeof(type)); \
+ if (size > 0) \
+ memcpy(dup, s, size); \
+ dup[len] = '\0'; \
+ \
+ return dup; \
+ }
+
+DEFINE_STRNDUP(char, xstrndup8, strnlen8);
+DEFINE_STRNDUP(char16_t, xstrndup16, strnlen16);
+
+static unsigned utf8_to_unichar(const char *utf8, size_t n, char32_t *c) {
+ char32_t unichar;
+ unsigned len;
+
+ assert(utf8);
+ assert(c);
+
+ if (!(utf8[0] & 0x80)) {
+ *c = utf8[0];
+ return 1;
+ } else if ((utf8[0] & 0xe0) == 0xc0) {
+ len = 2;
+ unichar = utf8[0] & 0x1f;
+ } else if ((utf8[0] & 0xf0) == 0xe0) {
+ len = 3;
+ unichar = utf8[0] & 0x0f;
+ } else if ((utf8[0] & 0xf8) == 0xf0) {
+ len = 4;
+ unichar = utf8[0] & 0x07;
+ } else if ((utf8[0] & 0xfc) == 0xf8) {
+ len = 5;
+ unichar = utf8[0] & 0x03;
+ } else if ((utf8[0] & 0xfe) == 0xfc) {
+ len = 6;
+ unichar = utf8[0] & 0x01;
+ } else {
+ *c = UINT32_MAX;
+ return 1;
+ }
+
+ if (len > n) {
+ *c = UINT32_MAX;
+ return len;
+ }
+
+ for (unsigned i = 1; i < len; i++) {
+ if ((utf8[i] & 0xc0) != 0x80) {
+ *c = UINT32_MAX;
+ return len;
+ }
+ unichar <<= 6;
+ unichar |= utf8[i] & 0x3f;
+ }
+
+ *c = unichar;
+ return len;
+}
+
+/* Convert UTF-8 to UCS-2, skipping any invalid or short byte sequences. */
+char16_t *xstrn8_to_16(const char *str8, size_t n) {
+ if (!str8 || n == 0)
+ return NULL;
+
+ size_t i = 0;
+ char16_t *str16 = xnew(char16_t, n + 1);
+
+ while (n > 0 && *str8 != '\0') {
+ char32_t unichar;
+
+ size_t utf8len = utf8_to_unichar(str8, n, &unichar);
+ str8 += utf8len;
+ n = LESS_BY(n, utf8len);
+
+ switch (unichar) {
+ case 0 ... 0xd7ffU:
+ case 0xe000U ... 0xffffU:
+ str16[i++] = unichar;
+ break;
+ }
+ }
+
+ str16[i] = '\0';
+ return str16;
+}
+
+static bool efi_fnmatch_prefix(const char16_t *p, const char16_t *h, const char16_t **ret_p, const char16_t **ret_h) {
+ assert(p);
+ assert(h);
+ assert(ret_p);
+ assert(ret_h);
+
+ for (;; p++, h++)
+ switch (*p) {
+ case '\0':
+ /* End of pattern. Check that haystack is now empty. */
+ return *h == '\0';
+
+ case '\\':
+ p++;
+ if (*p == '\0' || *p != *h)
+ /* Trailing escape or no match. */
+ return false;
+ break;
+
+ case '?':
+ if (*h == '\0')
+ /* Early end of haystack. */
+ return false;
+ break;
+
+ case '*':
+ /* Point ret_p at the remainder of the pattern. */
+ while (*p == '*')
+ p++;
+ *ret_p = p;
+ *ret_h = h;
+ return true;
+
+ case '[':
+ if (*h == '\0')
+ /* Early end of haystack. */
+ return false;
+
+ bool first = true, can_range = true, match = false;
+ for (;; first = false) {
+ p++;
+ if (*p == '\0')
+ return false;
+
+ if (*p == '\\') {
+ p++;
+ if (*p == '\0')
+ return false;
+ if (*p == *h)
+ match = true;
+ can_range = true;
+ continue;
+ }
+
+ /* End of set unless it's the first char. */
+ if (*p == ']' && !first)
+ break;
+
+ /* Range pattern if '-' is not first or last in set. */
+ if (*p == '-' && can_range && !first && *(p + 1) != ']') {
+ char16_t low = *(p - 1);
+ p++;
+ if (*p == '\\')
+ p++;
+ if (*p == '\0')
+ return false;
+
+ if (low <= *h && *h <= *p)
+ match = true;
+
+ /* Ranges cannot be chained: [a-c-f] == [-abcf] */
+ can_range = false;
+ continue;
+ }
+
+ if (*p == *h)
+ match = true;
+ can_range = true;
+ }
+
+ if (!match)
+ return false;
+ break;
+
+ default:
+ if (*p != *h)
+ /* Single char mismatch. */
+ return false;
+ }
+}
+
+/* Patterns are fnmatch-compatible (with reduced feature support). */
+bool efi_fnmatch(const char16_t *pattern, const char16_t *haystack) {
+ /* Patterns can be considered as simple patterns (without '*') concatenated by '*'. By doing so we
+ * simply have to make sure the very first simple pattern matches the start of haystack. Then we just
+ * look for the remaining simple patterns *somewhere* within the haystack (in order) as any extra
+ * characters in between would be matches by the '*'. We then only have to ensure that the very last
+ * simple pattern matches at the actual end of the haystack.
+ *
+ * This means we do not need to use backtracking which could have catastrophic runtimes with the
+ * right input data. */
+
+ for (bool first = true;;) {
+ const char16_t *pattern_tail = NULL, *haystack_tail = NULL;
+ bool match = efi_fnmatch_prefix(pattern, haystack, &pattern_tail, &haystack_tail);
+ if (first) {
+ if (!match)
+ /* Initial simple pattern must match. */
+ return false;
+ if (!pattern_tail)
+ /* No '*' was in pattern, we can return early. */
+ return true;
+ first = false;
+ }
+
+ if (pattern_tail) {
+ assert(match);
+ pattern = pattern_tail;
+ haystack = haystack_tail;
+ } else {
+ /* If we have a match this must be at the end of the haystack. Note that
+ * efi_fnmatch_prefix compares the NUL-bytes at the end, so we cannot match the end
+ * of pattern in the middle of haystack). */
+ if (match || *haystack == '\0')
+ return match;
+
+ /* Match one character using '*'. */
+ haystack++;
+ }
+ }
+}
+
+#define DEFINE_PARSE_NUMBER(type, name) \
+ bool name(const type *s, uint64_t *ret_u, const type **ret_tail) { \
+ assert(ret_u); \
+ \
+ if (!s) \
+ return false; \
+ \
+ /* Need at least one digit. */ \
+ if (*s < '0' || *s > '9') \
+ return false; \
+ \
+ uint64_t u = 0; \
+ while (*s >= '0' && *s <= '9') { \
+ if (__builtin_mul_overflow(u, 10, &u)) \
+ return false; \
+ if (__builtin_add_overflow(u, *s - '0', &u)) \
+ return false; \
+ s++; \
+ } \
+ \
+ if (!ret_tail && *s != '\0') \
+ return false; \
+ \
+ *ret_u = u; \
+ if (ret_tail) \
+ *ret_tail = s; \
+ return true; \
+ }
+
+DEFINE_PARSE_NUMBER(char, parse_number8);
+DEFINE_PARSE_NUMBER(char16_t, parse_number16);
+
+#ifdef SD_BOOT
+/* To provide the actual implementation for these we need to remove the redirection to the builtins. */
+# undef memcmp
+# undef memcpy
+# undef memset
+#else
+/* And for userspace unit testing we need to give them an efi_ prefix. */
+# define memcmp efi_memcmp
+# define memcpy efi_memcpy
+# define memset efi_memset
+#endif
+
+_used_ int memcmp(const void *p1, const void *p2, size_t n) {
+ const uint8_t *up1 = p1, *up2 = p2;
+ int r;
+
+ if (!p1 || !p2)
+ return CMP(p1, p2);
+
+ while (n > 0) {
+ r = CMP(*up1, *up2);
+ if (r != 0)
+ return r;
+
+ up1++;
+ up2++;
+ n--;
+ }
+
+ return 0;
+}
+
+_used_ _weak_ void *memcpy(void * restrict dest, const void * restrict src, size_t n) {
+ if (!dest || !src || n == 0)
+ return dest;
+
+#ifdef SD_BOOT
+ /* The firmware-provided memcpy is likely optimized, so use that. The function is guaranteed to be
+ * available by the UEFI spec. We still make it depend on the boot services pointer being set just in
+ * case the compiler emits a call before it is available. */
+ if (_likely_(BS)) {
+ BS->CopyMem(dest, (void *) src, n);
+ return dest;
+ }
+#endif
+
+ uint8_t *d = dest;
+ const uint8_t *s = src;
+
+ while (n > 0) {
+ *d = *s;
+ d++;
+ s++;
+ n--;
+ }
+
+ return dest;
+}
+
+_used_ _weak_ void *memset(void *p, int c, size_t n) {
+ if (!p || n == 0)
+ return p;
+
+#ifdef SD_BOOT
+ /* See comment in efi_memcpy. Note that the signature has c and n swapped! */
+ if (_likely_(BS)) {
+ BS->SetMem(p, n, c);
+ return p;
+ }
+#endif
+
+ uint8_t *q = p;
+ while (n > 0) {
+ *q = c;
+ q++;
+ n--;
+ }
+
+ return p;
+}
diff --git a/src/boot/efi/efi-string.h b/src/boot/efi/efi-string.h
new file mode 100644
index 0000000..9b2a9ad
--- /dev/null
+++ b/src/boot/efi/efi-string.h
@@ -0,0 +1,132 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <uchar.h>
+
+#include "macro-fundamental.h"
+
+size_t strnlen8(const char *s, size_t n);
+size_t strnlen16(const char16_t *s, size_t n);
+
+static inline size_t strlen8(const char *s) {
+ return strnlen8(s, SIZE_MAX);
+}
+
+static inline size_t strlen16(const char16_t *s) {
+ return strnlen16(s, SIZE_MAX);
+}
+
+static inline size_t strsize8(const char *s) {
+ return s ? (strlen8(s) + 1) * sizeof(*s) : 0;
+}
+
+static inline size_t strsize16(const char16_t *s) {
+ return s ? (strlen16(s) + 1) * sizeof(*s) : 0;
+}
+
+void strtolower8(char *s);
+void strtolower16(char16_t *s);
+
+int strncmp8(const char *s1, const char *s2, size_t n);
+int strncmp16(const char16_t *s1, const char16_t *s2, size_t n);
+int strncasecmp8(const char *s1, const char *s2, size_t n);
+int strncasecmp16(const char16_t *s1, const char16_t *s2, size_t n);
+
+static inline int strcmp8(const char *s1, const char *s2) {
+ return strncmp8(s1, s2, SIZE_MAX);
+}
+
+static inline int strcmp16(const char16_t *s1, const char16_t *s2) {
+ return strncmp16(s1, s2, SIZE_MAX);
+}
+
+static inline int strcasecmp8(const char *s1, const char *s2) {
+ return strncasecmp8(s1, s2, SIZE_MAX);
+}
+
+static inline int strcasecmp16(const char16_t *s1, const char16_t *s2) {
+ return strncasecmp16(s1, s2, SIZE_MAX);
+}
+
+static inline bool strneq8(const char *s1, const char *s2, size_t n) {
+ return strncmp8(s1, s2, n) == 0;
+}
+
+static inline bool strneq16(const char16_t *s1, const char16_t *s2, size_t n) {
+ return strncmp16(s1, s2, n) == 0;
+}
+
+static inline bool streq8(const char *s1, const char *s2) {
+ return strcmp8(s1, s2) == 0;
+}
+
+static inline bool streq16(const char16_t *s1, const char16_t *s2) {
+ return strcmp16(s1, s2) == 0;
+}
+
+static inline int strncaseeq8(const char *s1, const char *s2, size_t n) {
+ return strncasecmp8(s1, s2, n) == 0;
+}
+
+static inline int strncaseeq16(const char16_t *s1, const char16_t *s2, size_t n) {
+ return strncasecmp16(s1, s2, n) == 0;
+}
+
+static inline bool strcaseeq8(const char *s1, const char *s2) {
+ return strcasecmp8(s1, s2) == 0;
+}
+
+static inline bool strcaseeq16(const char16_t *s1, const char16_t *s2) {
+ return strcasecmp16(s1, s2) == 0;
+}
+
+char *strcpy8(char * restrict dest, const char * restrict src);
+char16_t *strcpy16(char16_t * restrict dest, const char16_t * restrict src);
+
+char *strchr8(const char *s, char c);
+char16_t *strchr16(const char16_t *s, char16_t c);
+
+char *xstrndup8(const char *s, size_t n);
+char16_t *xstrndup16(const char16_t *s, size_t n);
+
+static inline char *xstrdup8(const char *s) {
+ return xstrndup8(s, SIZE_MAX);
+}
+
+static inline char16_t *xstrdup16(const char16_t *s) {
+ return xstrndup16(s, SIZE_MAX);
+}
+
+char16_t *xstrn8_to_16(const char *str8, size_t n);
+static inline char16_t *xstr8_to_16(const char *str8) {
+ return xstrn8_to_16(str8, strlen8(str8));
+}
+
+bool efi_fnmatch(const char16_t *pattern, const char16_t *haystack);
+
+bool parse_number8(const char *s, uint64_t *ret_u, const char **ret_tail);
+bool parse_number16(const char16_t *s, uint64_t *ret_u, const char16_t **ret_tail);
+
+#ifdef SD_BOOT
+/* The compiler normally has knowledge about standard functions such as memcmp, but this is not the case when
+ * compiling with -ffreestanding. By referring to builtins, the compiler can check arguments and do
+ * optimizations again. Note that we still need to provide implementations as the compiler is free to not
+ * inline its own implementation and instead issue a library call. */
+# define memcmp __builtin_memcmp
+# define memcpy __builtin_memcpy
+# define memset __builtin_memset
+
+static inline void *mempcpy(void * restrict dest, const void * restrict src, size_t n) {
+ if (!dest || !src || n == 0)
+ return dest;
+ memcpy(dest, src, n);
+ return (uint8_t *) dest + n;
+}
+#else
+/* For unit testing. */
+int efi_memcmp(const void *p1, const void *p2, size_t n);
+void *efi_memcpy(void * restrict dest, const void * restrict src, size_t n);
+void *efi_memset(void *p, int c, size_t n);
+#endif
diff --git a/src/boot/efi/fuzz-bcd.c b/src/boot/efi/fuzz-bcd.c
new file mode 100644
index 0000000..297b71f
--- /dev/null
+++ b/src/boot/efi/fuzz-bcd.c
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "bcd.h"
+#include "fuzz.h"
+#include "utf8.h"
+
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+ _cleanup_free_ void *p = NULL;
+
+ /* This limit was borrowed from src/boot/efi/boot.c */
+ if (outside_size_range(size, 0, 100*1024))
+ return 0;
+
+ p = memdup(data, size);
+ assert_se(p);
+
+ char16_t *title = get_bcd_title(p, size);
+ /* If we get something, it must be NUL-terminated, but an empty string is still valid! */
+ DO_NOT_OPTIMIZE(title && char16_strlen(title));
+ return 0;
+}
diff --git a/src/boot/efi/fuzz-efi-string.c b/src/boot/efi/fuzz-efi-string.c
new file mode 100644
index 0000000..3c0f0f3
--- /dev/null
+++ b/src/boot/efi/fuzz-efi-string.c
@@ -0,0 +1,40 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "efi-string.h"
+#include "fuzz.h"
+#include "utf8.h"
+
+static char16_t *memdup_str16(const uint8_t *data, size_t size) {
+ char16_t *ret = memdup(data, size);
+ assert_se(ret);
+ ret[size / sizeof(char16_t) - 1] = '\0';
+ return ret;
+}
+
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+ if (outside_size_range(size, sizeof(size_t), 64 * 1024))
+ return 0;
+
+ size_t len, len2;
+ memcpy(&len, data, sizeof(len));
+ data += sizeof(len);
+ size -= sizeof(len);
+
+ len2 = size - len;
+ if (len > size || len < sizeof(char16_t) || len2 < sizeof(char16_t))
+ return 0;
+
+ const char *tail8 = NULL;
+ _cleanup_free_ char *str8 = ASSERT_SE_PTR(memdup_suffix0(data, size));
+ DO_NOT_OPTIMIZE(parse_number8(str8, &(uint64_t){ 0 }, size % 2 == 0 ? NULL : &tail8));
+
+ const char16_t *tail16 = NULL;
+ _cleanup_free_ char16_t *str16 = memdup_str16(data, size);
+ DO_NOT_OPTIMIZE(parse_number16(str16, &(uint64_t){ 0 }, size % 2 == 0 ? NULL : &tail16));
+
+ _cleanup_free_ char16_t *pattern = memdup_str16(data, len), *haystack = memdup_str16(data + len, len2);
+ DO_NOT_OPTIMIZE(efi_fnmatch(pattern, haystack));
+
+ return 0;
+}
diff --git a/src/boot/efi/graphics.c b/src/boot/efi/graphics.c
new file mode 100644
index 0000000..dc646bc
--- /dev/null
+++ b/src/boot/efi/graphics.c
@@ -0,0 +1,43 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright © 2013 Intel Corporation
+ * Authored by Joonas Lahtinen <joonas.lahtinen@linux.intel.com>
+ */
+
+#include <efi.h>
+#include <efilib.h>
+
+#include "graphics.h"
+#include "missing_efi.h"
+#include "util.h"
+
+EFI_STATUS graphics_mode(bool on) {
+ EFI_CONSOLE_CONTROL_PROTOCOL *ConsoleControl = NULL;
+ EFI_CONSOLE_CONTROL_SCREEN_MODE new;
+ EFI_CONSOLE_CONTROL_SCREEN_MODE current;
+ BOOLEAN uga_exists;
+ BOOLEAN stdin_locked;
+ EFI_STATUS err;
+
+ err = BS->LocateProtocol((EFI_GUID *) EFI_CONSOLE_CONTROL_GUID, NULL, (void **) &ConsoleControl);
+ if (err != EFI_SUCCESS)
+ /* console control protocol is nonstandard and might not exist. */
+ return err == EFI_NOT_FOUND ? EFI_SUCCESS : err;
+
+ /* check current mode */
+ err =ConsoleControl->GetMode(ConsoleControl, &current, &uga_exists, &stdin_locked);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ /* do not touch the mode */
+ new = on ? EfiConsoleControlScreenGraphics : EfiConsoleControlScreenText;
+ if (new == current)
+ return EFI_SUCCESS;
+
+ err =ConsoleControl->SetMode(ConsoleControl, new);
+
+ /* some firmware enables the cursor when switching modes */
+ ST->ConOut->EnableCursor(ST->ConOut, false);
+
+ return err;
+}
diff --git a/src/boot/efi/graphics.h b/src/boot/efi/graphics.h
new file mode 100644
index 0000000..9dadd39
--- /dev/null
+++ b/src/boot/efi/graphics.h
@@ -0,0 +1,11 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright © 2013 Intel Corporation
+ * Authored by Joonas Lahtinen <joonas.lahtinen@linux.intel.com>
+ */
+#pragma once
+
+#include <efi.h>
+#include <stdbool.h>
+
+EFI_STATUS graphics_mode(bool on);
diff --git a/src/boot/efi/initrd.c b/src/boot/efi/initrd.c
new file mode 100644
index 0000000..d994ef8
--- /dev/null
+++ b/src/boot/efi/initrd.c
@@ -0,0 +1,140 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <efi.h>
+#include <efilib.h>
+
+#include "initrd.h"
+#include "macro-fundamental.h"
+#include "missing_efi.h"
+#include "util.h"
+
+/* extend LoadFileProtocol */
+struct initrd_loader {
+ EFI_LOAD_FILE_PROTOCOL load_file;
+ const void *address;
+ UINTN length;
+};
+
+/* static structure for LINUX_INITRD_MEDIA device path
+ see https://github.com/torvalds/linux/blob/v5.13/drivers/firmware/efi/libstub/efi-stub-helper.c
+ */
+static const struct {
+ VENDOR_DEVICE_PATH vendor;
+ EFI_DEVICE_PATH end;
+} _packed_ efi_initrd_device_path = {
+ .vendor = {
+ .Header = {
+ .Type = MEDIA_DEVICE_PATH,
+ .SubType = MEDIA_VENDOR_DP,
+ .Length = { sizeof(efi_initrd_device_path.vendor), 0 }
+ },
+ .Guid = LINUX_INITRD_MEDIA_GUID
+ },
+ .end = {
+ .Type = END_DEVICE_PATH_TYPE,
+ .SubType = END_ENTIRE_DEVICE_PATH_SUBTYPE,
+ .Length = { sizeof(efi_initrd_device_path.end), 0 }
+ }
+};
+
+static EFIAPI EFI_STATUS initrd_load_file(
+ EFI_LOAD_FILE_PROTOCOL *this,
+ EFI_DEVICE_PATH *file_path,
+ BOOLEAN boot_policy,
+ UINTN *buffer_size,
+ void *buffer) {
+
+ struct initrd_loader *loader;
+
+ if (!this || !buffer_size || !file_path)
+ return EFI_INVALID_PARAMETER;
+ if (boot_policy)
+ return EFI_UNSUPPORTED;
+
+ loader = (struct initrd_loader *) this;
+
+ if (loader->length == 0 || !loader->address)
+ return EFI_NOT_FOUND;
+
+ if (!buffer || *buffer_size < loader->length) {
+ *buffer_size = loader->length;
+ return EFI_BUFFER_TOO_SMALL;
+ }
+
+ memcpy(buffer, loader->address, loader->length);
+ *buffer_size = loader->length;
+ return EFI_SUCCESS;
+}
+
+EFI_STATUS initrd_register(
+ const void *initrd_address,
+ UINTN initrd_length,
+ EFI_HANDLE *ret_initrd_handle) {
+
+ EFI_STATUS err;
+ EFI_DEVICE_PATH *dp = (EFI_DEVICE_PATH *) &efi_initrd_device_path;
+ EFI_HANDLE handle;
+ struct initrd_loader *loader;
+
+ assert(ret_initrd_handle);
+
+ if (!initrd_address || initrd_length == 0)
+ return EFI_SUCCESS;
+
+ /* check if a LINUX_INITRD_MEDIA_GUID DevicePath is already registered.
+ LocateDevicePath checks for the "closest DevicePath" and returns its handle,
+ where as InstallMultipleProtocolInterfaces only matches identical DevicePaths.
+ */
+ err = BS->LocateDevicePath(&EfiLoadFile2Protocol, &dp, &handle);
+ if (err != EFI_NOT_FOUND) /* InitrdMedia is already registered */
+ return EFI_ALREADY_STARTED;
+
+ loader = xnew(struct initrd_loader, 1);
+ *loader = (struct initrd_loader) {
+ .load_file.LoadFile = initrd_load_file,
+ .address = initrd_address,
+ .length = initrd_length
+ };
+
+ /* create a new handle and register the LoadFile2 protocol with the InitrdMediaPath on it */
+ err = BS->InstallMultipleProtocolInterfaces(
+ ret_initrd_handle,
+ &DevicePathProtocol, &efi_initrd_device_path,
+ &EfiLoadFile2Protocol, loader,
+ NULL);
+ if (err != EFI_SUCCESS)
+ free(loader);
+
+ return err;
+}
+
+EFI_STATUS initrd_unregister(EFI_HANDLE initrd_handle) {
+ EFI_STATUS err;
+ struct initrd_loader *loader;
+
+ if (!initrd_handle)
+ return EFI_SUCCESS;
+
+ /* get the LoadFile2 protocol that we allocated earlier */
+ err = BS->OpenProtocol(
+ initrd_handle, &EfiLoadFile2Protocol, (void **) &loader,
+ NULL, NULL, EFI_OPEN_PROTOCOL_GET_PROTOCOL);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ /* close the handle */
+ (void) BS->CloseProtocol(initrd_handle, &EfiLoadFile2Protocol, NULL, NULL);
+
+ /* uninstall all protocols thus destroying the handle */
+ err = BS->UninstallMultipleProtocolInterfaces(
+ initrd_handle,
+ &DevicePathProtocol, &efi_initrd_device_path,
+ &EfiLoadFile2Protocol, loader,
+ NULL);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ initrd_handle = NULL;
+ free(loader);
+ return EFI_SUCCESS;
+}
diff --git a/src/boot/efi/initrd.h b/src/boot/efi/initrd.h
new file mode 100644
index 0000000..d1478e3
--- /dev/null
+++ b/src/boot/efi/initrd.h
@@ -0,0 +1,16 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <efi.h>
+
+EFI_STATUS initrd_register(
+ const void *initrd_address,
+ UINTN initrd_length,
+ EFI_HANDLE *ret_initrd_handle);
+
+EFI_STATUS initrd_unregister(EFI_HANDLE initrd_handle);
+
+static inline void cleanup_initrd(EFI_HANDLE *initrd_handle) {
+ (void) initrd_unregister(*initrd_handle);
+ *initrd_handle = NULL;
+}
diff --git a/src/boot/efi/linux.c b/src/boot/efi/linux.c
new file mode 100644
index 0000000..48801f9
--- /dev/null
+++ b/src/boot/efi/linux.c
@@ -0,0 +1,155 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+/*
+ * Generic Linux boot protocol using the EFI/PE entry point of the kernel. Passes
+ * initrd with the LINUX_INITRD_MEDIA_GUID DevicePath and cmdline with
+ * EFI LoadedImageProtocol.
+ *
+ * This method works for Linux 5.8 and newer on ARM/Aarch64, x86/x68_64 and RISC-V.
+ */
+
+#include <efi.h>
+#include <efilib.h>
+
+#include "initrd.h"
+#include "linux.h"
+#include "pe.h"
+#include "secure-boot.h"
+#include "util.h"
+
+#define STUB_PAYLOAD_GUID \
+ { 0x55c5d1f8, 0x04cd, 0x46b5, { 0x8a, 0x20, 0xe5, 0x6c, 0xbb, 0x30, 0x52, 0xd0 } }
+
+typedef struct {
+ const void *addr;
+ size_t len;
+ const EFI_DEVICE_PATH *device_path;
+} ValidationContext;
+
+static bool validate_payload(
+ const void *ctx, const EFI_DEVICE_PATH *device_path, const void *file_buffer, size_t file_size) {
+
+ const ValidationContext *payload = ASSERT_PTR(ctx);
+
+ if (device_path != payload->device_path)
+ return false;
+
+ /* Security arch (1) protocol does not provide a file buffer. Instead we are supposed to fetch the payload
+ * ourselves, which is not needed as we already have everything in memory and the device paths match. */
+ if (file_buffer && (file_buffer != payload->addr || file_size != payload->len))
+ return false;
+
+ return true;
+}
+
+static EFI_STATUS load_image(EFI_HANDLE parent, const void *source, size_t len, EFI_HANDLE *ret_image) {
+ assert(parent);
+ assert(source);
+ assert(ret_image);
+
+ /* We could pass a NULL device path, but it's nicer to provide something and it allows us to identify
+ * the loaded image from within the security hooks. */
+ struct {
+ VENDOR_DEVICE_PATH payload;
+ EFI_DEVICE_PATH end;
+ } _packed_ payload_device_path = {
+ .payload = {
+ .Header = {
+ .Type = MEDIA_DEVICE_PATH,
+ .SubType = MEDIA_VENDOR_DP,
+ .Length = { sizeof(payload_device_path.payload), 0 },
+ },
+ .Guid = STUB_PAYLOAD_GUID,
+ },
+ .end = {
+ .Type = END_DEVICE_PATH_TYPE,
+ .SubType = END_ENTIRE_DEVICE_PATH_SUBTYPE,
+ .Length = { sizeof(payload_device_path.end), 0 },
+ },
+ };
+
+ /* We want to support unsigned kernel images as payload, which is safe to do under secure boot
+ * because it is embedded in this stub loader (and since it is already running it must be trusted). */
+ install_security_override(
+ validate_payload,
+ &(ValidationContext) {
+ .addr = source,
+ .len = len,
+ .device_path = &payload_device_path.payload.Header,
+ });
+
+ EFI_STATUS ret = BS->LoadImage(
+ /*BootPolicy=*/false,
+ parent,
+ &payload_device_path.payload.Header,
+ (void *) source,
+ len,
+ ret_image);
+
+ uninstall_security_override();
+
+ return ret;
+}
+
+EFI_STATUS linux_exec(
+ EFI_HANDLE parent,
+ const char16_t *cmdline,
+ const void *linux_buffer,
+ size_t linux_length,
+ const void *initrd_buffer,
+ size_t initrd_length) {
+
+ uint32_t compat_address;
+ EFI_STATUS err;
+
+ assert(parent);
+ assert(linux_buffer && linux_length > 0);
+ assert(initrd_buffer || initrd_length == 0);
+
+ err = pe_kernel_info(linux_buffer, &compat_address);
+#if defined(__i386__) || defined(__x86_64__)
+ if (err == EFI_UNSUPPORTED)
+ /* Kernel is too old to support LINUX_INITRD_MEDIA_GUID, try the deprecated EFI handover
+ * protocol. */
+ return linux_exec_efi_handover(
+ parent,
+ cmdline,
+ linux_buffer,
+ linux_length,
+ initrd_buffer,
+ initrd_length);
+#endif
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, u"Bad kernel image: %r", err);
+
+ _cleanup_(unload_imagep) EFI_HANDLE kernel_image = NULL;
+ err = load_image(parent, linux_buffer, linux_length, &kernel_image);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, u"Error loading kernel image: %r", err);
+
+ EFI_LOADED_IMAGE_PROTOCOL *loaded_image;
+ err = BS->HandleProtocol(kernel_image, &LoadedImageProtocol, (void **) &loaded_image);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, u"Error getting kernel loaded image protocol: %r", err);
+
+ if (cmdline) {
+ loaded_image->LoadOptions = (void *) cmdline;
+ loaded_image->LoadOptionsSize = strsize16(loaded_image->LoadOptions);
+ }
+
+ _cleanup_(cleanup_initrd) EFI_HANDLE initrd_handle = NULL;
+ err = initrd_register(initrd_buffer, initrd_length, &initrd_handle);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, u"Error registering initrd: %r", err);
+
+ err = BS->StartImage(kernel_image, NULL, NULL);
+
+ /* Try calling the kernel compat entry point if one exists. */
+ if (err == EFI_UNSUPPORTED && compat_address > 0) {
+ EFI_IMAGE_ENTRY_POINT compat_entry =
+ (EFI_IMAGE_ENTRY_POINT) ((uint8_t *) loaded_image->ImageBase + compat_address);
+ err = compat_entry(kernel_image, ST);
+ }
+
+ return log_error_status_stall(err, u"Error starting kernel image: %r", err);
+}
diff --git a/src/boot/efi/linux.h b/src/boot/efi/linux.h
new file mode 100644
index 0000000..f0a6a37
--- /dev/null
+++ b/src/boot/efi/linux.h
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <efi.h>
+#include <uchar.h>
+
+EFI_STATUS linux_exec(
+ EFI_HANDLE parent,
+ const char16_t *cmdline,
+ const void *linux_buffer,
+ size_t linux_length,
+ const void *initrd_buffer,
+ size_t initrd_length);
+EFI_STATUS linux_exec_efi_handover(
+ EFI_HANDLE parent,
+ const char16_t *cmdline,
+ const void *linux_buffer,
+ size_t linux_length,
+ const void *initrd_buffer,
+ size_t initrd_length);
diff --git a/src/boot/efi/linux_x86.c b/src/boot/efi/linux_x86.c
new file mode 100644
index 0000000..6a5e431
--- /dev/null
+++ b/src/boot/efi/linux_x86.c
@@ -0,0 +1,215 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+/*
+ * x86 specific code to for EFI handover boot protocol
+ * Linux kernels version 5.8 and newer support providing the initrd by
+ * LINUX_INITRD_MEDIA_GUID DevicePath. In order to support older kernels too,
+ * this x86 specific linux_exec function passes the initrd by setting the
+ * corresponding fields in the setup_header struct.
+ *
+ * see https://docs.kernel.org/x86/boot.html
+ */
+
+#include <efi.h>
+#include <efilib.h>
+
+#include "initrd.h"
+#include "linux.h"
+#include "macro-fundamental.h"
+#include "util.h"
+
+#define KERNEL_SECTOR_SIZE 512u
+#define BOOT_FLAG_MAGIC 0xAA55u
+#define SETUP_MAGIC 0x53726448u /* "HdrS" */
+#define SETUP_VERSION_2_11 0x20bu
+#define SETUP_VERSION_2_12 0x20cu
+#define SETUP_VERSION_2_15 0x20fu
+#define CMDLINE_PTR_MAX 0xA0000u
+
+enum {
+ XLF_KERNEL_64 = 1 << 0,
+ XLF_CAN_BE_LOADED_ABOVE_4G = 1 << 1,
+ XLF_EFI_HANDOVER_32 = 1 << 2,
+ XLF_EFI_HANDOVER_64 = 1 << 3,
+#ifdef __x86_64__
+ XLF_EFI_HANDOVER = XLF_EFI_HANDOVER_64,
+#else
+ XLF_EFI_HANDOVER = XLF_EFI_HANDOVER_32,
+#endif
+};
+
+typedef struct {
+ uint8_t setup_sects;
+ uint16_t root_flags;
+ uint32_t syssize;
+ uint16_t ram_size;
+ uint16_t vid_mode;
+ uint16_t root_dev;
+ uint16_t boot_flag;
+ uint8_t jump; /* We split the 2-byte jump field from the spec in two for convenience. */
+ uint8_t setup_size;
+ uint32_t header;
+ uint16_t version;
+ uint32_t realmode_swtch;
+ uint16_t start_sys_seg;
+ uint16_t kernel_version;
+ uint8_t type_of_loader;
+ uint8_t loadflags;
+ uint16_t setup_move_size;
+ uint32_t code32_start;
+ uint32_t ramdisk_image;
+ uint32_t ramdisk_size;
+ uint32_t bootsect_kludge;
+ uint16_t heap_end_ptr;
+ uint8_t ext_loader_ver;
+ uint8_t ext_loader_type;
+ uint32_t cmd_line_ptr;
+ uint32_t initrd_addr_max;
+ uint32_t kernel_alignment;
+ uint8_t relocatable_kernel;
+ uint8_t min_alignment;
+ uint16_t xloadflags;
+ uint32_t cmdline_size;
+ uint32_t hardware_subarch;
+ uint64_t hardware_subarch_data;
+ uint32_t payload_offset;
+ uint32_t payload_length;
+ uint64_t setup_data;
+ uint64_t pref_address;
+ uint32_t init_size;
+ uint32_t handover_offset;
+} _packed_ SetupHeader;
+
+/* We really only care about a few fields, but we still have to provide a full page otherwise. */
+typedef struct {
+ uint8_t pad[192];
+ uint32_t ext_ramdisk_image;
+ uint32_t ext_ramdisk_size;
+ uint32_t ext_cmd_line_ptr;
+ uint8_t pad2[293];
+ SetupHeader hdr;
+ uint8_t pad3[3480];
+} _packed_ BootParams;
+assert_cc(offsetof(BootParams, ext_ramdisk_image) == 0x0C0);
+assert_cc(sizeof(BootParams) == 4096);
+
+#ifdef __i386__
+# define __regparm0__ __attribute__((regparm(0)))
+#else
+# define __regparm0__
+#endif
+
+typedef void (*handover_f)(void *parent, EFI_SYSTEM_TABLE *table, BootParams *params) __regparm0__
+ __attribute__((sysv_abi));
+
+static void linux_efi_handover(EFI_HANDLE parent, uintptr_t kernel, BootParams *params) {
+ assert(params);
+
+ kernel += (params->hdr.setup_sects + 1) * KERNEL_SECTOR_SIZE; /* 32bit entry address. */
+
+ /* Old kernels needs this set, while newer ones seem to ignore this. Note that this gets truncated on
+ * above 4G boots, which is fine as long as we do not use the value to jump to kernel entry. */
+ params->hdr.code32_start = kernel;
+
+#ifdef __x86_64__
+ kernel += KERNEL_SECTOR_SIZE; /* 64bit entry address. */
+#endif
+
+ kernel += params->hdr.handover_offset; /* 32/64bit EFI handover address. */
+
+ /* Note in EFI mixed mode this now points to the correct 32bit handover entry point, allowing a 64bit
+ * kernel to be booted from a 32bit sd-stub. */
+
+ handover_f handover = (handover_f) kernel;
+ handover(parent, ST, params);
+}
+
+EFI_STATUS linux_exec_efi_handover(
+ EFI_HANDLE parent,
+ const char16_t *cmdline,
+ const void *linux_buffer,
+ size_t linux_length,
+ const void *initrd_buffer,
+ size_t initrd_length) {
+
+ assert(parent);
+ assert(linux_buffer);
+ assert(initrd_buffer || initrd_length == 0);
+
+ if (linux_length < sizeof(BootParams))
+ return EFI_LOAD_ERROR;
+
+ const BootParams *image_params = (const BootParams *) linux_buffer;
+ if (image_params->hdr.header != SETUP_MAGIC || image_params->hdr.boot_flag != BOOT_FLAG_MAGIC)
+ return log_error_status_stall(EFI_UNSUPPORTED, u"Unsupported kernel image.");
+ if (image_params->hdr.version < SETUP_VERSION_2_11)
+ return log_error_status_stall(EFI_UNSUPPORTED, u"Kernel too old.");
+ if (!image_params->hdr.relocatable_kernel)
+ return log_error_status_stall(EFI_UNSUPPORTED, u"Kernel is not relocatable.");
+
+ /* The xloadflags were added in version 2.12+ of the boot protocol but the handover support predates
+ * that, so we cannot safety-check this for 2.11. */
+ if (image_params->hdr.version >= SETUP_VERSION_2_12 &&
+ !FLAGS_SET(image_params->hdr.xloadflags, XLF_EFI_HANDOVER))
+ return log_error_status_stall(EFI_UNSUPPORTED, u"Kernel does not support EFI handover protocol.");
+
+ bool can_4g = image_params->hdr.version >= SETUP_VERSION_2_12 &&
+ FLAGS_SET(image_params->hdr.xloadflags, XLF_CAN_BE_LOADED_ABOVE_4G);
+
+ if (!can_4g && POINTER_TO_PHYSICAL_ADDRESS(linux_buffer) + linux_length > UINT32_MAX)
+ return log_error_status_stall(
+ EFI_UNSUPPORTED,
+ u"Unified kernel image was loaded above 4G, but kernel lacks support.");
+ if (!can_4g && POINTER_TO_PHYSICAL_ADDRESS(initrd_buffer) + initrd_length > UINT32_MAX)
+ return log_error_status_stall(
+ EFI_UNSUPPORTED, u"Initrd is above 4G, but kernel lacks support.");
+
+ _cleanup_pages_ Pages boot_params_page = xmalloc_pages(
+ can_4g ? AllocateAnyPages : AllocateMaxAddress,
+ EfiLoaderData,
+ EFI_SIZE_TO_PAGES(sizeof(BootParams)),
+ UINT32_MAX /* Below the 4G boundary */);
+ BootParams *boot_params = PHYSICAL_ADDRESS_TO_POINTER(boot_params_page.addr);
+ *boot_params = (BootParams){};
+
+ /* Setup size is determined by offset 0x0202 + byte value at offset 0x0201, which is the same as
+ * offset of the header field and the target from the jump field (which we split for this reason). */
+ memcpy(&boot_params->hdr,
+ &image_params->hdr,
+ offsetof(SetupHeader, header) + image_params->hdr.setup_size);
+
+ boot_params->hdr.type_of_loader = 0xff;
+
+ /* Spec says: For backwards compatibility, if the setup_sects field contains 0, the real value is 4. */
+ if (boot_params->hdr.setup_sects == 0)
+ boot_params->hdr.setup_sects = 4;
+
+ _cleanup_pages_ Pages cmdline_pages = {};
+ if (cmdline) {
+ size_t len = MIN(strlen16(cmdline), image_params->hdr.cmdline_size);
+
+ cmdline_pages = xmalloc_pages(
+ can_4g ? AllocateAnyPages : AllocateMaxAddress,
+ EfiLoaderData,
+ EFI_SIZE_TO_PAGES(len + 1),
+ CMDLINE_PTR_MAX);
+
+ /* Convert cmdline to ASCII. */
+ char *cmdline8 = PHYSICAL_ADDRESS_TO_POINTER(cmdline_pages.addr);
+ for (size_t i = 0; i < len; i++)
+ cmdline8[i] = cmdline[i] <= 0x7E ? cmdline[i] : ' ';
+ cmdline8[len] = '\0';
+
+ boot_params->hdr.cmd_line_ptr = (uint32_t) cmdline_pages.addr;
+ boot_params->ext_cmd_line_ptr = cmdline_pages.addr >> 32;
+ assert(can_4g || cmdline_pages.addr <= CMDLINE_PTR_MAX);
+ }
+
+ boot_params->hdr.ramdisk_image = (uintptr_t) initrd_buffer;
+ boot_params->ext_ramdisk_image = POINTER_TO_PHYSICAL_ADDRESS(initrd_buffer) >> 32;
+ boot_params->hdr.ramdisk_size = initrd_length;
+ boot_params->ext_ramdisk_size = ((uint64_t) initrd_length) >> 32;
+
+ linux_efi_handover(parent, (uintptr_t) linux_buffer, boot_params);
+ return EFI_LOAD_ERROR;
+}
diff --git a/src/boot/efi/measure.c b/src/boot/efi/measure.c
new file mode 100644
index 0000000..0b5b626
--- /dev/null
+++ b/src/boot/efi/measure.c
@@ -0,0 +1,221 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#if ENABLE_TPM
+
+#include <efi.h>
+#include <efilib.h>
+
+#include "tpm-pcr.h"
+#include "macro-fundamental.h"
+#include "measure.h"
+#include "missing_efi.h"
+#include "util.h"
+
+static EFI_STATUS tpm1_measure_to_pcr_and_event_log(
+ const EFI_TCG *tcg,
+ uint32_t pcrindex,
+ EFI_PHYSICAL_ADDRESS buffer,
+ UINTN buffer_size,
+ const char16_t *description) {
+
+ _cleanup_free_ TCG_PCR_EVENT *tcg_event = NULL;
+ EFI_PHYSICAL_ADDRESS event_log_last;
+ uint32_t event_number = 1;
+ UINTN desc_len;
+
+ assert(tcg);
+ assert(description);
+
+ desc_len = strsize16(description);
+ tcg_event = xmalloc(offsetof(TCG_PCR_EVENT, Event) + desc_len);
+ memset(tcg_event, 0, offsetof(TCG_PCR_EVENT, Event) + desc_len);
+ *tcg_event = (TCG_PCR_EVENT) {
+ .EventSize = desc_len,
+ .PCRIndex = pcrindex,
+ .EventType = EV_IPL,
+ };
+ memcpy(tcg_event->Event, description, desc_len);
+
+ return tcg->HashLogExtendEvent(
+ (EFI_TCG *) tcg,
+ buffer, buffer_size,
+ TCG_ALG_SHA,
+ tcg_event,
+ &event_number,
+ &event_log_last);
+}
+
+static EFI_STATUS tpm2_measure_to_pcr_and_event_log(
+ EFI_TCG2 *tcg,
+ uint32_t pcrindex,
+ EFI_PHYSICAL_ADDRESS buffer,
+ uint64_t buffer_size,
+ const char16_t *description) {
+
+ _cleanup_free_ EFI_TCG2_EVENT *tcg_event = NULL;
+ UINTN desc_len;
+
+ assert(tcg);
+ assert(description);
+
+ desc_len = strsize16(description);
+ tcg_event = xmalloc(offsetof(EFI_TCG2_EVENT, Event) + desc_len);
+ memset(tcg_event, 0, offsetof(EFI_TCG2_EVENT, Event) + desc_len);
+ *tcg_event = (EFI_TCG2_EVENT) {
+ .Size = offsetof(EFI_TCG2_EVENT, Event) + desc_len,
+ .Header.HeaderSize = sizeof(EFI_TCG2_EVENT_HEADER),
+ .Header.HeaderVersion = EFI_TCG2_EVENT_HEADER_VERSION,
+ .Header.PCRIndex = pcrindex,
+ .Header.EventType = EV_IPL,
+ };
+
+ memcpy(tcg_event->Event, description, desc_len);
+
+ return tcg->HashLogExtendEvent(
+ tcg,
+ 0,
+ buffer, buffer_size,
+ tcg_event);
+}
+
+static EFI_TCG *tcg1_interface_check(void) {
+ EFI_PHYSICAL_ADDRESS event_log_location, event_log_last_entry;
+ TCG_BOOT_SERVICE_CAPABILITY capability = {
+ .Size = sizeof(capability),
+ };
+ EFI_STATUS err;
+ uint32_t features;
+ EFI_TCG *tcg;
+
+ err = BS->LocateProtocol((EFI_GUID *) EFI_TCG_GUID, NULL, (void **) &tcg);
+ if (err != EFI_SUCCESS)
+ return NULL;
+
+ err = tcg->StatusCheck(
+ tcg,
+ &capability,
+ &features,
+ &event_log_location,
+ &event_log_last_entry);
+ if (err != EFI_SUCCESS)
+ return NULL;
+
+ if (capability.TPMDeactivatedFlag)
+ return NULL;
+
+ if (!capability.TPMPresentFlag)
+ return NULL;
+
+ return tcg;
+}
+
+static EFI_TCG2 * tcg2_interface_check(void) {
+ EFI_TCG2_BOOT_SERVICE_CAPABILITY capability = {
+ .Size = sizeof(capability),
+ };
+ EFI_STATUS err;
+ EFI_TCG2 *tcg;
+
+ err = BS->LocateProtocol((EFI_GUID *) EFI_TCG2_GUID, NULL, (void **) &tcg);
+ if (err != EFI_SUCCESS)
+ return NULL;
+
+ err = tcg->GetCapability(tcg, &capability);
+ if (err != EFI_SUCCESS)
+ return NULL;
+
+ if (capability.StructureVersion.Major == 1 &&
+ capability.StructureVersion.Minor == 0) {
+ TCG_BOOT_SERVICE_CAPABILITY *caps_1_0 =
+ (TCG_BOOT_SERVICE_CAPABILITY*) &capability;
+ if (caps_1_0->TPMPresentFlag)
+ return tcg;
+ }
+
+ if (!capability.TPMPresentFlag)
+ return NULL;
+
+ return tcg;
+}
+
+bool tpm_present(void) {
+ return tcg2_interface_check() || tcg1_interface_check();
+}
+
+EFI_STATUS tpm_log_event(uint32_t pcrindex, EFI_PHYSICAL_ADDRESS buffer, UINTN buffer_size, const char16_t *description, bool *ret_measured) {
+ EFI_TCG2 *tpm2;
+ EFI_STATUS err;
+
+ assert(description || pcrindex == UINT32_MAX);
+
+ /* If EFI_SUCCESS is returned, will initialize ret_measured to true if we actually measured
+ * something, or false if measurement was turned off. */
+
+ if (pcrindex == UINT32_MAX) { /* PCR disabled? */
+ if (ret_measured)
+ *ret_measured = false;
+
+ return EFI_SUCCESS;
+ }
+
+ tpm2 = tcg2_interface_check();
+ if (tpm2)
+ err = tpm2_measure_to_pcr_and_event_log(tpm2, pcrindex, buffer, buffer_size, description);
+ else {
+ EFI_TCG *tpm1;
+
+ tpm1 = tcg1_interface_check();
+ if (tpm1)
+ err = tpm1_measure_to_pcr_and_event_log(tpm1, pcrindex, buffer, buffer_size, description);
+ else {
+ /* No active TPM found, so don't return an error */
+
+ if (ret_measured)
+ *ret_measured = false;
+
+ return EFI_SUCCESS;
+ }
+ }
+
+ if (err == EFI_SUCCESS && ret_measured)
+ *ret_measured = true;
+
+ return err;
+}
+
+EFI_STATUS tpm_log_event_ascii(uint32_t pcrindex, EFI_PHYSICAL_ADDRESS buffer, UINTN buffer_size, const char *description, bool *ret_measured) {
+ _cleanup_free_ char16_t *c = NULL;
+
+ if (description)
+ c = xstr8_to_16(description);
+
+ return tpm_log_event(pcrindex, buffer, buffer_size, c, ret_measured);
+}
+
+EFI_STATUS tpm_log_load_options(const char16_t *load_options, bool *ret_measured) {
+ int measured = -1;
+ EFI_STATUS err;
+
+ /* Measures a load options string into the TPM2, i.e. the kernel command line */
+
+ for (UINTN i = 0; i < 2; i++) {
+ uint32_t pcr = i == 0 ? TPM_PCR_INDEX_KERNEL_PARAMETERS : TPM_PCR_INDEX_KERNEL_PARAMETERS_COMPAT;
+ bool m;
+
+ if (pcr == UINT32_MAX) /* Skip this one, if it's invalid, so that our 'measured' return value is not corrupted by it */
+ continue;
+
+ err = tpm_log_event(pcr, POINTER_TO_PHYSICAL_ADDRESS(load_options), strsize16(load_options), load_options, &m);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Unable to add load options (i.e. kernel command) line measurement to PCR %u: %r", pcr, err);
+
+ measured = measured < 0 ? m : (measured && m);
+ }
+
+ if (ret_measured)
+ *ret_measured = measured < 0 ? false : measured;
+
+ return EFI_SUCCESS;
+}
+
+#endif
diff --git a/src/boot/efi/measure.h b/src/boot/efi/measure.h
new file mode 100644
index 0000000..19a50f4
--- /dev/null
+++ b/src/boot/efi/measure.h
@@ -0,0 +1,39 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <efi.h>
+#include <stdbool.h>
+#include <uchar.h>
+
+#if ENABLE_TPM
+
+bool tpm_present(void);
+EFI_STATUS tpm_log_event(uint32_t pcrindex, EFI_PHYSICAL_ADDRESS buffer, UINTN buffer_size, const char16_t *description, bool *ret_measured);
+EFI_STATUS tpm_log_event_ascii(uint32_t pcrindex, EFI_PHYSICAL_ADDRESS buffer, UINTN buffer_size, const char *description, bool *ret_measured);
+EFI_STATUS tpm_log_load_options(const char16_t *cmdline, bool *ret_measured);
+
+#else
+
+static inline bool tpm_present(void) {
+ return false;
+}
+
+static inline EFI_STATUS tpm_log_event(uint32_t pcrindex, EFI_PHYSICAL_ADDRESS buffer, UINTN buffer_size, const char16_t *description, bool *ret_measured) {
+ if (ret_measured)
+ *ret_measured = false;
+ return EFI_SUCCESS;
+}
+
+static inline EFI_STATUS tpm_log_event_ascii(uint32_t pcrindex, EFI_PHYSICAL_ADDRESS buffer, UINTN buffer_size, const char *description, bool *ret_measured) {
+ if (ret_measured)
+ *ret_measured = false;
+ return EFI_SUCCESS;
+}
+
+static inline EFI_STATUS tpm_log_load_options(const char16_t *cmdline, bool *ret_measured) {
+ if (ret_measured)
+ *ret_measured = false;
+ return EFI_SUCCESS;
+}
+
+#endif
diff --git a/src/boot/efi/meson.build b/src/boot/efi/meson.build
new file mode 100644
index 0000000..0196314
--- /dev/null
+++ b/src/boot/efi/meson.build
@@ -0,0 +1,500 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+conf.set10('ENABLE_EFI', get_option('efi'))
+conf.set10('HAVE_GNU_EFI', false)
+
+efi_config_h_dir = meson.current_build_dir()
+
+if not get_option('efi') or get_option('gnu-efi') == 'false'
+ if get_option('gnu-efi') == 'true'
+ error('gnu-efi support requested, but general efi support is disabled')
+ endif
+ subdir_done()
+endif
+
+efi_arch = host_machine.cpu_family()
+if efi_arch == 'x86' and '-m64' in get_option('efi-cflags')
+ efi_arch = 'x86_64'
+elif efi_arch == 'x86_64' and '-m32' in get_option('efi-cflags')
+ efi_arch = 'x86'
+endif
+efi_arch = {
+ # host_cc_arch: [efi_arch (see Table 3-2 in UEFI spec), obsolete gnu_efi_inc_arch]
+ 'x86': ['ia32', 'ia32'],
+ 'x86_64': ['x64', 'x86_64'],
+ 'arm': ['arm', 'arm'],
+ 'aarch64': ['aa64', 'aarch64'],
+ 'riscv64': ['riscv64', 'riscv64'],
+}.get(efi_arch, [])
+
+efi_incdir = get_option('efi-includedir')
+found = false
+foreach efi_arch_candidate : efi_arch
+ efi_archdir = efi_incdir / efi_arch_candidate
+ if cc.has_header(efi_archdir / 'efibind.h',
+ args: get_option('efi-cflags'))
+ found = true
+ break
+ endif
+endforeach
+
+if not found
+ if get_option('gnu-efi') == 'true'
+ error('gnu-efi support requested, but headers not found or efi arch is unknown')
+ endif
+ warning('gnu-efi headers not found or efi arch is unknown, disabling gnu-efi support')
+ subdir_done()
+endif
+
+if not cc.has_header_symbol('efi.h', 'EFI_IMAGE_MACHINE_X64',
+ args: ['-nostdlib', '-ffreestanding', '-fshort-wchar'] + get_option('efi-cflags'),
+ include_directories: include_directories(efi_incdir,
+ efi_archdir))
+
+ if get_option('gnu-efi') == 'true'
+ error('gnu-efi support requested, but found headers are too old (3.0.5+ required)')
+ endif
+ warning('gnu-efi headers are too old (3.0.5+ required), disabling gnu-efi support')
+ subdir_done()
+endif
+
+objcopy = run_command(cc.cmd_array(), '-print-prog-name=objcopy', check: true).stdout().strip()
+objcopy_2_38 = find_program('objcopy', version: '>=2.38', required: false)
+
+efi_ld = get_option('efi-ld')
+if efi_ld == 'auto'
+ efi_ld = cc.get_linker_id().split('.')[1]
+ if efi_ld not in ['bfd', 'gold']
+ message('Not using @0@ as efi-ld, falling back to bfd'.format(efi_ld))
+ efi_ld = 'bfd'
+ endif
+endif
+
+efi_multilib = run_command(
+ cc.cmd_array(), '-print-multi-os-directory', get_option('efi-cflags'),
+ check: false
+).stdout().strip()
+efi_multilib = run_command(
+ 'realpath', '-e', '/usr/lib' / efi_multilib,
+ check: false
+).stdout().strip()
+
+efi_libdir = ''
+foreach dir : [get_option('efi-libdir'),
+ '/usr/lib/gnuefi' / efi_arch[0],
+ efi_multilib]
+ if dir != '' and fs.is_dir(dir)
+ efi_libdir = dir
+ break
+ endif
+endforeach
+if efi_libdir == ''
+ if get_option('gnu-efi') == 'true'
+ error('gnu-efi support requested, but efi-libdir was not found')
+ endif
+ warning('efi-libdir was not found, disabling gnu-efi support')
+ subdir_done()
+endif
+
+efi_lds = ''
+foreach location : [ # New locations first introduced with gnu-efi 3.0.11
+ [efi_libdir / 'efi.lds',
+ efi_libdir / 'crt0.o'],
+ # Older locations...
+ [efi_libdir / 'gnuefi' / 'elf_@0@_efi.lds'.format(efi_arch[1]),
+ efi_libdir / 'gnuefi' / 'crt0-efi-@0@.o'.format(efi_arch[1])],
+ [efi_libdir / 'elf_@0@_efi.lds'.format(efi_arch[1]),
+ efi_libdir / 'crt0-efi-@0@.o'.format(efi_arch[1])]]
+ if fs.is_file(location[0]) and fs.is_file(location[1])
+ efi_lds = location[0]
+ efi_crt0 = location[1]
+ break
+ endif
+endforeach
+if efi_lds == ''
+ if get_option('gnu-efi') == 'true'
+ error('gnu-efi support requested, but cannot find efi.lds')
+ endif
+ warning('efi.lds was not found, disabling gnu-efi support')
+ subdir_done()
+endif
+
+conf.set10('HAVE_GNU_EFI', true)
+conf.set_quoted('EFI_MACHINE_TYPE_NAME', efi_arch[0])
+
+efi_conf = configuration_data()
+efi_conf.set_quoted('EFI_MACHINE_TYPE_NAME', efi_arch[0])
+efi_conf.set10('ENABLE_TPM', get_option('tpm'))
+efi_conf.set10('EFI_TPM_PCR_COMPAT', get_option('efi-tpm-pcr-compat'))
+
+foreach ctype : ['color-normal', 'color-entry', 'color-highlight', 'color-edit']
+ c = get_option('efi-' + ctype).split(',')
+ efi_conf.set(ctype.underscorify().to_upper(), 'EFI_TEXT_ATTR(@0@, @1@)'.format(
+ 'EFI_' + c[0].strip().underscorify().to_upper(),
+ 'EFI_' + c[1].strip().underscorify().to_upper()))
+endforeach
+
+if meson.is_cross_build() and get_option('sbat-distro') == 'auto'
+ warning('Auto detection of SBAT information not supported when cross-building, disabling SBAT.')
+elif get_option('sbat-distro') != ''
+ efi_conf.set_quoted('SBAT_PROJECT', meson.project_name())
+ efi_conf.set_quoted('PROJECT_VERSION', meson.project_version())
+ efi_conf.set('PROJECT_URL', conf.get('PROJECT_URL'))
+ if get_option('sbat-distro-generation') < 1
+ error('SBAT Distro Generation must be a positive integer')
+ endif
+ efi_conf.set('SBAT_DISTRO_GENERATION', get_option('sbat-distro-generation'))
+ foreach sbatvar : [['sbat-distro', 'ID'],
+ ['sbat-distro-summary', 'NAME'],
+ ['sbat-distro-url', 'BUG_REPORT_URL']]
+ value = get_option(sbatvar[0])
+ if (value == '' or value == 'auto') and not meson.is_cross_build()
+ cmd = 'if [ -e /etc/os-release ]; then . /etc/os-release; else . /usr/lib/os-release; fi; echo $@0@'.format(sbatvar[1])
+ value = run_command(sh, '-c', cmd, check: true).stdout().strip()
+ endif
+ if value == ''
+ error('Required @0@ option not set and autodetection failed'.format(sbatvar[0]))
+ endif
+ efi_conf.set_quoted(sbatvar[0].underscorify().to_upper(), value)
+ endforeach
+
+ pkgname = get_option('sbat-distro-pkgname')
+ if pkgname == ''
+ pkgname = meson.project_name()
+ endif
+ efi_conf.set_quoted('SBAT_DISTRO_PKGNAME', pkgname)
+
+ pkgver = get_option('sbat-distro-version')
+ if pkgver == ''
+ efi_conf.set('SBAT_DISTRO_VERSION', 'GIT_VERSION')
+ # This is determined during build, not configuration, so we can't display it yet.
+ sbat_distro_version_display = '(git version)'
+ else
+ efi_conf.set_quoted('SBAT_DISTRO_VERSION', pkgver)
+ sbat_distro_version_display = pkgver
+ endif
+endif
+
+efi_config_h = configure_file(
+ output : 'efi_config.h',
+ configuration : efi_conf)
+
+efi_cflags = [
+ '-DGNU_EFI_USE_MS_ABI',
+ '-DSD_BOOT',
+ '-ffreestanding',
+ '-fshort-wchar',
+ '-fvisibility=hidden',
+ '-I', fundamental_path,
+ '-I', meson.current_source_dir(),
+ '-include', efi_config_h,
+ '-include', version_h,
+ '-I', efi_archdir,
+ '-isystem', efi_incdir,
+ '-std=gnu11',
+ '-Wall',
+ '-Wextra',
+] + cc.get_supported_arguments(
+ basic_disabled_warnings +
+ possible_common_cc_flags + [
+ '-fno-stack-protector',
+ '-fno-strict-aliasing',
+ '-fpic',
+ '-fwide-exec-charset=UCS2',
+ ]
+)
+
+# Our code size has increased enough to possibly create overlapping PE sections
+# at sd-stub runtime, which will often enough prevent the image from booting.
+# This only happens because the usual instructions for assembling a unified
+# kernel image contain hardcoded addresses for section VMAs added in. Until a
+# proper solution is in place, we can at least compile with as least -O1 to
+# reduce the likelihood of this happening.
+# https://github.com/systemd/systemd/issues/24202
+efi_cflags += '-O1'
+
+efi_cflags += cc.get_supported_arguments({
+ 'ia32': ['-mno-sse', '-mno-mmx'],
+ 'x86_64': ['-mno-red-zone', '-mno-sse', '-mno-mmx'],
+ 'arm': ['-mgeneral-regs-only', '-mfpu=none'],
+}.get(efi_arch[1], []))
+
+# We are putting the efi_cc command line together ourselves, so make sure to pull any
+# relevant compiler flags from meson/CFLAGS as povided by the user or distro.
+
+if get_option('werror')
+ efi_cflags += ['-Werror']
+endif
+if get_option('debug') and get_option('mode') == 'developer'
+ efi_cflags += ['-ggdb', '-DEFI_DEBUG']
+endif
+if get_option('optimization') in ['1', '2', '3', 's', 'g']
+ efi_cflags += ['-O' + get_option('optimization')]
+endif
+if get_option('b_ndebug') == 'true' or (
+ get_option('b_ndebug') == 'if-release' and get_option('buildtype') in ['plain', 'release'])
+ efi_cflags += ['-DNDEBUG']
+endif
+if get_option('b_lto')
+ efi_cflags += cc.has_argument('-flto=auto') ? ['-flto=auto'] : ['-flto']
+endif
+
+foreach arg : get_option('c_args')
+ if arg in [
+ '-DNDEBUG',
+ '-fno-lto',
+ '-O1', '-O2', '-O3', '-Og', '-Os',
+ '-Werror',
+ ] or arg.split('=')[0] in [
+ '-ffile-prefix-map',
+ '-flto',
+ ] or (get_option('mode') == 'developer' and arg in [
+ '-DEFI_DEBUG',
+ '-g', '-ggdb',
+ ])
+
+ message('Using "@0@" from c_args for EFI compiler'.format(arg))
+ efi_cflags += arg
+ endif
+endforeach
+
+efi_cflags += get_option('efi-cflags')
+
+efi_ldflags = [
+ '-fuse-ld=' + efi_ld,
+ '-L', efi_libdir,
+ '-nostdlib',
+ '-T', efi_lds,
+ '-Wl,--build-id=sha1',
+ '-Wl,--fatal-warnings',
+ '-Wl,--no-undefined',
+ '-Wl,--warn-common',
+ '-Wl,-Bsymbolic',
+ '-z', 'nocombreloc',
+ '-z', 'noexecstack',
+ efi_crt0,
+]
+
+foreach arg : ['-Wl,--no-warn-execstack',
+ '-Wl,--no-warn-rwx-segments']
+ # We need to check the correct linker for supported args. This is what
+ # cc.has_multi_link_arguments() is for, but it helpfully overrides our
+ # choice of linker by putting its own -fuse-ld= arg after ours.
+ if run_command('bash', '-c',
+ 'exec "$@" -x c -o/dev/null <(echo "int main(void){return 0;}")' +
+ ' -fuse-ld=' + efi_ld + ' -Wl,--fatal-warnings ' + arg,
+ 'bash', cc.cmd_array(),
+ check : false).returncode() == 0
+ efi_ldflags += arg
+ endif
+endforeach
+
+# If using objcopy, crt0 must not include the PE/COFF header
+if run_command('grep', '-q', 'coff_header', efi_crt0, check: false).returncode() == 0
+ coff_header_in_crt0 = true
+else
+ coff_header_in_crt0 = false
+endif
+
+if efi_arch[1] in ['arm', 'riscv64'] or (efi_arch[1] == 'aarch64' and (not objcopy_2_38.found() or coff_header_in_crt0))
+ efi_ldflags += ['-shared']
+ # ARM32 and 64bit RISC-V don't have an EFI capable objcopy.
+ # Older objcopy doesn't support Aarch64 either.
+ # Use 'binary' instead, and add required symbols manually.
+ efi_ldflags += ['-Wl,--defsym=EFI_SUBSYSTEM=0xa']
+ efi_format = ['-O', 'binary']
+else
+ efi_ldflags += ['-pie']
+ if efi_ld == 'bfd'
+ efi_ldflags += '-Wl,--no-dynamic-linker'
+ endif
+ efi_format = ['--target=efi-app-@0@'.format(efi_arch[1])]
+endif
+
+if efi_arch[1] == 'arm'
+ # On arm, the compiler (correctly) warns about wchar_t size mismatch. This
+ # is because libgcc is not compiled with -fshort-wchar, but it does not
+ # have any occurrences of wchar_t in its sources or the documentation, so
+ # it is safe to assume that we can ignore this warning.
+ efi_ldflags += ['-Wl,--no-wchar-size-warning']
+endif
+
+if run_command('grep', '-q', '__CTOR_LIST__', efi_lds, check: false).returncode() == 0
+ # fedora has a patched gnu-efi that adds support for ELF constructors.
+ # If ld is called by gcc something about these symbols breaks, resulting
+ # in sd-boot freezing when gnu-efi runs the constructors. Force defining
+ # them seems to work around this.
+ efi_ldflags += [
+ '-Wl,--defsym=_init_array=0',
+ '-Wl,--defsym=_init_array_end=0',
+ '-Wl,--defsym=_fini_array=0',
+ '-Wl,--defsym=_fini_array_end=0',
+ '-Wl,--defsym=__CTOR_LIST__=0',
+ '-Wl,--defsym=__CTOR_END__=0',
+ '-Wl,--defsym=__DTOR_LIST__=0',
+ '-Wl,--defsym=__DTOR_END__=0',
+ ]
+endif
+
+if cc.get_id() == 'clang' and cc.version().split('.')[0].to_int() <= 10
+ # clang <= 10 doesn't pass -T to the linker and then even complains about it being unused
+ efi_ldflags += ['-Wl,-T,' + efi_lds, '-Wno-unused-command-line-argument']
+endif
+
+summary({
+ 'EFI machine type' : efi_arch[0],
+ 'EFI LD' : efi_ld,
+ 'EFI lds' : efi_lds,
+ 'EFI crt0' : efi_crt0,
+ 'EFI include directory' : efi_archdir},
+ section : 'Extensible Firmware Interface')
+
+if efi_conf.get('SBAT_DISTRO', '') != ''
+ summary({
+ 'SBAT distro': efi_conf.get('SBAT_DISTRO'),
+ 'SBAT distro generation': efi_conf.get('SBAT_DISTRO_GENERATION'),
+ 'SBAT distro version': sbat_distro_version_display,
+ 'SBAT distro summary': efi_conf.get('SBAT_DISTRO_SUMMARY'),
+ 'SBAT distro URL': efi_conf.get('SBAT_DISTRO_URL')},
+ section : 'Extensible Firmware Interface')
+endif
+
+############################################################
+
+efi_headers = files(
+ 'bcd.h',
+ 'console.h',
+ 'cpio.h',
+ 'devicetree.h',
+ 'disk.h',
+ 'drivers.h',
+ 'efi-string.h',
+ 'graphics.h',
+ 'initrd.h',
+ 'linux.h',
+ 'measure.h',
+ 'missing_efi.h',
+ 'pe.h',
+ 'random-seed.h',
+ 'secure-boot.h',
+ 'shim.h',
+ 'splash.h',
+ 'ticks.h',
+ 'util.h',
+ 'xbootldr.h',
+)
+
+common_sources = files(
+ 'assert.c',
+ 'console.c',
+ 'devicetree.c',
+ 'disk.c',
+ 'efi-string.c',
+ 'graphics.c',
+ 'initrd.c',
+ 'measure.c',
+ 'pe.c',
+ 'secure-boot.c',
+ 'ticks.c',
+ 'util.c',
+)
+
+systemd_boot_sources = files(
+ 'boot.c',
+ 'drivers.c',
+ 'random-seed.c',
+ 'vmm.c',
+ 'shim.c',
+ 'xbootldr.c',
+)
+
+stub_sources = files(
+ 'cpio.c',
+ 'linux.c',
+ 'splash.c',
+ 'stub.c',
+)
+
+if efi_arch[1] in ['ia32', 'x86_64']
+ stub_sources += files('linux_x86.c')
+endif
+
+tests += [
+ [files('test-efi-string.c', 'efi-string.c')],
+]
+
+# BCD parser only makes sense on arches that Windows supports.
+if efi_arch[1] in ['ia32', 'x86_64', 'arm', 'aarch64']
+ systemd_boot_sources += files('bcd.c')
+ tests += [
+ [files('test-bcd.c', 'efi-string.c'),
+ [],
+ [libzstd],
+ [],
+ 'HAVE_ZSTD'],
+ ]
+ fuzzers += [
+ [files('fuzz-bcd.c', 'bcd.c', 'efi-string.c')],
+ [files('fuzz-efi-string.c', 'efi-string.c')],
+ ]
+endif
+
+systemd_boot_objects = []
+stub_objects = []
+foreach file : fundamental_source_paths + common_sources + systemd_boot_sources + stub_sources
+ # FIXME: replace ''.format(file) with fs.name(file) when meson_version requirement is >= 0.59.0
+ o_file = custom_target('@0@.o'.format(file).split('/')[-1],
+ input : file,
+ output : '@0@.o'.format(file).split('/')[-1],
+ command : [cc.cmd_array(), '-c', '@INPUT@', '-o', '@OUTPUT@', efi_cflags],
+ depend_files : efi_headers + fundamental_headers)
+ if (fundamental_source_paths + common_sources + systemd_boot_sources).contains(file)
+ systemd_boot_objects += o_file
+ endif
+ if (fundamental_source_paths + common_sources + stub_sources).contains(file)
+ stub_objects += o_file
+ endif
+endforeach
+
+foreach tuple : [['systemd-boot@0@.@1@', systemd_boot_objects, false, 'systemd-boot'],
+ ['linux@0@.@1@.stub', stub_objects, true, 'systemd-stub']]
+ elf = custom_target(
+ tuple[0].format(efi_arch[0], 'elf'),
+ input : tuple[1],
+ output : tuple[0].format(efi_arch[0], 'elf'),
+ command : [cc.cmd_array(),
+ '-o', '@OUTPUT@',
+ efi_cflags,
+ efi_ldflags,
+ '@INPUT@',
+ '-lefi',
+ '-lgnuefi',
+ '-lgcc'],
+ install : tuple[2],
+ install_tag: tuple[3],
+ install_dir : bootlibdir)
+
+ efi = custom_target(
+ tuple[0].format(efi_arch[0], 'efi'),
+ input : elf,
+ output : tuple[0].format(efi_arch[0], 'efi'),
+ command : [objcopy,
+ '-j', '.bss*',
+ '-j', '.data',
+ '-j', '.dynamic',
+ '-j', '.dynsym',
+ '-j', '.osrel',
+ '-j', '.rel*',
+ '-j', '.sbat',
+ '-j', '.sdata',
+ '-j', '.sdmagic',
+ '-j', '.text',
+ '--section-alignment=512',
+ efi_format,
+ '@INPUT@', '@OUTPUT@'],
+ install : true,
+ install_tag: tuple[3],
+ install_dir : bootlibdir)
+
+ alias_target(tuple[3], efi)
+endforeach
diff --git a/src/boot/efi/missing_efi.h b/src/boot/efi/missing_efi.h
new file mode 100644
index 0000000..250c84c
--- /dev/null
+++ b/src/boot/efi/missing_efi.h
@@ -0,0 +1,400 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <efi.h>
+
+#include "macro-fundamental.h"
+
+/* gnu-efi 3.0.13 */
+#ifndef EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL_GUID
+
+#define EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL_GUID \
+ { 0xdd9e7534, 0x7762, 0x4698, {0x8c, 0x14, 0xf5, 0x85, 0x17, 0xa6, 0x25, 0xaa} }
+#define SimpleTextInputExProtocol ((EFI_GUID)EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL_GUID)
+
+#define EFI_SHIFT_STATE_VALID 0x80000000
+#define EFI_RIGHT_SHIFT_PRESSED 0x00000001
+#define EFI_LEFT_SHIFT_PRESSED 0x00000002
+#define EFI_RIGHT_CONTROL_PRESSED 0x00000004
+#define EFI_LEFT_CONTROL_PRESSED 0x00000008
+#define EFI_RIGHT_ALT_PRESSED 0x00000010
+#define EFI_LEFT_ALT_PRESSED 0x00000020
+#define EFI_RIGHT_LOGO_PRESSED 0x00000040
+#define EFI_LEFT_LOGO_PRESSED 0x00000080
+
+struct _EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL;
+
+typedef EFI_STATUS (EFIAPI *EFI_INPUT_RESET_EX)(
+ struct _EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *This,
+ BOOLEAN ExtendedVerification
+);
+
+typedef UINT8 EFI_KEY_TOGGLE_STATE;
+
+typedef struct {
+ UINT32 KeyShiftState;
+ EFI_KEY_TOGGLE_STATE KeyToggleState;
+} EFI_KEY_STATE;
+
+typedef struct {
+ EFI_INPUT_KEY Key;
+ EFI_KEY_STATE KeyState;
+} EFI_KEY_DATA;
+
+typedef EFI_STATUS (EFIAPI *EFI_INPUT_READ_KEY_EX)(
+ struct _EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *This,
+ EFI_KEY_DATA *KeyData
+);
+
+typedef EFI_STATUS (EFIAPI *EFI_SET_STATE)(
+ struct _EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *This,
+ EFI_KEY_TOGGLE_STATE *KeyToggleState
+);
+
+typedef EFI_STATUS (EFIAPI *EFI_KEY_NOTIFY_FUNCTION)(
+ EFI_KEY_DATA *KeyData
+);
+
+typedef EFI_STATUS (EFIAPI *EFI_REGISTER_KEYSTROKE_NOTIFY)(
+ struct _EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *This,
+ EFI_KEY_DATA KeyData,
+ EFI_KEY_NOTIFY_FUNCTION KeyNotificationFunction,
+ VOID **NotifyHandle
+);
+
+typedef EFI_STATUS (EFIAPI *EFI_UNREGISTER_KEYSTROKE_NOTIFY)(
+ struct _EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *This,
+ VOID *NotificationHandle
+);
+
+typedef struct _EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL {
+ EFI_INPUT_RESET_EX Reset;
+ EFI_INPUT_READ_KEY_EX ReadKeyStrokeEx;
+ EFI_EVENT WaitForKeyEx;
+ EFI_SET_STATE SetState;
+ EFI_REGISTER_KEYSTROKE_NOTIFY RegisterKeyNotify;
+ EFI_UNREGISTER_KEYSTROKE_NOTIFY UnregisterKeyNotify;
+} EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL;
+
+#endif
+
+/* gnu-efi 3.0.14 */
+#ifndef EFI_IMAGE_MACHINE_RISCV64
+ #define EFI_IMAGE_MACHINE_RISCV64 0x5064
+#endif
+
+/* gnu-efi 3.0.14 */
+#ifndef EFI_DTB_TABLE_GUID
+#define EFI_DTB_TABLE_GUID \
+ { 0xb1b621d5, 0xf19c, 0x41a5, {0x83, 0x0b, 0xd9, 0x15, 0x2c, 0x69, 0xaa, 0xe0} }
+#define EfiDtbTableGuid ((EFI_GUID)EFI_DTB_TABLE_GUID)
+#endif
+
+#ifndef EFI_DT_FIXUP_PROTOCOL_GUID
+#define EFI_DT_FIXUP_PROTOCOL_GUID \
+ { 0xe617d64c, 0xfe08, 0x46da, {0xf4, 0xdc, 0xbb, 0xd5, 0x87, 0x0c, 0x73, 0x00} }
+#define EfiDtFixupProtocol ((EFI_GUID)EFI_DT_FIXUP_PROTOCOL_GUID)
+
+#define EFI_DT_FIXUP_PROTOCOL_REVISION 0x00010000
+
+/* Add nodes and update properties */
+#define EFI_DT_APPLY_FIXUPS 0x00000001
+/*
+ * Reserve memory according to the /reserved-memory node
+ * and the memory reservation block
+ */
+#define EFI_DT_RESERVE_MEMORY 0x00000002
+
+typedef struct _EFI_DT_FIXUP_PROTOCOL EFI_DT_FIXUP_PROTOCOL;
+
+typedef EFI_STATUS (EFIAPI *EFI_DT_FIXUP) (
+ IN EFI_DT_FIXUP_PROTOCOL *This,
+ IN VOID *Fdt,
+ IN OUT UINTN *BufferSize,
+ IN UINT32 Flags);
+
+struct _EFI_DT_FIXUP_PROTOCOL {
+ UINT64 Revision;
+ EFI_DT_FIXUP Fixup;
+};
+
+#endif
+
+/* TCG EFI Protocol Specification */
+#ifndef EFI_TCG_GUID
+
+#define EFI_TCG_GUID \
+ &(const EFI_GUID) { 0xf541796d, 0xa62e, 0x4954, { 0xa7, 0x75, 0x95, 0x84, 0xf6, 0x1b, 0x9c, 0xdd } }
+
+typedef struct _TCG_VERSION {
+ UINT8 Major;
+ UINT8 Minor;
+ UINT8 RevMajor;
+ UINT8 RevMinor;
+} TCG_VERSION;
+
+typedef struct tdEFI_TCG2_VERSION {
+ UINT8 Major;
+ UINT8 Minor;
+} EFI_TCG2_VERSION;
+
+typedef struct _TCG_BOOT_SERVICE_CAPABILITY {
+ UINT8 Size;
+ struct _TCG_VERSION StructureVersion;
+ struct _TCG_VERSION ProtocolSpecVersion;
+ UINT8 HashAlgorithmBitmap;
+ BOOLEAN TPMPresentFlag;
+ BOOLEAN TPMDeactivatedFlag;
+} TCG_BOOT_SERVICE_CAPABILITY;
+
+typedef struct tdTREE_BOOT_SERVICE_CAPABILITY {
+ UINT8 Size;
+ EFI_TCG2_VERSION StructureVersion;
+ EFI_TCG2_VERSION ProtocolVersion;
+ UINT32 HashAlgorithmBitmap;
+ UINT32 SupportedEventLogs;
+ BOOLEAN TrEEPresentFlag;
+ UINT16 MaxCommandSize;
+ UINT16 MaxResponseSize;
+ UINT32 ManufacturerID;
+} TREE_BOOT_SERVICE_CAPABILITY;
+
+typedef UINT32 TCG_ALGORITHM_ID;
+#define TCG_ALG_SHA 0x00000004 // The SHA1 algorithm
+
+#define SHA1_DIGEST_SIZE 20
+
+typedef struct _TCG_DIGEST {
+ UINT8 Digest[SHA1_DIGEST_SIZE];
+} TCG_DIGEST;
+
+#define EV_IPL 13
+
+typedef struct _TCG_PCR_EVENT {
+ UINT32 PCRIndex;
+ UINT32 EventType;
+ struct _TCG_DIGEST digest;
+ UINT32 EventSize;
+ UINT8 Event[1];
+} TCG_PCR_EVENT;
+
+INTERFACE_DECL(_EFI_TCG);
+
+typedef EFI_STATUS(EFIAPI * EFI_TCG_STATUS_CHECK) (IN struct _EFI_TCG * This,
+ OUT struct _TCG_BOOT_SERVICE_CAPABILITY * ProtocolCapability,
+ OUT UINT32 * TCGFeatureFlags,
+ OUT EFI_PHYSICAL_ADDRESS * EventLogLocation,
+ OUT EFI_PHYSICAL_ADDRESS * EventLogLastEntry);
+
+typedef EFI_STATUS(EFIAPI * EFI_TCG_HASH_ALL) (IN struct _EFI_TCG * This,
+ IN UINT8 * HashData,
+ IN UINT64 HashDataLen,
+ IN TCG_ALGORITHM_ID AlgorithmId,
+ IN OUT UINT64 * HashedDataLen, IN OUT UINT8 ** HashedDataResult);
+
+typedef EFI_STATUS(EFIAPI * EFI_TCG_LOG_EVENT) (IN struct _EFI_TCG * This,
+ IN struct _TCG_PCR_EVENT * TCGLogData,
+ IN OUT UINT32 * EventNumber, IN UINT32 Flags);
+
+typedef EFI_STATUS(EFIAPI * EFI_TCG_PASS_THROUGH_TO_TPM) (IN struct _EFI_TCG * This,
+ IN UINT32 TpmInputParameterBlockSize,
+ IN UINT8 * TpmInputParameterBlock,
+ IN UINT32 TpmOutputParameterBlockSize,
+ IN UINT8 * TpmOutputParameterBlock);
+
+typedef EFI_STATUS(EFIAPI * EFI_TCG_HASH_LOG_EXTEND_EVENT) (IN struct _EFI_TCG * This,
+ IN EFI_PHYSICAL_ADDRESS HashData,
+ IN UINT64 HashDataLen,
+ IN TCG_ALGORITHM_ID AlgorithmId,
+ IN struct _TCG_PCR_EVENT * TCGLogData,
+ IN OUT UINT32 * EventNumber,
+ OUT EFI_PHYSICAL_ADDRESS * EventLogLastEntry);
+
+typedef struct _EFI_TCG {
+ EFI_TCG_STATUS_CHECK StatusCheck;
+ EFI_TCG_HASH_ALL HashAll;
+ EFI_TCG_LOG_EVENT LogEvent;
+ EFI_TCG_PASS_THROUGH_TO_TPM PassThroughToTPM;
+ EFI_TCG_HASH_LOG_EXTEND_EVENT HashLogExtendEvent;
+} EFI_TCG;
+
+#endif
+
+/* TCG EFI Protocol Specification */
+#ifndef EFI_TCG2_GUID
+
+#define EFI_TCG2_GUID \
+ &(const EFI_GUID) { 0x607f766c, 0x7455, 0x42be, { 0x93, 0x0b, 0xe4, 0xd7, 0x6d, 0xb2, 0x72, 0x0f } }
+
+typedef struct tdEFI_TCG2_PROTOCOL EFI_TCG2_PROTOCOL;
+
+typedef UINT32 EFI_TCG2_EVENT_LOG_BITMAP;
+typedef UINT32 EFI_TCG2_EVENT_LOG_FORMAT;
+typedef UINT32 EFI_TCG2_EVENT_ALGORITHM_BITMAP;
+
+typedef struct tdEFI_TCG2_BOOT_SERVICE_CAPABILITY {
+ UINT8 Size;
+ EFI_TCG2_VERSION StructureVersion;
+ EFI_TCG2_VERSION ProtocolVersion;
+ EFI_TCG2_EVENT_ALGORITHM_BITMAP HashAlgorithmBitmap;
+ EFI_TCG2_EVENT_LOG_BITMAP SupportedEventLogs;
+ BOOLEAN TPMPresentFlag;
+ UINT16 MaxCommandSize;
+ UINT16 MaxResponseSize;
+ UINT32 ManufacturerID;
+ UINT32 NumberOfPCRBanks;
+ EFI_TCG2_EVENT_ALGORITHM_BITMAP ActivePcrBanks;
+} EFI_TCG2_BOOT_SERVICE_CAPABILITY;
+
+#define EFI_TCG2_EVENT_HEADER_VERSION 1
+
+typedef struct {
+ UINT32 HeaderSize;
+ UINT16 HeaderVersion;
+ UINT32 PCRIndex;
+ UINT32 EventType;
+} _packed_ EFI_TCG2_EVENT_HEADER;
+
+typedef struct tdEFI_TCG2_EVENT {
+ UINT32 Size;
+ EFI_TCG2_EVENT_HEADER Header;
+ UINT8 Event[1];
+} _packed_ EFI_TCG2_EVENT;
+
+typedef EFI_STATUS(EFIAPI * EFI_TCG2_GET_CAPABILITY) (IN EFI_TCG2_PROTOCOL * This,
+ IN OUT EFI_TCG2_BOOT_SERVICE_CAPABILITY * ProtocolCapability);
+
+typedef EFI_STATUS(EFIAPI * EFI_TCG2_GET_EVENT_LOG) (IN EFI_TCG2_PROTOCOL * This,
+ IN EFI_TCG2_EVENT_LOG_FORMAT EventLogFormat,
+ OUT EFI_PHYSICAL_ADDRESS * EventLogLocation,
+ OUT EFI_PHYSICAL_ADDRESS * EventLogLastEntry,
+ OUT BOOLEAN * EventLogTruncated);
+
+typedef EFI_STATUS(EFIAPI * EFI_TCG2_HASH_LOG_EXTEND_EVENT) (IN EFI_TCG2_PROTOCOL * This,
+ IN UINT64 Flags,
+ IN EFI_PHYSICAL_ADDRESS DataToHash,
+ IN UINT64 DataToHashLen, IN EFI_TCG2_EVENT * EfiTcgEvent);
+
+typedef EFI_STATUS(EFIAPI * EFI_TCG2_SUBMIT_COMMAND) (IN EFI_TCG2_PROTOCOL * This,
+ IN UINT32 InputParameterBlockSize,
+ IN UINT8 * InputParameterBlock,
+ IN UINT32 OutputParameterBlockSize, IN UINT8 * OutputParameterBlock);
+
+typedef EFI_STATUS(EFIAPI * EFI_TCG2_GET_ACTIVE_PCR_BANKS) (IN EFI_TCG2_PROTOCOL * This, OUT UINT32 * ActivePcrBanks);
+
+typedef EFI_STATUS(EFIAPI * EFI_TCG2_SET_ACTIVE_PCR_BANKS) (IN EFI_TCG2_PROTOCOL * This, IN UINT32 ActivePcrBanks);
+
+typedef EFI_STATUS(EFIAPI * EFI_TCG2_GET_RESULT_OF_SET_ACTIVE_PCR_BANKS) (IN EFI_TCG2_PROTOCOL * This,
+ OUT UINT32 * OperationPresent, OUT UINT32 * Response);
+
+typedef struct tdEFI_TCG2_PROTOCOL {
+ EFI_TCG2_GET_CAPABILITY GetCapability;
+ EFI_TCG2_GET_EVENT_LOG GetEventLog;
+ EFI_TCG2_HASH_LOG_EXTEND_EVENT HashLogExtendEvent;
+ EFI_TCG2_SUBMIT_COMMAND SubmitCommand;
+ EFI_TCG2_GET_ACTIVE_PCR_BANKS GetActivePcrBanks;
+ EFI_TCG2_SET_ACTIVE_PCR_BANKS SetActivePcrBanks;
+ EFI_TCG2_GET_RESULT_OF_SET_ACTIVE_PCR_BANKS GetResultOfSetActivePcrBanks;
+} EFI_TCG2;
+
+#endif
+
+#ifndef EFI_LOAD_FILE2_PROTOCOL_GUID
+#define EFI_LOAD_FILE2_PROTOCOL_GUID \
+ {0x4006c0c1, 0xfcb3, 0x403e, {0x99, 0x6d, 0x4a, 0x6c, 0x87, 0x24, 0xe0, 0x6d} }
+#define EfiLoadFile2Protocol ((EFI_GUID)EFI_LOAD_FILE2_PROTOCOL_GUID)
+#endif
+
+#define LINUX_INITRD_MEDIA_GUID \
+ {0x5568e427, 0x68fc, 0x4f3d, {0xac, 0x74, 0xca, 0x55, 0x52, 0x31, 0xcc, 0x68} }
+
+/* UEFI Platform Initialization (Vol2: DXE) */
+#ifndef EFI_SECURITY_ARCH_PROTOCOL_GUID
+
+#define EFI_SECURITY_ARCH_PROTOCOL_GUID \
+ { 0xa46423e3, 0x4617, 0x49f1, { 0xb9, 0xff, 0xd1, 0xbf, 0xa9, 0x11, 0x58, 0x39 } }
+#define EFI_SECURITY2_ARCH_PROTOCOL_GUID \
+ { 0x94ab2f58, 0x1438, 0x4ef1, { 0x91, 0x52, 0x18, 0x94, 0x1a, 0x3a, 0x0e, 0x68 } }
+
+typedef struct EFI_SECURITY_ARCH_PROTOCOL EFI_SECURITY_ARCH_PROTOCOL;
+typedef struct EFI_SECURITY2_ARCH_PROTOCOL EFI_SECURITY2_ARCH_PROTOCOL;
+
+typedef EFI_STATUS (EFIAPI *EFI_SECURITY_FILE_AUTHENTICATION_STATE)(
+ const EFI_SECURITY_ARCH_PROTOCOL *This,
+ uint32_t AuthenticationStatus,
+ const EFI_DEVICE_PATH *File);
+
+struct EFI_SECURITY_ARCH_PROTOCOL {
+ EFI_SECURITY_FILE_AUTHENTICATION_STATE FileAuthenticationState;
+};
+
+typedef EFI_STATUS (EFIAPI *EFI_SECURITY2_FILE_AUTHENTICATION)(
+ const EFI_SECURITY2_ARCH_PROTOCOL *This,
+ const EFI_DEVICE_PATH *DevicePath,
+ void *FileBuffer,
+ UINTN FileSize,
+ BOOLEAN BootPolicy);
+
+struct EFI_SECURITY2_ARCH_PROTOCOL {
+ EFI_SECURITY2_FILE_AUTHENTICATION FileAuthentication;
+};
+
+#endif
+
+#ifndef EFI_CONSOLE_CONTROL_GUID
+
+#define EFI_CONSOLE_CONTROL_GUID \
+ &(const EFI_GUID) { 0xf42f7782, 0x12e, 0x4c12, { 0x99, 0x56, 0x49, 0xf9, 0x43, 0x4, 0xf7, 0x21 } }
+
+struct _EFI_CONSOLE_CONTROL_PROTOCOL;
+
+typedef enum {
+ EfiConsoleControlScreenText,
+ EfiConsoleControlScreenGraphics,
+ EfiConsoleControlScreenMaxValue,
+} EFI_CONSOLE_CONTROL_SCREEN_MODE;
+
+typedef EFI_STATUS (EFIAPI *EFI_CONSOLE_CONTROL_PROTOCOL_GET_MODE)(
+ struct _EFI_CONSOLE_CONTROL_PROTOCOL *This,
+ EFI_CONSOLE_CONTROL_SCREEN_MODE *Mode,
+ BOOLEAN *UgaExists,
+ BOOLEAN *StdInLocked
+);
+
+typedef EFI_STATUS (EFIAPI *EFI_CONSOLE_CONTROL_PROTOCOL_SET_MODE)(
+ struct _EFI_CONSOLE_CONTROL_PROTOCOL *This,
+ EFI_CONSOLE_CONTROL_SCREEN_MODE Mode
+);
+
+typedef EFI_STATUS (EFIAPI *EFI_CONSOLE_CONTROL_PROTOCOL_LOCK_STD_IN)(
+ struct _EFI_CONSOLE_CONTROL_PROTOCOL *This,
+ CHAR16 *Password
+);
+
+typedef struct _EFI_CONSOLE_CONTROL_PROTOCOL {
+ EFI_CONSOLE_CONTROL_PROTOCOL_GET_MODE GetMode;
+ EFI_CONSOLE_CONTROL_PROTOCOL_SET_MODE SetMode;
+ EFI_CONSOLE_CONTROL_PROTOCOL_LOCK_STD_IN LockStdIn;
+} EFI_CONSOLE_CONTROL_PROTOCOL;
+
+#endif
+
+#ifndef EFI_IMAGE_SECURITY_DATABASE_VARIABLE
+
+#define EFI_IMAGE_SECURITY_DATABASE_VARIABLE \
+ { 0xd719b2cb, 0x3d3a, 0x4596, {0xa3, 0xbc, 0xda, 0xd0, 0xe, 0x67, 0x65, 0x6f }}
+
+#endif
+
+#ifndef EFI_SHELL_PARAMETERS_PROTOCOL_GUID
+# define EFI_SHELL_PARAMETERS_PROTOCOL_GUID \
+ { 0x752f3136, 0x4e16, 0x4fdc, { 0xa2, 0x2a, 0xe5, 0xf4, 0x68, 0x12, 0xf4, 0xca } }
+
+typedef struct {
+ CHAR16 **Argv;
+ UINTN Argc;
+ void *StdIn;
+ void *StdOut;
+ void *StdErr;
+} EFI_SHELL_PARAMETERS_PROTOCOL;
+#endif
diff --git a/src/boot/efi/pe.c b/src/boot/efi/pe.c
new file mode 100644
index 0000000..3d5da14
--- /dev/null
+++ b/src/boot/efi/pe.c
@@ -0,0 +1,338 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <efi.h>
+#include <efilib.h>
+
+#include "missing_efi.h"
+#include "pe.h"
+#include "util.h"
+
+#define DOS_FILE_MAGIC "MZ"
+#define PE_FILE_MAGIC "PE\0\0"
+#define MAX_SECTIONS 96
+
+#if defined(__i386__)
+# define TARGET_MACHINE_TYPE EFI_IMAGE_MACHINE_IA32
+# define TARGET_MACHINE_TYPE_COMPATIBILITY EFI_IMAGE_MACHINE_X64
+#elif defined(__x86_64__)
+# define TARGET_MACHINE_TYPE EFI_IMAGE_MACHINE_X64
+#elif defined(__aarch64__)
+# define TARGET_MACHINE_TYPE EFI_IMAGE_MACHINE_AARCH64
+#elif defined(__arm__)
+# define TARGET_MACHINE_TYPE EFI_IMAGE_MACHINE_ARMTHUMB_MIXED
+#elif defined(__riscv) && __riscv_xlen == 64
+# define TARGET_MACHINE_TYPE EFI_IMAGE_MACHINE_RISCV64
+#else
+# error Unknown EFI arch
+#endif
+
+#ifndef TARGET_MACHINE_TYPE_COMPATIBILITY
+# define TARGET_MACHINE_TYPE_COMPATIBILITY 0
+#endif
+
+typedef struct DosFileHeader {
+ uint8_t Magic[2];
+ uint16_t LastSize;
+ uint16_t nBlocks;
+ uint16_t nReloc;
+ uint16_t HdrSize;
+ uint16_t MinAlloc;
+ uint16_t MaxAlloc;
+ uint16_t ss;
+ uint16_t sp;
+ uint16_t Checksum;
+ uint16_t ip;
+ uint16_t cs;
+ uint16_t RelocPos;
+ uint16_t nOverlay;
+ uint16_t reserved[4];
+ uint16_t OEMId;
+ uint16_t OEMInfo;
+ uint16_t reserved2[10];
+ uint32_t ExeHeader;
+} _packed_ DosFileHeader;
+
+typedef struct CoffFileHeader {
+ uint16_t Machine;
+ uint16_t NumberOfSections;
+ uint32_t TimeDateStamp;
+ uint32_t PointerToSymbolTable;
+ uint32_t NumberOfSymbols;
+ uint16_t SizeOfOptionalHeader;
+ uint16_t Characteristics;
+} _packed_ CoffFileHeader;
+
+#define OPTHDR32_MAGIC 0x10B /* PE32 OptionalHeader */
+#define OPTHDR64_MAGIC 0x20B /* PE32+ OptionalHeader */
+
+typedef struct PeOptionalHeader {
+ uint16_t Magic;
+ uint8_t LinkerMajor;
+ uint8_t LinkerMinor;
+ uint32_t SizeOfCode;
+ uint32_t SizeOfInitializedData;
+ uint32_t SizeOfUninitializeData;
+ uint32_t AddressOfEntryPoint;
+ uint32_t BaseOfCode;
+ union {
+ struct { /* PE32 */
+ uint32_t BaseOfData;
+ uint32_t ImageBase32;
+ };
+ uint64_t ImageBase64; /* PE32+ */
+ };
+ uint32_t SectionAlignment;
+ uint32_t FileAlignment;
+ uint16_t MajorOperatingSystemVersion;
+ uint16_t MinorOperatingSystemVersion;
+ uint16_t MajorImageVersion;
+ uint16_t MinorImageVersion;
+ uint16_t MajorSubsystemVersion;
+ uint16_t MinorSubsystemVersion;
+ uint32_t Win32VersionValue;
+ uint32_t SizeOfImage;
+ uint32_t SizeOfHeaders;
+ uint32_t CheckSum;
+ uint16_t Subsystem;
+ uint16_t DllCharacteristics;
+ /* fields with different sizes for 32/64 omitted */
+} _packed_ PeOptionalHeader;
+
+typedef struct PeFileHeader {
+ uint8_t Magic[4];
+ CoffFileHeader FileHeader;
+ PeOptionalHeader OptionalHeader;
+} _packed_ PeFileHeader;
+
+typedef struct PeSectionHeader {
+ uint8_t Name[8];
+ uint32_t VirtualSize;
+ uint32_t VirtualAddress;
+ uint32_t SizeOfRawData;
+ uint32_t PointerToRawData;
+ uint32_t PointerToRelocations;
+ uint32_t PointerToLinenumbers;
+ uint16_t NumberOfRelocations;
+ uint16_t NumberOfLinenumbers;
+ uint32_t Characteristics;
+} _packed_ PeSectionHeader;
+
+static inline bool verify_dos(const DosFileHeader *dos) {
+ assert(dos);
+ return memcmp(dos->Magic, DOS_FILE_MAGIC, STRLEN(DOS_FILE_MAGIC)) == 0;
+}
+
+static inline bool verify_pe(const PeFileHeader *pe, bool allow_compatibility) {
+ assert(pe);
+ return memcmp(pe->Magic, PE_FILE_MAGIC, STRLEN(PE_FILE_MAGIC)) == 0 &&
+ (pe->FileHeader.Machine == TARGET_MACHINE_TYPE ||
+ (allow_compatibility && pe->FileHeader.Machine == TARGET_MACHINE_TYPE_COMPATIBILITY)) &&
+ pe->FileHeader.NumberOfSections > 0 &&
+ pe->FileHeader.NumberOfSections <= MAX_SECTIONS &&
+ IN_SET(pe->OptionalHeader.Magic, OPTHDR32_MAGIC, OPTHDR64_MAGIC);
+}
+
+static inline UINTN section_table_offset(const DosFileHeader *dos, const PeFileHeader *pe) {
+ assert(dos);
+ assert(pe);
+ return dos->ExeHeader + offsetof(PeFileHeader, OptionalHeader) + pe->FileHeader.SizeOfOptionalHeader;
+}
+
+static void locate_sections(
+ const PeSectionHeader section_table[],
+ UINTN n_table,
+ const char * const sections[],
+ UINTN *offsets,
+ UINTN *sizes,
+ bool in_memory) {
+
+ assert(section_table);
+ assert(sections);
+ assert(offsets);
+ assert(sizes);
+
+ size_t prev_section_addr = 0;
+
+ for (UINTN i = 0; i < n_table; i++) {
+ const PeSectionHeader *sect = section_table + i;
+
+ if (in_memory) {
+ if (prev_section_addr > sect->VirtualAddress)
+ log_error_stall(u"Overlapping PE sections detected. Boot may fail due to image memory corruption!");
+ prev_section_addr = sect->VirtualAddress + sect->VirtualSize;
+ }
+
+ for (UINTN j = 0; sections[j]; j++) {
+ if (memcmp(sect->Name, sections[j], strlen8(sections[j])) != 0)
+ continue;
+
+ offsets[j] = in_memory ? sect->VirtualAddress : sect->PointerToRawData;
+ sizes[j] = sect->VirtualSize;
+ }
+ }
+}
+
+static uint32_t get_compatibility_entry_address(const DosFileHeader *dos, const PeFileHeader *pe) {
+ UINTN addr = 0, size = 0;
+ static const char *sections[] = { ".compat", NULL };
+
+ /* The kernel may provide alternative PE entry points for different PE architectures. This allows
+ * booting a 64bit kernel on 32bit EFI that is otherwise running on a 64bit CPU. The locations of any
+ * such compat entry points are located in a special PE section. */
+
+ locate_sections((const PeSectionHeader *) ((const uint8_t *) dos + section_table_offset(dos, pe)),
+ pe->FileHeader.NumberOfSections,
+ sections,
+ &addr,
+ &size,
+ /*in_memory=*/true);
+
+ if (size == 0)
+ return 0;
+
+ typedef struct {
+ uint8_t type;
+ uint8_t size;
+ uint16_t machine_type;
+ uint32_t entry_point;
+ } _packed_ LinuxPeCompat1;
+
+ while (size >= sizeof(LinuxPeCompat1) && addr % __alignof__(LinuxPeCompat1) == 0) {
+ LinuxPeCompat1 *compat = (LinuxPeCompat1 *) ((uint8_t *) dos + addr);
+
+ if (compat->type == 0 || compat->size == 0 || compat->size > size)
+ break;
+
+ if (compat->type == 1 &&
+ compat->size >= sizeof(LinuxPeCompat1) &&
+ compat->machine_type == TARGET_MACHINE_TYPE)
+ return compat->entry_point;
+
+ addr += compat->size;
+ size -= compat->size;
+ }
+
+ return 0;
+}
+
+EFI_STATUS pe_kernel_info(const void *base, uint32_t *ret_compat_address) {
+ assert(base);
+ assert(ret_compat_address);
+
+ const DosFileHeader *dos = (const DosFileHeader *) base;
+ if (!verify_dos(dos))
+ return EFI_LOAD_ERROR;
+
+ const PeFileHeader *pe = (const PeFileHeader *) ((const uint8_t *) base + dos->ExeHeader);
+ if (!verify_pe(pe, /* allow_compatibility= */ true))
+ return EFI_LOAD_ERROR;
+
+ /* Support for LINUX_INITRD_MEDIA_GUID was added in kernel stub 1.0. */
+ if (pe->OptionalHeader.MajorImageVersion < 1)
+ return EFI_UNSUPPORTED;
+
+ if (pe->FileHeader.Machine == TARGET_MACHINE_TYPE) {
+ *ret_compat_address = 0;
+ return EFI_SUCCESS;
+ }
+
+ uint32_t compat_address = get_compatibility_entry_address(dos, pe);
+ if (compat_address == 0)
+ /* Image type not supported and no compat entry found. */
+ return EFI_UNSUPPORTED;
+
+ *ret_compat_address = compat_address;
+ return EFI_SUCCESS;
+}
+
+EFI_STATUS pe_memory_locate_sections(const void *base, const char * const sections[], UINTN *addrs, UINTN *sizes) {
+ const DosFileHeader *dos;
+ const PeFileHeader *pe;
+ UINTN offset;
+
+ assert(base);
+ assert(sections);
+ assert(addrs);
+ assert(sizes);
+
+ dos = (const DosFileHeader *) base;
+ if (!verify_dos(dos))
+ return EFI_LOAD_ERROR;
+
+ pe = (const PeFileHeader *) ((uint8_t *) base + dos->ExeHeader);
+ if (!verify_pe(pe, /* allow_compatibility= */ false))
+ return EFI_LOAD_ERROR;
+
+ offset = section_table_offset(dos, pe);
+ locate_sections((PeSectionHeader *) ((uint8_t *) base + offset),
+ pe->FileHeader.NumberOfSections,
+ sections,
+ addrs,
+ sizes,
+ /*in_memory=*/true);
+
+ return EFI_SUCCESS;
+}
+
+EFI_STATUS pe_file_locate_sections(
+ EFI_FILE *dir,
+ const char16_t *path,
+ const char * const sections[],
+ UINTN *offsets,
+ UINTN *sizes) {
+ _cleanup_free_ PeSectionHeader *section_table = NULL;
+ _cleanup_(file_closep) EFI_FILE *handle = NULL;
+ DosFileHeader dos;
+ PeFileHeader pe;
+ UINTN len, section_table_len;
+ EFI_STATUS err;
+
+ assert(dir);
+ assert(path);
+ assert(sections);
+ assert(offsets);
+ assert(sizes);
+
+ err = dir->Open(dir, &handle, (char16_t *) path, EFI_FILE_MODE_READ, 0ULL);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ len = sizeof(dos);
+ err = handle->Read(handle, &len, &dos);
+ if (err != EFI_SUCCESS)
+ return err;
+ if (len != sizeof(dos) || !verify_dos(&dos))
+ return EFI_LOAD_ERROR;
+
+ err = handle->SetPosition(handle, dos.ExeHeader);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ len = sizeof(pe);
+ err = handle->Read(handle, &len, &pe);
+ if (err != EFI_SUCCESS)
+ return err;
+ if (len != sizeof(pe) || !verify_pe(&pe, /* allow_compatibility= */ false))
+ return EFI_LOAD_ERROR;
+
+ section_table_len = pe.FileHeader.NumberOfSections * sizeof(PeSectionHeader);
+ section_table = xmalloc(section_table_len);
+ if (!section_table)
+ return EFI_OUT_OF_RESOURCES;
+
+ err = handle->SetPosition(handle, section_table_offset(&dos, &pe));
+ if (err != EFI_SUCCESS)
+ return err;
+
+ len = section_table_len;
+ err = handle->Read(handle, &len, section_table);
+ if (err != EFI_SUCCESS)
+ return err;
+ if (len != section_table_len)
+ return EFI_LOAD_ERROR;
+
+ locate_sections(section_table, pe.FileHeader.NumberOfSections,
+ sections, offsets, sizes, /*in_memory=*/false);
+
+ return EFI_SUCCESS;
+}
diff --git a/src/boot/efi/pe.h b/src/boot/efi/pe.h
new file mode 100644
index 0000000..ff7ff47
--- /dev/null
+++ b/src/boot/efi/pe.h
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <efidef.h>
+#include <uchar.h>
+
+EFI_STATUS pe_memory_locate_sections(
+ const void *base,
+ const char * const sections[],
+ UINTN *addrs,
+ UINTN *sizes);
+
+EFI_STATUS pe_file_locate_sections(
+ EFI_FILE *dir,
+ const char16_t *path,
+ const char * const sections[],
+ UINTN *offsets,
+ UINTN *sizes);
+
+EFI_STATUS pe_kernel_info(const void *base, uint32_t *ret_compat_address);
diff --git a/src/boot/efi/random-seed.c b/src/boot/efi/random-seed.c
new file mode 100644
index 0000000..aea4f7e
--- /dev/null
+++ b/src/boot/efi/random-seed.c
@@ -0,0 +1,321 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <efi.h>
+#include <efilib.h>
+
+#include "missing_efi.h"
+#include "random-seed.h"
+#include "secure-boot.h"
+#include "sha256.h"
+#include "util.h"
+
+#define RANDOM_MAX_SIZE_MIN (32U)
+#define RANDOM_MAX_SIZE_MAX (32U*1024U)
+
+#define EFI_RNG_GUID &(const EFI_GUID) EFI_RNG_PROTOCOL_GUID
+
+/* SHA256 gives us 256/8=32 bytes */
+#define HASH_VALUE_SIZE 32
+
+static EFI_STATUS acquire_rng(UINTN size, void **ret) {
+ _cleanup_free_ void *data = NULL;
+ EFI_RNG_PROTOCOL *rng;
+ EFI_STATUS err;
+
+ assert(ret);
+
+ /* Try to acquire the specified number of bytes from the UEFI RNG */
+
+ err = BS->LocateProtocol((EFI_GUID *) EFI_RNG_GUID, NULL, (void **) &rng);
+ if (err != EFI_SUCCESS)
+ return err;
+ if (!rng)
+ return EFI_UNSUPPORTED;
+
+ data = xmalloc(size);
+
+ err = rng->GetRNG(rng, NULL, size, data);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Failed to acquire RNG data: %r", err);
+
+ *ret = TAKE_PTR(data);
+ return EFI_SUCCESS;
+}
+
+static void hash_once(
+ const void *old_seed,
+ const void *rng,
+ UINTN size,
+ const void *system_token,
+ UINTN system_token_size,
+ uint64_t uefi_monotonic_counter,
+ UINTN counter,
+ uint8_t ret[static HASH_VALUE_SIZE]) {
+
+ /* This hashes together:
+ *
+ * 1. The contents of the old seed file
+ * 2. Some random data acquired from the UEFI RNG (optional)
+ * 3. Some 'system token' the installer installed as EFI variable (optional)
+ * 4. The UEFI "monotonic counter" that increases with each boot
+ * 5. A supplied counter value
+ *
+ * And writes the result to the specified buffer.
+ */
+
+ struct sha256_ctx hash;
+
+ assert(old_seed);
+ assert(system_token_size == 0 || system_token);
+
+ sha256_init_ctx(&hash);
+ sha256_process_bytes(old_seed, size, &hash);
+ if (rng)
+ sha256_process_bytes(rng, size, &hash);
+ if (system_token_size > 0)
+ sha256_process_bytes(system_token, system_token_size, &hash);
+ sha256_process_bytes(&uefi_monotonic_counter, sizeof(uefi_monotonic_counter), &hash);
+ sha256_process_bytes(&counter, sizeof(counter), &hash);
+ sha256_finish_ctx(&hash, ret);
+}
+
+static EFI_STATUS hash_many(
+ const void *old_seed,
+ const void *rng,
+ UINTN size,
+ const void *system_token,
+ UINTN system_token_size,
+ uint64_t uefi_monotonic_counter,
+ UINTN counter_start,
+ UINTN n,
+ void **ret) {
+
+ _cleanup_free_ void *output = NULL;
+
+ assert(old_seed);
+ assert(system_token_size == 0 || system_token);
+ assert(ret);
+
+ /* Hashes the specified parameters in counter mode, generating n hash values, with the counter in the
+ * range counter_start…counter_start+n-1. */
+
+ output = xmalloc_multiply(HASH_VALUE_SIZE, n);
+
+ for (UINTN i = 0; i < n; i++)
+ hash_once(old_seed, rng, size,
+ system_token, system_token_size,
+ uefi_monotonic_counter,
+ counter_start + i,
+ (uint8_t*) output + (i * HASH_VALUE_SIZE));
+
+ *ret = TAKE_PTR(output);
+ return EFI_SUCCESS;
+}
+
+static EFI_STATUS mangle_random_seed(
+ const void *old_seed,
+ const void *rng,
+ UINTN size,
+ const void *system_token,
+ UINTN system_token_size,
+ uint64_t uefi_monotonic_counter,
+ void **ret_new_seed,
+ void **ret_for_kernel) {
+
+ _cleanup_free_ void *new_seed = NULL, *for_kernel = NULL;
+ EFI_STATUS err;
+ UINTN n;
+
+ assert(old_seed);
+ assert(system_token_size == 0 || system_token);
+ assert(ret_new_seed);
+ assert(ret_for_kernel);
+
+ /* This takes the old seed file contents, an (optional) random number acquired from the UEFI RNG, an
+ * (optional) system 'token' installed once by the OS installer in an EFI variable, and hashes them
+ * together in counter mode, generating a new seed (to replace the file on disk) and the seed for the
+ * kernel. To keep things simple, the new seed and kernel data have the same size as the old seed and
+ * RNG data. */
+
+ n = (size + HASH_VALUE_SIZE - 1) / HASH_VALUE_SIZE;
+
+ /* Begin hashing in counter mode at counter 0 for the new seed for the disk */
+ err = hash_many(old_seed, rng, size, system_token, system_token_size, uefi_monotonic_counter, 0, n, &new_seed);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ /* Continue counting at 'n' for the seed for the kernel */
+ err = hash_many(old_seed, rng, size, system_token, system_token_size, uefi_monotonic_counter, n, n, &for_kernel);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ *ret_new_seed = TAKE_PTR(new_seed);
+ *ret_for_kernel = TAKE_PTR(for_kernel);
+
+ return EFI_SUCCESS;
+}
+
+static EFI_STATUS acquire_system_token(void **ret, UINTN *ret_size) {
+ _cleanup_free_ char *data = NULL;
+ EFI_STATUS err;
+ UINTN size;
+
+ assert(ret);
+ assert(ret_size);
+
+ err = efivar_get_raw(LOADER_GUID, L"LoaderSystemToken", &data, &size);
+ if (err != EFI_SUCCESS) {
+ if (err != EFI_NOT_FOUND)
+ log_error_stall(L"Failed to read LoaderSystemToken EFI variable: %r", err);
+ return err;
+ }
+
+ if (size <= 0)
+ return log_error_status_stall(EFI_NOT_FOUND, L"System token too short, ignoring.");
+
+ *ret = TAKE_PTR(data);
+ *ret_size = size;
+
+ return EFI_SUCCESS;
+}
+
+static void validate_sha256(void) {
+
+#ifdef EFI_DEBUG
+ /* Let's validate our SHA256 implementation. We stole it from glibc, and converted it to UEFI
+ * style. We better check whether it does the right stuff. We use the simpler test vectors from the
+ * SHA spec. Note that we strip this out in optimization builds. */
+
+ static const struct {
+ const char *string;
+ uint8_t hash[HASH_VALUE_SIZE];
+ } array[] = {
+ { "abc",
+ { 0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea,
+ 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23,
+ 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c,
+ 0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad }},
+
+ { "",
+ { 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14,
+ 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24,
+ 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c,
+ 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55 }},
+
+ { "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
+ { 0x24, 0x8d, 0x6a, 0x61, 0xd2, 0x06, 0x38, 0xb8,
+ 0xe5, 0xc0, 0x26, 0x93, 0x0c, 0x3e, 0x60, 0x39,
+ 0xa3, 0x3c, 0xe4, 0x59, 0x64, 0xff, 0x21, 0x67,
+ 0xf6, 0xec, 0xed, 0xd4, 0x19, 0xdb, 0x06, 0xc1 }},
+
+ { "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu",
+ { 0xcf, 0x5b, 0x16, 0xa7, 0x78, 0xaf, 0x83, 0x80,
+ 0x03, 0x6c, 0xe5, 0x9e, 0x7b, 0x04, 0x92, 0x37,
+ 0x0b, 0x24, 0x9b, 0x11, 0xe8, 0xf0, 0x7a, 0x51,
+ 0xaf, 0xac, 0x45, 0x03, 0x7a, 0xfe, 0xe9, 0xd1 }},
+ };
+
+ for (UINTN i = 0; i < ELEMENTSOF(array); i++)
+ assert(memcmp(SHA256_DIRECT(array[i].string, strlen8(array[i].string)), array[i].hash, HASH_VALUE_SIZE) == 0);
+#endif
+}
+
+EFI_STATUS process_random_seed(EFI_FILE *root_dir, RandomSeedMode mode) {
+ _cleanup_free_ void *seed = NULL, *new_seed = NULL, *rng = NULL, *for_kernel = NULL, *system_token = NULL;
+ _cleanup_(file_closep) EFI_FILE *handle = NULL;
+ UINTN size, rsize, wsize, system_token_size = 0;
+ _cleanup_free_ EFI_FILE_INFO *info = NULL;
+ uint64_t uefi_monotonic_counter = 0;
+ EFI_STATUS err;
+
+ assert(root_dir);
+
+ validate_sha256();
+
+ if (mode == RANDOM_SEED_OFF)
+ return EFI_NOT_FOUND;
+
+ /* Let's better be safe than sorry, and for now disable this logic in SecureBoot mode, so that we
+ * don't credit a random seed that is not authenticated. */
+ if (secure_boot_enabled())
+ return EFI_NOT_FOUND;
+
+ /* Get some system specific seed that the installer might have placed in an EFI variable. We include
+ * it in our hash. This is protection against golden master image sloppiness, and it remains on the
+ * system, even when disk images are duplicated or swapped out. */
+ err = acquire_system_token(&system_token, &system_token_size);
+ if (mode != RANDOM_SEED_ALWAYS && err != EFI_SUCCESS)
+ return err;
+
+ err = root_dir->Open(
+ root_dir,
+ &handle,
+ (char16_t *) L"\\loader\\random-seed",
+ EFI_FILE_MODE_READ | EFI_FILE_MODE_WRITE,
+ 0);
+ if (err != EFI_SUCCESS) {
+ if (err != EFI_NOT_FOUND && err != EFI_WRITE_PROTECTED)
+ log_error_stall(L"Failed to open random seed file: %r", err);
+ return err;
+ }
+
+ err = get_file_info_harder(handle, &info, NULL);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Failed to get file info for random seed: %r");
+
+ size = info->FileSize;
+ if (size < RANDOM_MAX_SIZE_MIN)
+ return log_error_status_stall(EFI_INVALID_PARAMETER, L"Random seed file is too short.");
+
+ if (size > RANDOM_MAX_SIZE_MAX)
+ return log_error_status_stall(EFI_INVALID_PARAMETER, L"Random seed file is too large.");
+
+ seed = xmalloc(size);
+
+ rsize = size;
+ err = handle->Read(handle, &rsize, seed);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Failed to read random seed file: %r", err);
+ if (rsize != size)
+ return log_error_status_stall(EFI_PROTOCOL_ERROR, L"Short read on random seed file.");
+
+ err = handle->SetPosition(handle, 0);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Failed to seek to beginning of random seed file: %r", err);
+
+ /* Request some random data from the UEFI RNG. We don't need this to work safely, but it's a good
+ * idea to use it because it helps us for cases where users mistakenly include a random seed in
+ * golden master images that are replicated many times. */
+ (void) acquire_rng(size, &rng); /* It's fine if this fails */
+
+ /* Let's also include the UEFI monotonic counter (which is supposedly increasing on every single
+ * boot) in the hash, so that even if the changes to the ESP for some reason should not be
+ * persistent, the random seed we generate will still be different on every single boot. */
+ err = BS->GetNextMonotonicCount(&uefi_monotonic_counter);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Failed to acquire UEFI monotonic counter: %r", err);
+
+ /* Calculate new random seed for the disk and what to pass to the kernel */
+ err = mangle_random_seed(seed, rng, size, system_token, system_token_size, uefi_monotonic_counter, &new_seed, &for_kernel);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ /* Update the random seed on disk before we use it */
+ wsize = size;
+ err = handle->Write(handle, &wsize, new_seed);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Failed to write random seed file: %r", err);
+ if (wsize != size)
+ return log_error_status_stall(EFI_PROTOCOL_ERROR, L"Short write on random seed file.");
+
+ err = handle->Flush(handle);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Failed to flush random seed file: %r", err);
+
+ /* We are good to go */
+ err = efivar_set_raw(LOADER_GUID, L"LoaderRandomSeed", for_kernel, size, 0);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Failed to write random seed to EFI variable: %r", err);
+
+ return EFI_SUCCESS;
+}
diff --git a/src/boot/efi/random-seed.h b/src/boot/efi/random-seed.h
new file mode 100644
index 0000000..6aa1cc5
--- /dev/null
+++ b/src/boot/efi/random-seed.h
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <efi.h>
+#include <errno.h>
+#include <uchar.h>
+
+typedef enum RandomSeedMode {
+ RANDOM_SEED_OFF,
+ RANDOM_SEED_WITH_SYSTEM_TOKEN,
+ RANDOM_SEED_ALWAYS,
+ _RANDOM_SEED_MODE_MAX,
+ _RANDOM_SEED_MODE_INVALID = -EINVAL,
+} RandomSeedMode;
+
+static const char16_t * const random_seed_modes_table[_RANDOM_SEED_MODE_MAX] = {
+ [RANDOM_SEED_OFF] = L"off",
+ [RANDOM_SEED_WITH_SYSTEM_TOKEN] = L"with-system-token",
+ [RANDOM_SEED_ALWAYS] = L"always",
+};
+
+EFI_STATUS process_random_seed(EFI_FILE *root_dir, RandomSeedMode mode);
diff --git a/src/boot/efi/secure-boot.c b/src/boot/efi/secure-boot.c
new file mode 100644
index 0000000..6212868
--- /dev/null
+++ b/src/boot/efi/secure-boot.c
@@ -0,0 +1,217 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sbat.h"
+#include "secure-boot.h"
+#include "console.h"
+#include "util.h"
+
+bool secure_boot_enabled(void) {
+ bool secure = false; /* avoid false maybe-uninitialized warning */
+ EFI_STATUS err;
+
+ err = efivar_get_boolean_u8(EFI_GLOBAL_GUID, L"SecureBoot", &secure);
+
+ return err == EFI_SUCCESS && secure;
+}
+
+SecureBootMode secure_boot_mode(void) {
+ bool secure, audit = false, deployed = false, setup = false;
+ EFI_STATUS err;
+
+ err = efivar_get_boolean_u8(EFI_GLOBAL_GUID, L"SecureBoot", &secure);
+ if (err != EFI_SUCCESS)
+ return SECURE_BOOT_UNSUPPORTED;
+
+ /* We can assume false for all these if they are abscent (AuditMode and
+ * DeployedMode may not exist on older firmware). */
+ (void) efivar_get_boolean_u8(EFI_GLOBAL_GUID, L"AuditMode", &audit);
+ (void) efivar_get_boolean_u8(EFI_GLOBAL_GUID, L"DeployedMode", &deployed);
+ (void) efivar_get_boolean_u8(EFI_GLOBAL_GUID, L"SetupMode", &setup);
+
+ return decode_secure_boot_mode(secure, audit, deployed, setup);
+}
+
+#ifdef SBAT_DISTRO
+static const char sbat[] _used_ _section_(".sbat") = SBAT_SECTION_TEXT;
+#endif
+
+EFI_STATUS secure_boot_enroll_at(EFI_FILE *root_dir, const char16_t *path) {
+ assert(root_dir);
+ assert(path);
+
+ EFI_STATUS err;
+
+ clear_screen(COLOR_NORMAL);
+
+ Print(L"Enrolling secure boot keys from directory: %s\n"
+ L"Warning: Enrolling custom Secure Boot keys might soft-brick your machine!\n",
+ path);
+
+ unsigned timeout_sec = 15;
+ for(;;) {
+ /* Enrolling secure boot keys is safe to do in virtualized environments as there is nothing
+ * we can brick there. */
+ if (in_hypervisor())
+ break;
+
+ PrintAt(0, ST->ConOut->Mode->CursorRow, L"Enrolling in %2u s, press any key to abort.", timeout_sec);
+
+ uint64_t key;
+ err = console_key_read(&key, 1000 * 1000);
+ if (err == EFI_NOT_READY)
+ continue;
+ if (err == EFI_TIMEOUT) {
+ if (timeout_sec == 0) /* continue enrolling keys */
+ break;
+ timeout_sec--;
+ continue;
+ }
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Error waiting for user input to enroll Secure Boot keys: %r", err);
+
+ /* user aborted, returning EFI_SUCCESS here allows the user to go back to the menu */
+ return EFI_SUCCESS;
+ }
+
+ _cleanup_(file_closep) EFI_FILE *dir = NULL;
+
+ err = open_directory(root_dir, path, &dir);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Failed opening keys directory %s: %r", path, err);
+
+ struct {
+ const char16_t *name;
+ const char16_t *filename;
+ const EFI_GUID vendor;
+ char *buffer;
+ size_t size;
+ } sb_vars[] = {
+ { u"db", u"db.auth", EFI_IMAGE_SECURITY_DATABASE_VARIABLE, NULL, 0 },
+ { u"KEK", u"KEK.auth", EFI_GLOBAL_VARIABLE, NULL, 0 },
+ { u"PK", u"PK.auth", EFI_GLOBAL_VARIABLE, NULL, 0 },
+ };
+
+ /* Make sure all keys files exist before we start enrolling them by loading them from the disk first. */
+ for (size_t i = 0; i < ELEMENTSOF(sb_vars); i++) {
+ err = file_read(dir, sb_vars[i].filename, 0, 0, &sb_vars[i].buffer, &sb_vars[i].size);
+ if (err != EFI_SUCCESS) {
+ log_error_stall(L"Failed reading file %s\\%s: %r", path, sb_vars[i].filename, err);
+ goto out_deallocate;
+ }
+ }
+
+ for (size_t i = 0; i < ELEMENTSOF(sb_vars); i++) {
+ uint32_t sb_vars_opts =
+ EFI_VARIABLE_NON_VOLATILE |
+ EFI_VARIABLE_BOOTSERVICE_ACCESS |
+ EFI_VARIABLE_RUNTIME_ACCESS |
+ EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS;
+
+ err = efivar_set_raw(&sb_vars[i].vendor, sb_vars[i].name, sb_vars[i].buffer, sb_vars[i].size, sb_vars_opts);
+ if (err != EFI_SUCCESS) {
+ log_error_stall(L"Failed to write %s secure boot variable: %r", sb_vars[i].name, err);
+ goto out_deallocate;
+ }
+ }
+
+ /* The system should be in secure boot mode now and we could continue a regular boot. But at least
+ * TPM PCR7 measurements should change on next boot. Reboot now so that any OS we load does not end
+ * up relying on the old PCR state. */
+ RT->ResetSystem(EfiResetCold, EFI_SUCCESS, 0, NULL);
+ assert_not_reached();
+
+out_deallocate:
+ for (size_t i = 0; i < ELEMENTSOF(sb_vars); i++)
+ FreePool(sb_vars[i].buffer);
+
+ return err;
+}
+
+static struct SecurityOverride {
+ EFI_SECURITY_ARCH_PROTOCOL *security;
+ EFI_SECURITY2_ARCH_PROTOCOL *security2;
+ EFI_SECURITY_FILE_AUTHENTICATION_STATE original_hook;
+ EFI_SECURITY2_FILE_AUTHENTICATION original_hook2;
+
+ security_validator_t validator;
+ const void *validator_ctx;
+} security_override;
+
+static EFIAPI EFI_STATUS security_hook(
+ const EFI_SECURITY_ARCH_PROTOCOL *this,
+ uint32_t authentication_status,
+ const EFI_DEVICE_PATH *file) {
+
+ assert(security_override.validator);
+ assert(security_override.security);
+ assert(security_override.original_hook);
+
+ if (security_override.validator(security_override.validator_ctx, file, NULL, 0))
+ return EFI_SUCCESS;
+
+ return security_override.original_hook(security_override.security, authentication_status, file);
+}
+
+static EFIAPI EFI_STATUS security2_hook(
+ const EFI_SECURITY2_ARCH_PROTOCOL *this,
+ const EFI_DEVICE_PATH *device_path,
+ void *file_buffer,
+ size_t file_size,
+ BOOLEAN boot_policy) {
+
+ assert(security_override.validator);
+ assert(security_override.security2);
+ assert(security_override.original_hook2);
+
+ if (security_override.validator(security_override.validator_ctx, device_path, file_buffer, file_size))
+ return EFI_SUCCESS;
+
+ return security_override.original_hook2(
+ security_override.security2, device_path, file_buffer, file_size, boot_policy);
+}
+
+/* This replaces the platform provided security arch protocols hooks (defined in the UEFI Platform
+ * Initialization Specification) with our own that uses the given validator to decide if a image is to be
+ * trusted. If not running in secure boot or the protocols are not available nothing happens. The override
+ * must be removed with uninstall_security_override() after LoadImage() has been called.
+ *
+ * This is a hack as we do not own the security protocol instances and modifying them is not an official part
+ * of their spec. But there is little else we can do to circumvent secure boot short of implementing our own
+ * PE loader. We could replace the firmware instances with our own instance using
+ * ReinstallProtocolInterface(), but some firmware will still use the old ones. */
+void install_security_override(security_validator_t validator, const void *validator_ctx) {
+ EFI_STATUS err;
+
+ assert(validator);
+
+ if (!secure_boot_enabled())
+ return;
+
+ security_override = (struct SecurityOverride) {
+ .validator = validator,
+ .validator_ctx = validator_ctx,
+ };
+
+ EFI_SECURITY_ARCH_PROTOCOL *security = NULL;
+ err = BS->LocateProtocol(&(EFI_GUID) EFI_SECURITY_ARCH_PROTOCOL_GUID, NULL, (void **) &security);
+ if (err == EFI_SUCCESS) {
+ security_override.security = security;
+ security_override.original_hook = security->FileAuthenticationState;
+ security->FileAuthenticationState = security_hook;
+ }
+
+ EFI_SECURITY2_ARCH_PROTOCOL *security2 = NULL;
+ err = BS->LocateProtocol(&(EFI_GUID) EFI_SECURITY2_ARCH_PROTOCOL_GUID, NULL, (void **) &security2);
+ if (err == EFI_SUCCESS) {
+ security_override.security2 = security2;
+ security_override.original_hook2 = security2->FileAuthentication;
+ security2->FileAuthentication = security2_hook;
+ }
+}
+
+void uninstall_security_override(void) {
+ if (security_override.original_hook)
+ security_override.security->FileAuthenticationState = security_override.original_hook;
+ if (security_override.original_hook2)
+ security_override.security2->FileAuthentication = security_override.original_hook2;
+}
diff --git a/src/boot/efi/secure-boot.h b/src/boot/efi/secure-boot.h
new file mode 100644
index 0000000..e98de81
--- /dev/null
+++ b/src/boot/efi/secure-boot.h
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <efi.h>
+
+#include "efivars-fundamental.h"
+#include "missing_efi.h"
+
+typedef enum {
+ ENROLL_OFF, /* no Secure Boot key enrollment whatsoever, even manual entries are not generated */
+ ENROLL_MANUAL, /* Secure Boot key enrollment is strictly manual: manual entries are generated and need to be selected by the user */
+ ENROLL_FORCE, /* Secure Boot key enrollment may be automatic if it is available but might not be safe */
+} secure_boot_enroll;
+
+bool secure_boot_enabled(void);
+SecureBootMode secure_boot_mode(void);
+
+EFI_STATUS secure_boot_enroll_at(EFI_FILE *root_dir, const char16_t *path);
+
+typedef bool (*security_validator_t)(
+ const void *ctx,
+ const EFI_DEVICE_PATH *device_path,
+ const void *file_buffer,
+ size_t file_size);
+
+void install_security_override(security_validator_t validator, const void *validator_ctx);
+void uninstall_security_override(void);
diff --git a/src/boot/efi/shim.c b/src/boot/efi/shim.c
new file mode 100644
index 0000000..ac22433
--- /dev/null
+++ b/src/boot/efi/shim.c
@@ -0,0 +1,101 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Port to systemd-boot
+ * Copyright © 2017 Max Resch <resch.max@gmail.com>
+ *
+ * Security Policy Handling
+ * Copyright © 2012 <James.Bottomley@HansenPartnership.com>
+ * https://github.com/mjg59/efitools
+ */
+
+#include <efi.h>
+#include <efilib.h>
+
+#include "missing_efi.h"
+#include "util.h"
+#include "secure-boot.h"
+#include "shim.h"
+
+#if defined(__x86_64__) || defined(__i386__)
+#define __sysv_abi__ __attribute__((sysv_abi))
+#else
+#define __sysv_abi__
+#endif
+
+struct ShimLock {
+ EFI_STATUS __sysv_abi__ (*shim_verify) (const void *buffer, uint32_t size);
+
+ /* context is actually a struct for the PE header, but it isn't needed so void is sufficient just do define the interface
+ * see shim.c/shim.h and PeHeader.h in the github shim repo */
+ EFI_STATUS __sysv_abi__ (*generate_hash) (void *data, uint32_t datasize, void *context, uint8_t *sha256hash, uint8_t *sha1hash);
+
+ EFI_STATUS __sysv_abi__ (*read_header) (void *data, uint32_t datasize, void *context);
+};
+
+#define SHIM_LOCK_GUID \
+ &(const EFI_GUID) { 0x605dab50, 0xe046, 0x4300, { 0xab, 0xb6, 0x3d, 0xd8, 0x10, 0xdd, 0x8b, 0x23 } }
+
+bool shim_loaded(void) {
+ struct ShimLock *shim_lock;
+
+ return BS->LocateProtocol((EFI_GUID*) SHIM_LOCK_GUID, NULL, (void**) &shim_lock) == EFI_SUCCESS;
+}
+
+static bool shim_validate(
+ const void *ctx, const EFI_DEVICE_PATH *device_path, const void *file_buffer, size_t file_size) {
+
+ EFI_STATUS err;
+ _cleanup_free_ char *file_buffer_owned = NULL;
+
+ if (!file_buffer) {
+ if (!device_path)
+ return false;
+
+ EFI_HANDLE device_handle;
+ EFI_DEVICE_PATH *file_dp = (EFI_DEVICE_PATH *) device_path;
+ err = BS->LocateDevicePath(&FileSystemProtocol, &file_dp, &device_handle);
+ if (err != EFI_SUCCESS)
+ return false;
+
+ _cleanup_(file_closep) EFI_FILE *root = NULL;
+ err = open_volume(device_handle, &root);
+ if (err != EFI_SUCCESS)
+ return false;
+
+ _cleanup_free_ char16_t *dp_str = NULL;
+ err = device_path_to_str(file_dp, &dp_str);
+ if (err != EFI_SUCCESS)
+ return false;
+
+ err = file_read(root, dp_str, 0, 0, &file_buffer_owned, &file_size);
+ if (err != EFI_SUCCESS)
+ return false;
+
+ file_buffer = file_buffer_owned;
+ }
+
+ struct ShimLock *shim_lock;
+ err = BS->LocateProtocol((EFI_GUID *) SHIM_LOCK_GUID, NULL, (void **) &shim_lock);
+ if (err != EFI_SUCCESS)
+ return false;
+
+ return shim_lock->shim_verify(file_buffer, file_size) == EFI_SUCCESS;
+}
+
+EFI_STATUS shim_load_image(EFI_HANDLE parent, const EFI_DEVICE_PATH *device_path, EFI_HANDLE *ret_image) {
+ assert(device_path);
+ assert(ret_image);
+
+ bool have_shim = shim_loaded();
+
+ if (have_shim)
+ install_security_override(shim_validate, NULL);
+
+ EFI_STATUS ret = BS->LoadImage(
+ /*BootPolicy=*/false, parent, (EFI_DEVICE_PATH *) device_path, NULL, 0, ret_image);
+
+ if (have_shim)
+ uninstall_security_override();
+
+ return ret;
+}
diff --git a/src/boot/efi/shim.h b/src/boot/efi/shim.h
new file mode 100644
index 0000000..6d213f5
--- /dev/null
+++ b/src/boot/efi/shim.h
@@ -0,0 +1,16 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Port to systemd-boot
+ * Copyright © 2017 Max Resch <resch.max@gmail.com>
+ *
+ * Security Policy Handling
+ * Copyright © 2012 <James.Bottomley@HansenPartnership.com>
+ * https://github.com/mjg59/efitools
+ */
+#pragma once
+
+#include <efi.h>
+#include <stdbool.h>
+
+bool shim_loaded(void);
+EFI_STATUS shim_load_image(EFI_HANDLE parent, const EFI_DEVICE_PATH *device_path, EFI_HANDLE *ret_image);
diff --git a/src/boot/efi/splash.c b/src/boot/efi/splash.c
new file mode 100644
index 0000000..5bc1084
--- /dev/null
+++ b/src/boot/efi/splash.c
@@ -0,0 +1,322 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <efi.h>
+#include <efilib.h>
+
+#include "graphics.h"
+#include "splash.h"
+#include "util.h"
+
+struct bmp_file {
+ char signature[2];
+ uint32_t size;
+ uint16_t reserved[2];
+ uint32_t offset;
+} _packed_;
+
+/* we require at least BITMAPINFOHEADER, later versions are
+ accepted, but their features ignored */
+struct bmp_dib {
+ uint32_t size;
+ uint32_t x;
+ uint32_t y;
+ uint16_t planes;
+ uint16_t depth;
+ uint32_t compression;
+ uint32_t image_size;
+ int32_t x_pixel_meter;
+ int32_t y_pixel_meter;
+ uint32_t colors_used;
+ uint32_t colors_important;
+} _packed_;
+
+struct bmp_map {
+ uint8_t blue;
+ uint8_t green;
+ uint8_t red;
+ uint8_t reserved;
+} _packed_;
+
+static EFI_STATUS bmp_parse_header(
+ const uint8_t *bmp,
+ UINTN size,
+ struct bmp_dib **ret_dib,
+ struct bmp_map **ret_map,
+ const uint8_t **pixmap) {
+
+ struct bmp_file *file;
+ struct bmp_dib *dib;
+ struct bmp_map *map;
+ UINTN row_size;
+
+ assert(bmp);
+ assert(ret_dib);
+ assert(ret_map);
+ assert(pixmap);
+
+ if (size < sizeof(struct bmp_file) + sizeof(struct bmp_dib))
+ return EFI_INVALID_PARAMETER;
+
+ /* check file header */
+ file = (struct bmp_file *)bmp;
+ if (file->signature[0] != 'B' || file->signature[1] != 'M')
+ return EFI_INVALID_PARAMETER;
+ if (file->size != size)
+ return EFI_INVALID_PARAMETER;
+ if (file->size < file->offset)
+ return EFI_INVALID_PARAMETER;
+
+ /* check device-independent bitmap */
+ dib = (struct bmp_dib *)(bmp + sizeof(struct bmp_file));
+ if (dib->size < sizeof(struct bmp_dib))
+ return EFI_UNSUPPORTED;
+
+ switch (dib->depth) {
+ case 1:
+ case 4:
+ case 8:
+ case 24:
+ if (dib->compression != 0)
+ return EFI_UNSUPPORTED;
+
+ break;
+
+ case 16:
+ case 32:
+ if (dib->compression != 0 && dib->compression != 3)
+ return EFI_UNSUPPORTED;
+
+ break;
+
+ default:
+ return EFI_UNSUPPORTED;
+ }
+
+ row_size = ((UINTN) dib->depth * dib->x + 31) / 32 * 4;
+ if (file->size - file->offset < dib->y * row_size)
+ return EFI_INVALID_PARAMETER;
+ if (row_size * dib->y > 64 * 1024 * 1024)
+ return EFI_INVALID_PARAMETER;
+
+ /* check color table */
+ map = (struct bmp_map *)(bmp + sizeof(struct bmp_file) + dib->size);
+ if (file->offset < sizeof(struct bmp_file) + dib->size)
+ return EFI_INVALID_PARAMETER;
+
+ if (file->offset > sizeof(struct bmp_file) + dib->size) {
+ uint32_t map_count;
+ UINTN map_size;
+
+ if (dib->colors_used)
+ map_count = dib->colors_used;
+ else {
+ switch (dib->depth) {
+ case 1:
+ case 4:
+ case 8:
+ map_count = 1 << dib->depth;
+ break;
+
+ default:
+ map_count = 0;
+ break;
+ }
+ }
+
+ map_size = file->offset - (sizeof(struct bmp_file) + dib->size);
+ if (map_size != sizeof(struct bmp_map) * map_count)
+ return EFI_INVALID_PARAMETER;
+ }
+
+ *ret_map = map;
+ *ret_dib = dib;
+ *pixmap = bmp + file->offset;
+
+ return EFI_SUCCESS;
+}
+
+static void pixel_blend(uint32_t *dst, const uint32_t source) {
+ uint32_t alpha, src, src_rb, src_g, dst_rb, dst_g, rb, g;
+
+ assert(dst);
+
+ alpha = (source & 0xff);
+
+ /* convert src from RGBA to XRGB */
+ src = source >> 8;
+
+ /* decompose into RB and G components */
+ src_rb = (src & 0xff00ff);
+ src_g = (src & 0x00ff00);
+
+ dst_rb = (*dst & 0xff00ff);
+ dst_g = (*dst & 0x00ff00);
+
+ /* blend */
+ rb = ((((src_rb - dst_rb) * alpha + 0x800080) >> 8) + dst_rb) & 0xff00ff;
+ g = ((((src_g - dst_g) * alpha + 0x008000) >> 8) + dst_g) & 0x00ff00;
+
+ *dst = (rb | g);
+}
+
+static EFI_STATUS bmp_to_blt(
+ EFI_GRAPHICS_OUTPUT_BLT_PIXEL *buf,
+ struct bmp_dib *dib,
+ struct bmp_map *map,
+ const uint8_t *pixmap) {
+
+ const uint8_t *in;
+
+ assert(buf);
+ assert(dib);
+ assert(map);
+ assert(pixmap);
+
+ /* transform and copy pixels */
+ in = pixmap;
+ for (UINTN y = 0; y < dib->y; y++) {
+ EFI_GRAPHICS_OUTPUT_BLT_PIXEL *out;
+ UINTN row_size;
+
+ out = &buf[(dib->y - y - 1) * dib->x];
+ for (UINTN x = 0; x < dib->x; x++, in++, out++) {
+ switch (dib->depth) {
+ case 1: {
+ for (UINTN i = 0; i < 8 && x < dib->x; i++) {
+ out->Red = map[((*in) >> (7 - i)) & 1].red;
+ out->Green = map[((*in) >> (7 - i)) & 1].green;
+ out->Blue = map[((*in) >> (7 - i)) & 1].blue;
+ out++;
+ x++;
+ }
+ out--;
+ x--;
+ break;
+ }
+
+ case 4: {
+ UINTN i;
+
+ i = (*in) >> 4;
+ out->Red = map[i].red;
+ out->Green = map[i].green;
+ out->Blue = map[i].blue;
+ if (x < (dib->x - 1)) {
+ out++;
+ x++;
+ i = (*in) & 0x0f;
+ out->Red = map[i].red;
+ out->Green = map[i].green;
+ out->Blue = map[i].blue;
+ }
+ break;
+ }
+
+ case 8:
+ out->Red = map[*in].red;
+ out->Green = map[*in].green;
+ out->Blue = map[*in].blue;
+ break;
+
+ case 16: {
+ uint16_t i = *(uint16_t *) in;
+
+ out->Red = (i & 0x7c00) >> 7;
+ out->Green = (i & 0x3e0) >> 2;
+ out->Blue = (i & 0x1f) << 3;
+ in += 1;
+ break;
+ }
+
+ case 24:
+ out->Red = in[2];
+ out->Green = in[1];
+ out->Blue = in[0];
+ in += 2;
+ break;
+
+ case 32: {
+ uint32_t i = *(uint32_t *) in;
+
+ pixel_blend((uint32_t *)out, i);
+
+ in += 3;
+ break;
+ }
+ }
+ }
+
+ /* add row padding; new lines always start at 32 bit boundary */
+ row_size = in - pixmap;
+ in += ((row_size + 3) & ~3) - row_size;
+ }
+
+ return EFI_SUCCESS;
+}
+
+EFI_STATUS graphics_splash(const uint8_t *content, UINTN len) {
+ EFI_GRAPHICS_OUTPUT_BLT_PIXEL background = {};
+ EFI_GRAPHICS_OUTPUT_PROTOCOL *GraphicsOutput = NULL;
+ struct bmp_dib *dib;
+ struct bmp_map *map;
+ const uint8_t *pixmap;
+ _cleanup_free_ void *blt = NULL;
+ UINTN x_pos = 0;
+ UINTN y_pos = 0;
+ EFI_STATUS err;
+
+ if (len == 0)
+ return EFI_SUCCESS;
+
+ assert(content);
+
+ if (strcaseeq16(ST->FirmwareVendor, u"Apple")) {
+ background.Red = 0xc0;
+ background.Green = 0xc0;
+ background.Blue = 0xc0;
+ }
+
+ err = BS->LocateProtocol(&GraphicsOutputProtocol, NULL, (void **) &GraphicsOutput);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ err = bmp_parse_header(content, len, &dib, &map, &pixmap);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ if (dib->x < GraphicsOutput->Mode->Info->HorizontalResolution)
+ x_pos = (GraphicsOutput->Mode->Info->HorizontalResolution - dib->x) / 2;
+ if (dib->y < GraphicsOutput->Mode->Info->VerticalResolution)
+ y_pos = (GraphicsOutput->Mode->Info->VerticalResolution - dib->y) / 2;
+
+ err = GraphicsOutput->Blt(
+ GraphicsOutput, &background,
+ EfiBltVideoFill, 0, 0, 0, 0,
+ GraphicsOutput->Mode->Info->HorizontalResolution,
+ GraphicsOutput->Mode->Info->VerticalResolution, 0);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ /* EFI buffer */
+ blt = xnew(EFI_GRAPHICS_OUTPUT_BLT_PIXEL, dib->x * dib->y);
+
+ err = GraphicsOutput->Blt(
+ GraphicsOutput, blt,
+ EfiBltVideoToBltBuffer, x_pos, y_pos, 0, 0,
+ dib->x, dib->y, 0);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ err = bmp_to_blt(blt, dib, map, pixmap);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ err = graphics_mode(true);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ return GraphicsOutput->Blt(
+ GraphicsOutput, blt,
+ EfiBltBufferToVideo, 0, 0, x_pos, y_pos,
+ dib->x, dib->y, 0);
+}
diff --git a/src/boot/efi/splash.h b/src/boot/efi/splash.h
new file mode 100644
index 0000000..2e502e5
--- /dev/null
+++ b/src/boot/efi/splash.h
@@ -0,0 +1,6 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <efi.h>
+
+EFI_STATUS graphics_splash(const uint8_t *content, UINTN len);
diff --git a/src/boot/efi/stub.c b/src/boot/efi/stub.c
new file mode 100644
index 0000000..7c42a16
--- /dev/null
+++ b/src/boot/efi/stub.c
@@ -0,0 +1,415 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <efi.h>
+#include <efilib.h>
+
+#include "cpio.h"
+#include "devicetree.h"
+#include "disk.h"
+#include "graphics.h"
+#include "linux.h"
+#include "measure.h"
+#include "pe.h"
+#include "secure-boot.h"
+#include "splash.h"
+#include "tpm-pcr.h"
+#include "util.h"
+
+/* magic string to find in the binary image */
+_used_ _section_(".sdmagic") static const char magic[] = "#### LoaderInfo: systemd-stub " GIT_VERSION " ####";
+
+static EFI_STATUS combine_initrd(
+ EFI_PHYSICAL_ADDRESS initrd_base, UINTN initrd_size,
+ const void * const extra_initrds[], const size_t extra_initrd_sizes[], size_t n_extra_initrds,
+ Pages *ret_initr_pages, UINTN *ret_initrd_size) {
+
+ UINTN n;
+
+ assert(ret_initr_pages);
+ assert(ret_initrd_size);
+
+ /* Combines four initrds into one, by simple concatenation in memory */
+
+ n = ALIGN4(initrd_size); /* main initrd might not be padded yet */
+
+ for (size_t i = 0; i < n_extra_initrds; i++) {
+ if (!extra_initrds[i])
+ continue;
+
+ if (n > UINTN_MAX - extra_initrd_sizes[i])
+ return EFI_OUT_OF_RESOURCES;
+
+ n += extra_initrd_sizes[i];
+ }
+
+ _cleanup_pages_ Pages pages = xmalloc_pages(
+ AllocateMaxAddress,
+ EfiLoaderData,
+ EFI_SIZE_TO_PAGES(n),
+ UINT32_MAX /* Below 4G boundary. */);
+ uint8_t *p = PHYSICAL_ADDRESS_TO_POINTER(pages.addr);
+ if (initrd_base != 0) {
+ UINTN pad;
+
+ /* Order matters, the real initrd must come first, since it might include microcode updates
+ * which the kernel only looks for in the first cpio archive */
+ p = mempcpy(p, PHYSICAL_ADDRESS_TO_POINTER(initrd_base), initrd_size);
+
+ pad = ALIGN4(initrd_size) - initrd_size;
+ if (pad > 0) {
+ memset(p, 0, pad);
+ p += pad;
+ }
+ }
+
+ for (size_t i = 0; i < n_extra_initrds; i++) {
+ if (!extra_initrds[i])
+ continue;
+
+ p = mempcpy(p, extra_initrds[i], extra_initrd_sizes[i]);
+ }
+
+ assert(PHYSICAL_ADDRESS_TO_POINTER(pages.addr + n) == p);
+
+ *ret_initr_pages = pages;
+ *ret_initrd_size = n;
+ pages.n_pages = 0;
+
+ return EFI_SUCCESS;
+}
+
+static void export_variables(EFI_LOADED_IMAGE_PROTOCOL *loaded_image) {
+ static const uint64_t stub_features =
+ EFI_STUB_FEATURE_REPORT_BOOT_PARTITION | /* We set LoaderDevicePartUUID */
+ EFI_STUB_FEATURE_PICK_UP_CREDENTIALS | /* We pick up credentials from the boot partition */
+ EFI_STUB_FEATURE_PICK_UP_SYSEXTS | /* We pick up system extensions from the boot partition */
+ EFI_STUB_FEATURE_THREE_PCRS | /* We can measure kernel image, parameters and sysext */
+ 0;
+
+ char16_t uuid[37];
+
+ assert(loaded_image);
+
+ /* Export the device path this image is started from, if it's not set yet */
+ if (efivar_get_raw(LOADER_GUID, L"LoaderDevicePartUUID", NULL, NULL) != EFI_SUCCESS)
+ if (disk_get_part_uuid(loaded_image->DeviceHandle, uuid) == EFI_SUCCESS)
+ efivar_set(LOADER_GUID, L"LoaderDevicePartUUID", uuid, 0);
+
+ /* If LoaderImageIdentifier is not set, assume the image with this stub was loaded directly from the
+ * UEFI firmware without any boot loader, and hence set the LoaderImageIdentifier ourselves. Note
+ * that some boot chain loaders neither set LoaderImageIdentifier nor make FilePath available to us,
+ * in which case there's simple nothing to set for us. (The UEFI spec doesn't really say who's wrong
+ * here, i.e. whether FilePath may be NULL or not, hence handle this gracefully and check if FilePath
+ * is non-NULL explicitly.) */
+ if (efivar_get_raw(LOADER_GUID, L"LoaderImageIdentifier", NULL, NULL) != EFI_SUCCESS &&
+ loaded_image->FilePath) {
+ _cleanup_free_ char16_t *s = NULL;
+ if (device_path_to_str(loaded_image->FilePath, &s) == EFI_SUCCESS)
+ efivar_set(LOADER_GUID, L"LoaderImageIdentifier", s, 0);
+ }
+
+ /* if LoaderFirmwareInfo is not set, let's set it */
+ if (efivar_get_raw(LOADER_GUID, L"LoaderFirmwareInfo", NULL, NULL) != EFI_SUCCESS) {
+ _cleanup_free_ char16_t *s = NULL;
+ s = xpool_print(L"%s %u.%02u", ST->FirmwareVendor, ST->FirmwareRevision >> 16, ST->FirmwareRevision & 0xffff);
+ efivar_set(LOADER_GUID, L"LoaderFirmwareInfo", s, 0);
+ }
+
+ /* ditto for LoaderFirmwareType */
+ if (efivar_get_raw(LOADER_GUID, L"LoaderFirmwareType", NULL, NULL) != EFI_SUCCESS) {
+ _cleanup_free_ char16_t *s = NULL;
+ s = xpool_print(L"UEFI %u.%02u", ST->Hdr.Revision >> 16, ST->Hdr.Revision & 0xffff);
+ efivar_set(LOADER_GUID, L"LoaderFirmwareType", s, 0);
+ }
+
+
+ /* add StubInfo (this is one is owned by the stub, hence we unconditionally override this with our
+ * own data) */
+ (void) efivar_set(LOADER_GUID, L"StubInfo", L"systemd-stub " GIT_VERSION, 0);
+
+ (void) efivar_set_uint64_le(LOADER_GUID, L"StubFeatures", stub_features, 0);
+}
+
+static bool use_load_options(
+ EFI_HANDLE stub_image,
+ EFI_LOADED_IMAGE_PROTOCOL *loaded_image,
+ bool have_cmdline,
+ char16_t **ret) {
+
+ assert(stub_image);
+ assert(loaded_image);
+ assert(ret);
+
+ /* We only allow custom command lines if we aren't in secure boot or if no cmdline was baked into
+ * the stub image. */
+ if (secure_boot_enabled() && have_cmdline)
+ return false;
+
+ /* We also do a superficial check whether first character of passed command line
+ * is printable character (for compat with some Dell systems which fill in garbage?). */
+ if (loaded_image->LoadOptionsSize < sizeof(char16_t) || ((char16_t *) loaded_image->LoadOptions)[0] <= 0x1F)
+ return false;
+
+ /* The UEFI shell registers EFI_SHELL_PARAMETERS_PROTOCOL onto images it runs. This lets us know that
+ * LoadOptions starts with the stub binary path which we want to strip off. */
+ EFI_SHELL_PARAMETERS_PROTOCOL *shell;
+ if (BS->HandleProtocol(stub_image, &(EFI_GUID) EFI_SHELL_PARAMETERS_PROTOCOL_GUID, (void **) &shell)
+ != EFI_SUCCESS) {
+ /* Not running from EFI shell, use entire LoadOptions. Note that LoadOptions is a void*, so
+ * it could be anything! */
+ *ret = xstrndup16(loaded_image->LoadOptions, loaded_image->LoadOptionsSize / sizeof(char16_t));
+ mangle_stub_cmdline(*ret);
+ return true;
+ }
+
+ if (shell->Argc < 2)
+ /* No arguments were provided? Then we fall back to built-in cmdline. */
+ return false;
+
+ /* Assemble the command line ourselves without our stub path. */
+ *ret = xstrdup16(shell->Argv[1]);
+ for (size_t i = 2; i < shell->Argc; i++) {
+ _cleanup_free_ char16_t *old = *ret;
+ *ret = xpool_print(u"%s %s", old, shell->Argv[i]);
+ }
+
+ mangle_stub_cmdline(*ret);
+ return true;
+}
+
+EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *sys_table) {
+ _cleanup_free_ void *credential_initrd = NULL, *global_credential_initrd = NULL, *sysext_initrd = NULL, *pcrsig_initrd = NULL, *pcrpkey_initrd = NULL;
+ size_t credential_initrd_size = 0, global_credential_initrd_size = 0, sysext_initrd_size = 0, pcrsig_initrd_size = 0, pcrpkey_initrd_size = 0;
+ size_t linux_size, initrd_size, dt_size;
+ EFI_PHYSICAL_ADDRESS linux_base, initrd_base, dt_base;
+ _cleanup_(devicetree_cleanup) struct devicetree_state dt_state = {};
+ EFI_LOADED_IMAGE_PROTOCOL *loaded_image;
+ size_t addrs[_UNIFIED_SECTION_MAX] = {}, szs[_UNIFIED_SECTION_MAX] = {};
+ _cleanup_free_ char16_t *cmdline = NULL;
+ int sections_measured = -1, parameters_measured = -1;
+ bool sysext_measured = false, m;
+ EFI_STATUS err;
+
+ InitializeLib(image, sys_table);
+ debug_hook(L"systemd-stub");
+ /* Uncomment the next line if you need to wait for debugger. */
+ // debug_break();
+
+ err = BS->OpenProtocol(
+ image,
+ &LoadedImageProtocol,
+ (void **)&loaded_image,
+ image,
+ NULL,
+ EFI_OPEN_PROTOCOL_GET_PROTOCOL);
+ if (err != EFI_SUCCESS)
+ return log_error_status_stall(err, L"Error getting a LoadedImageProtocol handle: %r", err);
+
+ err = pe_memory_locate_sections(loaded_image->ImageBase, unified_sections, addrs, szs);
+ if (err != EFI_SUCCESS || szs[UNIFIED_SECTION_LINUX] == 0) {
+ if (err == EFI_SUCCESS)
+ err = EFI_NOT_FOUND;
+ return log_error_status_stall(err, L"Unable to locate embedded .linux section: %r", err);
+ }
+
+ /* Measure all "payload" of this PE image into a separate PCR (i.e. where nothing else is written
+ * into so far), so that we have one PCR that we can nicely write policies against because it
+ * contains all static data of this image, and thus can be easily be pre-calculated. */
+ for (UnifiedSection section = 0; section < _UNIFIED_SECTION_MAX; section++) {
+
+ if (!unified_section_measure(section)) /* shall not measure? */
+ continue;
+
+ if (szs[section] == 0) /* not found */
+ continue;
+
+ m = false;
+
+ /* First measure the name of the section */
+ (void) tpm_log_event_ascii(
+ TPM_PCR_INDEX_KERNEL_IMAGE,
+ POINTER_TO_PHYSICAL_ADDRESS(unified_sections[section]),
+ strsize8(unified_sections[section]), /* including NUL byte */
+ unified_sections[section],
+ &m);
+
+ sections_measured = sections_measured < 0 ? m : (sections_measured && m);
+
+ /* Then measure the data of the section */
+ (void) tpm_log_event_ascii(
+ TPM_PCR_INDEX_KERNEL_IMAGE,
+ POINTER_TO_PHYSICAL_ADDRESS(loaded_image->ImageBase) + addrs[section],
+ szs[section],
+ unified_sections[section],
+ &m);
+
+ sections_measured = sections_measured < 0 ? m : (sections_measured && m);
+ }
+
+ /* After we are done, set an EFI variable that tells userspace this was done successfully, and encode
+ * in it which PCR was used. */
+ if (sections_measured > 0)
+ (void) efivar_set_uint_string(LOADER_GUID, L"StubPcrKernelImage", TPM_PCR_INDEX_KERNEL_IMAGE, 0);
+
+ /* Show splash screen as early as possible */
+ graphics_splash((const uint8_t*) loaded_image->ImageBase + addrs[UNIFIED_SECTION_SPLASH], szs[UNIFIED_SECTION_SPLASH]);
+
+ if (use_load_options(image, loaded_image, szs[UNIFIED_SECTION_CMDLINE] > 0, &cmdline)) {
+ /* Let's measure the passed kernel command line into the TPM. Note that this possibly
+ * duplicates what we already did in the boot menu, if that was already used. However, since
+ * we want the boot menu to support an EFI binary, and want to this stub to be usable from
+ * any boot menu, let's measure things anyway. */
+ m = false;
+ (void) tpm_log_load_options(cmdline, &m);
+ parameters_measured = m;
+ } else if (szs[UNIFIED_SECTION_CMDLINE] > 0) {
+ cmdline = xstrn8_to_16(
+ (char *) loaded_image->ImageBase + addrs[UNIFIED_SECTION_CMDLINE],
+ szs[UNIFIED_SECTION_CMDLINE]);
+ mangle_stub_cmdline(cmdline);
+ }
+
+ export_variables(loaded_image);
+
+ if (pack_cpio(loaded_image,
+ NULL,
+ L".cred",
+ ".extra/credentials",
+ /* dir_mode= */ 0500,
+ /* access_mode= */ 0400,
+ /* tpm_pcr= */ (uint32_t[]) { TPM_PCR_INDEX_KERNEL_PARAMETERS, TPM_PCR_INDEX_KERNEL_PARAMETERS_COMPAT },
+ /* n_tpm_pcr= */ 2,
+ L"Credentials initrd",
+ &credential_initrd,
+ &credential_initrd_size,
+ &m) == EFI_SUCCESS)
+ parameters_measured = parameters_measured < 0 ? m : (parameters_measured && m);
+
+ if (pack_cpio(loaded_image,
+ L"\\loader\\credentials",
+ L".cred",
+ ".extra/global_credentials",
+ /* dir_mode= */ 0500,
+ /* access_mode= */ 0400,
+ /* tpm_pcr= */ (uint32_t[]) { TPM_PCR_INDEX_KERNEL_PARAMETERS, TPM_PCR_INDEX_KERNEL_PARAMETERS_COMPAT },
+ /* n_tpm_pcr= */ 2,
+ L"Global credentials initrd",
+ &global_credential_initrd,
+ &global_credential_initrd_size,
+ &m) == EFI_SUCCESS)
+ parameters_measured = parameters_measured < 0 ? m : (parameters_measured && m);
+
+ if (pack_cpio(loaded_image,
+ NULL,
+ L".raw",
+ ".extra/sysext",
+ /* dir_mode= */ 0555,
+ /* access_mode= */ 0444,
+ /* tpm_pcr= */ (uint32_t[]) { TPM_PCR_INDEX_INITRD_SYSEXTS },
+ /* n_tpm_pcr= */ 1,
+ L"System extension initrd",
+ &sysext_initrd,
+ &sysext_initrd_size,
+ &m) == EFI_SUCCESS)
+ sysext_measured = m;
+
+ if (parameters_measured > 0)
+ (void) efivar_set_uint_string(LOADER_GUID, L"StubPcrKernelParameters", TPM_PCR_INDEX_KERNEL_PARAMETERS, 0);
+ if (sysext_measured)
+ (void) efivar_set_uint_string(LOADER_GUID, L"StubPcrInitRDSysExts", TPM_PCR_INDEX_INITRD_SYSEXTS, 0);
+
+ /* If the PCR signature was embedded in the PE image, then let's wrap it in a cpio and also pass it
+ * to the kernel, so that it can be read from /.extra/tpm2-pcr-signature.json. Note that this section
+ * is not measured, neither as raw section (see above), nor as cpio (here), because it is the
+ * signature of expected PCR values, i.e. its input are PCR measurements, and hence it shouldn't
+ * itself be input for PCR measurements. */
+ if (szs[UNIFIED_SECTION_PCRSIG] > 0)
+ (void) pack_cpio_literal(
+ (uint8_t*) loaded_image->ImageBase + addrs[UNIFIED_SECTION_PCRSIG],
+ szs[UNIFIED_SECTION_PCRSIG],
+ ".extra",
+ L"tpm2-pcr-signature.json",
+ /* dir_mode= */ 0555,
+ /* access_mode= */ 0444,
+ /* tpm_pcr= */ NULL,
+ /* n_tpm_pcr= */ 0,
+ /* tpm_description= */ NULL,
+ &pcrsig_initrd,
+ &pcrsig_initrd_size,
+ /* ret_measured= */ NULL);
+
+ /* If the public key used for the PCR signatures was embedded in the PE image, then let's wrap it in
+ * a cpio and also pass it to the kernel, so that it can be read from
+ * /.extra/tpm2-pcr-public-key.pem. This section is already measure above, hence we won't measure the
+ * cpio. */
+ if (szs[UNIFIED_SECTION_PCRPKEY] > 0)
+ (void) pack_cpio_literal(
+ (uint8_t*) loaded_image->ImageBase + addrs[UNIFIED_SECTION_PCRPKEY],
+ szs[UNIFIED_SECTION_PCRPKEY],
+ ".extra",
+ L"tpm2-pcr-public-key.pem",
+ /* dir_mode= */ 0555,
+ /* access_mode= */ 0444,
+ /* tpm_pcr= */ NULL,
+ /* n_tpm_pcr= */ 0,
+ /* tpm_description= */ NULL,
+ &pcrpkey_initrd,
+ &pcrpkey_initrd_size,
+ /* ret_measured= */ NULL);
+
+ linux_size = szs[UNIFIED_SECTION_LINUX];
+ linux_base = POINTER_TO_PHYSICAL_ADDRESS(loaded_image->ImageBase) + addrs[UNIFIED_SECTION_LINUX];
+
+ initrd_size = szs[UNIFIED_SECTION_INITRD];
+ initrd_base = initrd_size != 0 ? POINTER_TO_PHYSICAL_ADDRESS(loaded_image->ImageBase) + addrs[UNIFIED_SECTION_INITRD] : 0;
+
+ dt_size = szs[UNIFIED_SECTION_DTB];
+ dt_base = dt_size != 0 ? POINTER_TO_PHYSICAL_ADDRESS(loaded_image->ImageBase) + addrs[UNIFIED_SECTION_DTB] : 0;
+
+ _cleanup_pages_ Pages initrd_pages = {};
+ if (credential_initrd || global_credential_initrd || sysext_initrd || pcrsig_initrd || pcrpkey_initrd) {
+ /* If we have generated initrds dynamically, let's combine them with the built-in initrd. */
+ err = combine_initrd(
+ initrd_base, initrd_size,
+ (const void*const[]) {
+ credential_initrd,
+ global_credential_initrd,
+ sysext_initrd,
+ pcrsig_initrd,
+ pcrpkey_initrd,
+ },
+ (const size_t[]) {
+ credential_initrd_size,
+ global_credential_initrd_size,
+ sysext_initrd_size,
+ pcrsig_initrd_size,
+ pcrpkey_initrd_size,
+ },
+ 5,
+ &initrd_pages, &initrd_size);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ initrd_base = initrd_pages.addr;
+
+ /* Given these might be large let's free them explicitly, quickly. */
+ credential_initrd = mfree(credential_initrd);
+ global_credential_initrd = mfree(global_credential_initrd);
+ sysext_initrd = mfree(sysext_initrd);
+ pcrsig_initrd = mfree(pcrsig_initrd);
+ pcrpkey_initrd = mfree(pcrpkey_initrd);
+ }
+
+ if (dt_size > 0) {
+ err = devicetree_install_from_memory(
+ &dt_state, PHYSICAL_ADDRESS_TO_POINTER(dt_base), dt_size);
+ if (err != EFI_SUCCESS)
+ log_error_stall(L"Error loading embedded devicetree: %r", err);
+ }
+
+ err = linux_exec(image, cmdline,
+ PHYSICAL_ADDRESS_TO_POINTER(linux_base), linux_size,
+ PHYSICAL_ADDRESS_TO_POINTER(initrd_base), initrd_size);
+ graphics_mode(false);
+ return err;
+}
diff --git a/src/boot/efi/test-bcd.c b/src/boot/efi/test-bcd.c
new file mode 100644
index 0000000..0ee2947
--- /dev/null
+++ b/src/boot/efi/test-bcd.c
@@ -0,0 +1,162 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "bcd.h"
+#include "compress.h"
+#include "fileio.h"
+#include "tests.h"
+#include "utf8.h"
+
+/* Include the implementation directly, so we can poke at some internals. */
+#include "bcd.c"
+
+static void load_bcd(const char *path, void **ret_bcd, size_t *ret_bcd_len) {
+ size_t len;
+ _cleanup_free_ char *fn = NULL, *compressed = NULL;
+
+ assert_se(get_testdata_dir(path, &fn) >= 0);
+ assert_se(read_full_file_full(AT_FDCWD, fn, UINT64_MAX, SIZE_MAX, 0, NULL, &compressed, &len) >= 0);
+ assert_se(decompress_blob_zstd(compressed, len, ret_bcd, ret_bcd_len, SIZE_MAX) >= 0);
+}
+
+static void test_get_bcd_title_one(
+ const char *path,
+ const char16_t *title_expect,
+ size_t title_len_expect) {
+
+ size_t len;
+ _cleanup_free_ void *bcd = NULL;
+
+ log_info("/* %s(%s) */", __func__, path);
+
+ load_bcd(path, &bcd, &len);
+
+ char16_t *title = get_bcd_title(bcd, len);
+ if (title_expect) {
+ assert_se(title);
+ assert_se(memcmp(title, title_expect, title_len_expect) == 0);
+ } else
+ assert_se(!title);
+}
+
+TEST(get_bcd_title) {
+ test_get_bcd_title_one("test-bcd/win10.bcd.zst", u"Windows 10", sizeof(u"Windows 10"));
+
+ test_get_bcd_title_one("test-bcd/description-bad-type.bcd.zst", NULL, 0);
+ test_get_bcd_title_one("test-bcd/description-empty.bcd.zst", NULL, 0);
+ test_get_bcd_title_one("test-bcd/description-missing.bcd.zst", NULL, 0);
+ test_get_bcd_title_one("test-bcd/description-too-small.bcd.zst", NULL, 0);
+ test_get_bcd_title_one("test-bcd/displayorder-bad-name.bcd.zst", NULL, 0);
+ test_get_bcd_title_one("test-bcd/displayorder-bad-size.bcd.zst", NULL, 0);
+ test_get_bcd_title_one("test-bcd/displayorder-bad-type.bcd.zst", NULL, 0);
+ test_get_bcd_title_one("test-bcd/empty.bcd.zst", NULL, 0);
+}
+
+TEST(base_block) {
+ size_t len;
+ BaseBlock backup;
+ uint8_t *bcd_base;
+ _cleanup_free_ BaseBlock *bcd = NULL;
+
+ load_bcd("test-bcd/win10.bcd.zst", (void **) &bcd, &len);
+ backup = *bcd;
+ bcd_base = (uint8_t *) bcd;
+
+ assert_se(get_bcd_title(bcd_base, len));
+
+ /* Try various "corruptions" of the base block. */
+
+ assert_se(!get_bcd_title(bcd_base, sizeof(BaseBlock) - 1));
+
+ bcd->sig = 0;
+ assert_se(!get_bcd_title(bcd_base, len));
+ *bcd = backup;
+
+ bcd->version_minor = 2;
+ assert_se(!get_bcd_title(bcd_base, len));
+ *bcd = backup;
+
+ bcd->version_major = 4;
+ assert_se(!get_bcd_title(bcd_base, len));
+ *bcd = backup;
+
+ bcd->type = 1;
+ assert_se(!get_bcd_title(bcd_base, len));
+ *bcd = backup;
+
+ bcd->primary_seqnum++;
+ assert_se(!get_bcd_title(bcd_base, len));
+ *bcd = backup;
+}
+
+TEST(bad_bcd) {
+ size_t len;
+ uint8_t *hbins;
+ uint32_t offset;
+ _cleanup_free_ void *bcd = NULL;
+
+ /* This BCD hive has been manipulated to have bad offsets/sizes at various places. */
+ load_bcd("test-bcd/corrupt.bcd.zst", &bcd, &len);
+
+ assert_se(len >= HIVE_CELL_OFFSET);
+ hbins = (uint8_t *) bcd + HIVE_CELL_OFFSET;
+ len -= HIVE_CELL_OFFSET;
+ offset = ((BaseBlock *) bcd)->root_cell_offset;
+
+ const Key *root = get_key(hbins, len, offset, "\0");
+ assert_se(root);
+ assert_se(!get_key(hbins, sizeof(Key) - 1, offset, "\0"));
+
+ assert_se(!get_key(hbins, len, offset, "\0BadOffset\0"));
+ assert_se(!get_key(hbins, len, offset, "\0BadSig\0"));
+ assert_se(!get_key(hbins, len, offset, "\0BadKeyNameLen\0"));
+ assert_se(!get_key(hbins, len, offset, "\0SubkeyBadOffset\0Dummy\0"));
+ assert_se(!get_key(hbins, len, offset, "\0SubkeyBadSig\0Dummy\0"));
+ assert_se(!get_key(hbins, len, offset, "\0SubkeyBadNEntries\0Dummy\0"));
+
+ assert_se(!get_key_value(hbins, len, root, "Dummy"));
+
+ const Key *kv_bad_offset = get_key(hbins, len, offset, "\0KeyValuesBadOffset\0");
+ assert_se(kv_bad_offset);
+ assert_se(!get_key_value(hbins, len, kv_bad_offset, "Dummy"));
+
+ const Key *kv_bad_n_key_values = get_key(hbins, len, offset, "\0KeyValuesBadNKeyValues\0");
+ assert_se(kv_bad_n_key_values);
+ assert_se(!get_key_value(hbins, len, kv_bad_n_key_values, "Dummy"));
+
+ const Key *kv = get_key(hbins, len, offset, "\0KeyValues\0");
+ assert_se(kv);
+
+ assert_se(!get_key_value(hbins, len, kv, "BadOffset"));
+ assert_se(!get_key_value(hbins, len, kv, "BadSig"));
+ assert_se(!get_key_value(hbins, len, kv, "BadNameLen"));
+ assert_se(!get_key_value(hbins, len, kv, "InlineData"));
+ assert_se(!get_key_value(hbins, len, kv, "BadDataOffset"));
+ assert_se(!get_key_value(hbins, len, kv, "BadDataSize"));
+}
+
+TEST(argv_bcds) {
+ for (int i = 1; i < saved_argc; i++) {
+ size_t len;
+ _cleanup_free_ void *bcd = NULL;
+
+ assert_se(read_full_file_full(
+ AT_FDCWD,
+ saved_argv[i],
+ UINT64_MAX,
+ SIZE_MAX,
+ 0,
+ NULL,
+ (char **) &bcd,
+ &len) >= 0);
+
+ char16_t *title = get_bcd_title(bcd, len);
+ if (title) {
+ _cleanup_free_ char *title_utf8 = utf16_to_utf8(title, char16_strlen(title) * 2);
+ log_info("%s: \"%s\"", saved_argv[i], title_utf8);
+ } else
+ log_info("%s: Bad BCD", saved_argv[i]);
+ }
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/boot/efi/test-efi-string.c b/src/boot/efi/test-efi-string.c
new file mode 100644
index 0000000..7b43e1d
--- /dev/null
+++ b/src/boot/efi/test-efi-string.c
@@ -0,0 +1,523 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fnmatch.h>
+
+#include "efi-string.h"
+#include "tests.h"
+
+TEST(strlen8) {
+ assert_se(strlen8(NULL) == 0);
+ assert_se(strlen8("") == 0);
+ assert_se(strlen8("1") == 1);
+ assert_se(strlen8("11") == 2);
+ assert_se(strlen8("123456789") == 9);
+ assert_se(strlen8("12\0004") == 2);
+}
+
+TEST(strlen16) {
+ assert_se(strlen16(NULL) == 0);
+ assert_se(strlen16(u"") == 0);
+ assert_se(strlen16(u"1") == 1);
+ assert_se(strlen16(u"11") == 2);
+ assert_se(strlen16(u"123456789") == 9);
+ assert_se(strlen16(u"12\0004") == 2);
+}
+
+TEST(strnlen8) {
+ assert_se(strnlen8(NULL, 0) == 0);
+ assert_se(strnlen8(NULL, 10) == 0);
+ assert_se(strnlen8("", 10) == 0);
+ assert_se(strnlen8("1", 10) == 1);
+ assert_se(strnlen8("11", 1) == 1);
+ assert_se(strnlen8("123456789", 7) == 7);
+ assert_se(strnlen8("12\0004", 5) == 2);
+}
+
+TEST(strnlen16) {
+ assert_se(strnlen16(NULL, 0) == 0);
+ assert_se(strnlen16(NULL, 10) == 0);
+ assert_se(strnlen16(u"", 10) == 0);
+ assert_se(strnlen16(u"1", 10) == 1);
+ assert_se(strnlen16(u"11", 1) == 1);
+ assert_se(strnlen16(u"123456789", 7) == 7);
+ assert_se(strnlen16(u"12\0004", 5) == 2);
+}
+
+TEST(strsize8) {
+ assert_se(strsize8(NULL) == 0);
+ assert_se(strsize8("") == 1);
+ assert_se(strsize8("1") == 2);
+ assert_se(strsize8("11") == 3);
+ assert_se(strsize8("123456789") == 10);
+ assert_se(strsize8("12\0004") == 3);
+}
+
+TEST(strsize16) {
+ assert_se(strsize16(NULL) == 0);
+ assert_se(strsize16(u"") == 2);
+ assert_se(strsize16(u"1") == 4);
+ assert_se(strsize16(u"11") == 6);
+ assert_se(strsize16(u"123456789") == 20);
+ assert_se(strsize16(u"12\0004") == 6);
+}
+
+TEST(strtolower8) {
+ char s[] = "\0001234abcDEF!\0zZ";
+
+ strtolower8(NULL);
+
+ strtolower8(s);
+ assert_se(memcmp(s, "\0001234abcDEF!\0zZ", sizeof(s)) == 0);
+
+ s[0] = '#';
+ strtolower8(s);
+ assert_se(memcmp(s, "#1234abcdef!\0zZ", sizeof(s)) == 0);
+}
+
+TEST(strtolower16) {
+ char16_t s[] = u"\0001234abcDEF!\0zZ";
+
+ strtolower16(NULL);
+
+ strtolower16(s);
+ assert_se(memcmp(s, u"\0001234abcDEF!\0zZ", sizeof(s)) == 0);
+
+ s[0] = '#';
+ strtolower16(s);
+ assert_se(memcmp(s, u"#1234abcdef!\0zZ", sizeof(s)) == 0);
+}
+
+TEST(strncmp8) {
+ assert_se(strncmp8(NULL, "", 10) < 0);
+ assert_se(strncmp8("", NULL, 10) > 0);
+ assert_se(strncmp8(NULL, NULL, 0) == 0);
+ assert_se(strncmp8(NULL, NULL, 10) == 0);
+ assert_se(strncmp8("", "", 10) == 0);
+ assert_se(strncmp8("abc", "abc", 2) == 0);
+ assert_se(strncmp8("aBc", "aBc", 3) == 0);
+ assert_se(strncmp8("aBC", "aBC", 4) == 0);
+ assert_se(strncmp8("", "a", 0) == 0);
+ assert_se(strncmp8("b", "a", 0) == 0);
+ assert_se(strncmp8("", "a", 3) < 0);
+ assert_se(strncmp8("=", "=", 1) == 0);
+ assert_se(strncmp8("A", "a", 1) < 0);
+ assert_se(strncmp8("a", "A", 2) > 0);
+ assert_se(strncmp8("a", "Aa", 2) > 0);
+ assert_se(strncmp8("12\00034", "12345", 4) < 0);
+ assert_se(strncmp8("12\00034", "12345", SIZE_MAX) < 0);
+ assert_se(strncmp8("abc\0def", "abc", SIZE_MAX) == 0);
+ assert_se(strncmp8("abc\0def", "abcdef", SIZE_MAX) < 0);
+
+ assert_se(strncmp8((char[]){ CHAR_MIN }, (char[]){ CHAR_MIN }, 1) == 0);
+ assert_se(strncmp8((char[]){ CHAR_MAX }, (char[]){ CHAR_MAX }, 1) == 0);
+ assert_se(strncmp8((char[]){ CHAR_MIN }, (char[]){ CHAR_MAX }, 1) < 0);
+ assert_se(strncmp8((char[]){ CHAR_MAX }, (char[]){ CHAR_MIN }, 1) > 0);
+}
+
+TEST(strncmp16) {
+ assert_se(strncmp16(NULL, u"", 10) < 0);
+ assert_se(strncmp16(u"", NULL, 10) > 0);
+ assert_se(strncmp16(NULL, NULL, 0) == 0);
+ assert_se(strncmp16(NULL, NULL, 10) == 0);
+ assert_se(strncmp16(u"", u"", 0) == 0);
+ assert_se(strncmp16(u"", u"", 10) == 0);
+ assert_se(strncmp16(u"abc", u"abc", 2) == 0);
+ assert_se(strncmp16(u"aBc", u"aBc", 3) == 0);
+ assert_se(strncmp16(u"aBC", u"aBC", 4) == 0);
+ assert_se(strncmp16(u"", u"a", 0) == 0);
+ assert_se(strncmp16(u"b", u"a", 0) == 0);
+ assert_se(strncmp16(u"", u"a", 3) < 0);
+ assert_se(strncmp16(u"=", u"=", 1) == 0);
+ assert_se(strncmp16(u"A", u"a", 1) < 0);
+ assert_se(strncmp16(u"a", u"A", 2) > 0);
+ assert_se(strncmp16(u"a", u"Aa", 2) > 0);
+ assert_se(strncmp16(u"12\00034", u"12345", 4) < 0);
+ assert_se(strncmp16(u"12\00034", u"12345", SIZE_MAX) < 0);
+ assert_se(strncmp16(u"abc\0def", u"abc", SIZE_MAX) == 0);
+ assert_se(strncmp16(u"abc\0def", u"abcdef", SIZE_MAX) < 0);
+
+ assert_se(strncmp16((char16_t[]){ UINT16_MAX }, (char16_t[]){ UINT16_MAX }, 1) == 0);
+ assert_se(strncmp16((char16_t[]){ 0 }, (char16_t[]){ UINT16_MAX }, 1) < 0);
+ assert_se(strncmp16((char16_t[]){ UINT16_MAX }, (char16_t[]){ 0 }, 1) > 0);
+}
+
+TEST(strncasecmp8) {
+ assert_se(strncasecmp8(NULL, "", 10) < 0);
+ assert_se(strncasecmp8("", NULL, 10) > 0);
+ assert_se(strncasecmp8(NULL, NULL, 0) == 0);
+ assert_se(strncasecmp8(NULL, NULL, 10) == 0);
+ assert_se(strncasecmp8("", "", 10) == 0);
+ assert_se(strncasecmp8("abc", "abc", 2) == 0);
+ assert_se(strncasecmp8("aBc", "AbC", 3) == 0);
+ assert_se(strncasecmp8("aBC", "Abc", 4) == 0);
+ assert_se(strncasecmp8("", "a", 0) == 0);
+ assert_se(strncasecmp8("b", "a", 0) == 0);
+ assert_se(strncasecmp8("", "a", 3) < 0);
+ assert_se(strncasecmp8("=", "=", 1) == 0);
+ assert_se(strncasecmp8("A", "a", 1) == 0);
+ assert_se(strncasecmp8("a", "A", 2) == 0);
+ assert_se(strncasecmp8("a", "Aa", 2) < 0);
+ assert_se(strncasecmp8("12\00034", "12345", 4) < 0);
+ assert_se(strncasecmp8("12\00034", "12345", SIZE_MAX) < 0);
+ assert_se(strncasecmp8("abc\0def", "ABC", SIZE_MAX) == 0);
+ assert_se(strncasecmp8("abc\0def", "ABCDEF", SIZE_MAX) < 0);
+
+ assert_se(strncasecmp8((char[]){ CHAR_MIN }, (char[]){ CHAR_MIN }, 1) == 0);
+ assert_se(strncasecmp8((char[]){ CHAR_MAX }, (char[]){ CHAR_MAX }, 1) == 0);
+ assert_se(strncasecmp8((char[]){ CHAR_MIN }, (char[]){ CHAR_MAX }, 1) < 0);
+ assert_se(strncasecmp8((char[]){ CHAR_MAX }, (char[]){ CHAR_MIN }, 1) > 0);
+}
+
+TEST(strncasecmp16) {
+ assert_se(strncasecmp16(NULL, u"", 10) < 0);
+ assert_se(strncasecmp16(u"", NULL, 10) > 0);
+ assert_se(strncasecmp16(NULL, NULL, 0) == 0);
+ assert_se(strncasecmp16(NULL, NULL, 10) == 0);
+ assert_se(strncasecmp16(u"", u"", 10) == 0);
+ assert_se(strncasecmp16(u"abc", u"abc", 2) == 0);
+ assert_se(strncasecmp16(u"aBc", u"AbC", 3) == 0);
+ assert_se(strncasecmp16(u"aBC", u"Abc", 4) == 0);
+ assert_se(strncasecmp16(u"", u"a", 0) == 0);
+ assert_se(strncasecmp16(u"b", u"a", 0) == 0);
+ assert_se(strncasecmp16(u"", u"a", 3) < 0);
+ assert_se(strncasecmp16(u"=", u"=", 1) == 0);
+ assert_se(strncasecmp16(u"A", u"a", 1) == 0);
+ assert_se(strncasecmp16(u"a", u"A", 2) == 0);
+ assert_se(strncasecmp16(u"a", u"Aa", 2) < 0);
+ assert_se(strncasecmp16(u"12\00034", u"12345", 4) < 0);
+ assert_se(strncasecmp16(u"12\00034", u"12345", SIZE_MAX) < 0);
+ assert_se(strncasecmp16(u"abc\0def", u"ABC", SIZE_MAX) == 0);
+ assert_se(strncasecmp16(u"abc\0def", u"ABCDEF", SIZE_MAX) < 0);
+
+ assert_se(strncasecmp16((char16_t[]){ UINT16_MAX }, (char16_t[]){ UINT16_MAX }, 1) == 0);
+ assert_se(strncasecmp16((char16_t[]){ 0 }, (char16_t[]){ UINT16_MAX }, 1) < 0);
+ assert_se(strncasecmp16((char16_t[]){ UINT16_MAX }, (char16_t[]){ 0 }, 1) > 0);
+}
+
+TEST(strcpy8) {
+ char buf[128];
+
+ assert_se(strcpy8(buf, "123") == buf);
+ assert_se(streq8(buf, "123"));
+ assert_se(strcpy8(buf, "") == buf);
+ assert_se(streq8(buf, ""));
+ assert_se(strcpy8(buf, "A") == buf);
+ assert_se(streq8(buf, "A"));
+ assert_se(strcpy8(buf, NULL) == buf);
+ assert_se(streq8(buf, ""));
+}
+
+TEST(strcpy16) {
+ char16_t buf[128];
+
+ assert_se(strcpy16(buf, u"123") == buf);
+ assert_se(streq16(buf, u"123"));
+ assert_se(strcpy16(buf, u"") == buf);
+ assert_se(streq16(buf, u""));
+ assert_se(strcpy16(buf, u"A") == buf);
+ assert_se(streq16(buf, u"A"));
+ assert_se(strcpy16(buf, NULL) == buf);
+ assert_se(streq16(buf, u""));
+}
+
+TEST(strchr8) {
+ assert_se(!strchr8(NULL, 'a'));
+ assert_se(!strchr8("", 'a'));
+ assert_se(!strchr8("123", 'a'));
+
+ const char str[] = "abcaBc";
+ assert_se(strchr8(str, 'a') == &str[0]);
+ assert_se(strchr8(str, 'c') == &str[2]);
+ assert_se(strchr8(str, 'B') == &str[4]);
+}
+
+TEST(strchr16) {
+ assert_se(!strchr16(NULL, 'a'));
+ assert_se(!strchr16(u"", 'a'));
+ assert_se(!strchr16(u"123", 'a'));
+
+ const char16_t str[] = u"abcaBc";
+ assert_se(strchr16(str, 'a') == &str[0]);
+ assert_se(strchr16(str, 'c') == &str[2]);
+ assert_se(strchr16(str, 'B') == &str[4]);
+}
+
+TEST(xstrndup8) {
+ char *s = NULL;
+
+ assert_se(xstrndup8(NULL, 0) == NULL);
+ assert_se(xstrndup8(NULL, 10) == NULL);
+
+ assert_se(s = xstrndup8("", 10));
+ assert_se(streq8(s, ""));
+ free(s);
+
+ assert_se(s = xstrndup8("abc", 0));
+ assert_se(streq8(s, ""));
+ free(s);
+
+ assert_se(s = xstrndup8("ABC", 3));
+ assert_se(streq8(s, "ABC"));
+ free(s);
+
+ assert_se(s = xstrndup8("123abcDEF", 5));
+ assert_se(streq8(s, "123ab"));
+ free(s);
+}
+
+TEST(xstrdup8) {
+ char *s = NULL;
+
+ assert_se(xstrdup8(NULL) == NULL);
+
+ assert_se(s = xstrdup8(""));
+ assert_se(streq8(s, ""));
+ free(s);
+
+ assert_se(s = xstrdup8("1"));
+ assert_se(streq8(s, "1"));
+ free(s);
+
+ assert_se(s = xstrdup8("123abcDEF"));
+ assert_se(streq8(s, "123abcDEF"));
+ free(s);
+}
+
+TEST(xstrndup16) {
+ char16_t *s = NULL;
+
+ assert_se(xstrndup16(NULL, 0) == NULL);
+ assert_se(xstrndup16(NULL, 10) == NULL);
+
+ assert_se(s = xstrndup16(u"", 10));
+ assert_se(streq16(s, u""));
+ free(s);
+
+ assert_se(s = xstrndup16(u"abc", 0));
+ assert_se(streq16(s, u""));
+ free(s);
+
+ assert_se(s = xstrndup16(u"ABC", 3));
+ assert_se(streq16(s, u"ABC"));
+ free(s);
+
+ assert_se(s = xstrndup16(u"123abcDEF", 5));
+ assert_se(streq16(s, u"123ab"));
+ free(s);
+}
+
+TEST(xstrdup16) {
+ char16_t *s = NULL;
+
+ assert_se(xstrdup16(NULL) == NULL);
+
+ assert_se(s = xstrdup16(u""));
+ assert_se(streq16(s, u""));
+ free(s);
+
+ assert_se(s = xstrdup16(u"1"));
+ assert_se(streq16(s, u"1"));
+ free(s);
+
+ assert_se(s = xstrdup16(u"123abcDEF"));
+ assert_se(streq16(s, u"123abcDEF"));
+ free(s);
+}
+
+TEST(xstrn8_to_16) {
+ char16_t *s = NULL;
+
+ assert_se(xstrn8_to_16(NULL, 1) == NULL);
+ assert_se(xstrn8_to_16("a", 0) == NULL);
+
+ assert_se(s = xstrn8_to_16("", 1));
+ assert_se(streq16(s, u""));
+ free(s);
+
+ assert_se(s = xstrn8_to_16("1", 1));
+ assert_se(streq16(s, u"1"));
+ free(s);
+
+ assert_se(s = xstr8_to_16("abcxyzABCXYZ09 .,-_#*!\"§$%&/()=?`~"));
+ assert_se(streq16(s, u"abcxyzABCXYZ09 .,-_#*!\"§$%&/()=?`~"));
+ free(s);
+
+ assert_se(s = xstr8_to_16("ÿⱿ𝇉 😺"));
+ assert_se(streq16(s, u"ÿⱿ "));
+ free(s);
+
+ assert_se(s = xstrn8_to_16("¶¶", 3));
+ assert_se(streq16(s, u"¶"));
+ free(s);
+}
+
+#define TEST_FNMATCH_ONE(pattern, haystack, expect) \
+ ({ \
+ assert_se(fnmatch(pattern, haystack, 0) == (expect ? 0 : FNM_NOMATCH)); \
+ assert_se(efi_fnmatch(u##pattern, u##haystack) == expect); \
+ })
+
+TEST(efi_fnmatch) {
+ TEST_FNMATCH_ONE("", "", true);
+ TEST_FNMATCH_ONE("abc", "abc", true);
+ TEST_FNMATCH_ONE("aBc", "abc", false);
+ TEST_FNMATCH_ONE("b", "a", false);
+ TEST_FNMATCH_ONE("b", "", false);
+ TEST_FNMATCH_ONE("abc", "a", false);
+ TEST_FNMATCH_ONE("a?c", "azc", true);
+ TEST_FNMATCH_ONE("???", "?.9", true);
+ TEST_FNMATCH_ONE("1?", "1", false);
+ TEST_FNMATCH_ONE("***", "", true);
+ TEST_FNMATCH_ONE("*", "123", true);
+ TEST_FNMATCH_ONE("**", "abcd", true);
+ TEST_FNMATCH_ONE("*b*", "abcd", true);
+ TEST_FNMATCH_ONE("abc*d", "abc", false);
+ TEST_FNMATCH_ONE("start*end", "startend", true);
+ TEST_FNMATCH_ONE("start*end", "startendend", true);
+ TEST_FNMATCH_ONE("start*end", "startenddne", false);
+ TEST_FNMATCH_ONE("start*end", "startendstartend", true);
+ TEST_FNMATCH_ONE("start*end", "starten", false);
+ TEST_FNMATCH_ONE("*.conf", "arch.conf", true);
+ TEST_FNMATCH_ONE("debian-*.conf", "debian-wheezy.conf", true);
+ TEST_FNMATCH_ONE("debian-*.*", "debian-wheezy.efi", true);
+ TEST_FNMATCH_ONE("ab*cde", "abzcd", false);
+ TEST_FNMATCH_ONE("\\*\\a\\[", "*a[", true);
+ TEST_FNMATCH_ONE("[abc] [abc] [abc]", "a b c", true);
+ TEST_FNMATCH_ONE("abc]", "abc]", true);
+ TEST_FNMATCH_ONE("[abc]", "z", false);
+ TEST_FNMATCH_ONE("[abc", "a", false);
+ TEST_FNMATCH_ONE("[][!] [][!] [][!]", "[ ] !", true);
+ TEST_FNMATCH_ONE("[]-] []-]", "] -", true);
+ TEST_FNMATCH_ONE("[1\\]] [1\\]]", "1 ]", true);
+ TEST_FNMATCH_ONE("[$-\\+]", "&", true);
+ TEST_FNMATCH_ONE("[1-3A-C] [1-3A-C]", "2 B", true);
+ TEST_FNMATCH_ONE("[3-5] [3-5] [3-5]", "3 4 5", true);
+ TEST_FNMATCH_ONE("[f-h] [f-h] [f-h]", "f g h", true);
+ TEST_FNMATCH_ONE("[a-c-f] [a-c-f] [a-c-f] [a-c-f] [a-c-f]", "a b c - f", true);
+ TEST_FNMATCH_ONE("[a-c-f]", "e", false);
+ TEST_FNMATCH_ONE("[--0] [--0] [--0]", "- . 0", true);
+ TEST_FNMATCH_ONE("[+--] [+--] [+--]", "+ , -", true);
+ TEST_FNMATCH_ONE("[f-l]", "m", false);
+ TEST_FNMATCH_ONE("[b]", "z-a", false);
+ TEST_FNMATCH_ONE("[a\\-z]", "b", false);
+ TEST_FNMATCH_ONE("?a*b[.-0]c", "/a/b/c", true);
+ TEST_FNMATCH_ONE("debian-*-*-*.*", "debian-jessie-2018-06-17-kernel-image-5.10.0-16-amd64.efi", true);
+
+ /* These would take forever with a backtracking implementation. */
+ TEST_FNMATCH_ONE(
+ "a*b*c*d*e*f*g*h*i*j*k*l*m*n*o*p*q*r*s*t*u*v*w*x*y*z*",
+ "aaaabbbbccccddddeeeeffffgggghhhhiiiijjjjkkkkllllmmmmnnnnooooppppqqqqrrrrssssttttuuuuvvvvwwwwxxxxyyyy",
+ false);
+ TEST_FNMATCH_ONE(
+ "a*b*c*d*e*f*g*h*i*j*k*l*m*n*o*p*q*r*s*t*u*v*w*x*y*z*",
+ "aaaabbbbccccddddeeeeffffgggghhhhiiiijjjjkkkkllllmmmmnnnnooooppppqqqqrrrrssssttttuuuuvvvvwwwwxxxxyyyyzzzz!!!!",
+ true);
+}
+
+TEST(parse_number8) {
+ uint64_t u;
+ const char *tail;
+
+ assert_se(!parse_number8(NULL, &u, NULL));
+ assert_se(!parse_number8("", &u, NULL));
+ assert_se(!parse_number8("a1", &u, NULL));
+ assert_se(!parse_number8("1a", &u, NULL));
+ assert_se(!parse_number8("-42", &u, NULL));
+ assert_se(!parse_number8("18446744073709551616", &u, NULL));
+
+ assert_se(parse_number8("0", &u, NULL));
+ assert_se(u == 0);
+ assert_se(parse_number8("1", &u, NULL));
+ assert_se(u == 1);
+ assert_se(parse_number8("999", &u, NULL));
+ assert_se(u == 999);
+ assert_se(parse_number8("18446744073709551615", &u, NULL));
+ assert_se(u == UINT64_MAX);
+ assert_se(parse_number8("42", &u, &tail));
+ assert_se(u == 42);
+ assert_se(streq8(tail, ""));
+ assert_se(parse_number8("54321rest", &u, &tail));
+ assert_se(u == 54321);
+ assert_se(streq8(tail, "rest"));
+}
+
+TEST(parse_number16) {
+ uint64_t u;
+ const char16_t *tail;
+
+ assert_se(!parse_number16(NULL, &u, NULL));
+ assert_se(!parse_number16(u"", &u, NULL));
+ assert_se(!parse_number16(u"a1", &u, NULL));
+ assert_se(!parse_number16(u"1a", &u, NULL));
+ assert_se(!parse_number16(u"-42", &u, NULL));
+ assert_se(!parse_number16(u"18446744073709551616", &u, NULL));
+
+ assert_se(parse_number16(u"0", &u, NULL));
+ assert_se(u == 0);
+ assert_se(parse_number16(u"1", &u, NULL));
+ assert_se(u == 1);
+ assert_se(parse_number16(u"999", &u, NULL));
+ assert_se(u == 999);
+ assert_se(parse_number16(u"18446744073709551615", &u, NULL));
+ assert_se(u == UINT64_MAX);
+ assert_se(parse_number16(u"42", &u, &tail));
+ assert_se(u == 42);
+ assert_se(streq16(tail, u""));
+ assert_se(parse_number16(u"54321rest", &u, &tail));
+ assert_se(u == 54321);
+ assert_se(streq16(tail, u"rest"));
+}
+
+TEST(efi_memcmp) {
+ assert_se(efi_memcmp(NULL, NULL, 0) == 0);
+ assert_se(efi_memcmp(NULL, NULL, 1) == 0);
+ assert_se(efi_memcmp(NULL, "", 1) < 0);
+ assert_se(efi_memcmp("", NULL, 1) > 0);
+ assert_se(efi_memcmp("", "", 0) == 0);
+ assert_se(efi_memcmp("", "", 1) == 0);
+ assert_se(efi_memcmp("1", "1", 1) == 0);
+ assert_se(efi_memcmp("1", "2", 1) < 0);
+ assert_se(efi_memcmp("A", "a", 1) < 0);
+ assert_se(efi_memcmp("a", "A", 1) > 0);
+ assert_se(efi_memcmp("abc", "ab", 2) == 0);
+ assert_se(efi_memcmp("ab", "abc", 3) < 0);
+ assert_se(efi_memcmp("abc", "ab", 3) > 0);
+ assert_se(efi_memcmp("ab\000bd", "ab\000bd", 6) == 0);
+ assert_se(efi_memcmp("ab\000b\0", "ab\000bd", 6) < 0);
+}
+
+TEST(efi_memcpy) {
+ char buf[10];
+
+ assert_se(!efi_memcpy(NULL, NULL, 0));
+ assert_se(!efi_memcpy(NULL, "", 1));
+ assert_se(efi_memcpy(buf, NULL, 0) == buf);
+ assert_se(efi_memcpy(buf, NULL, 1) == buf);
+ assert_se(efi_memcpy(buf, "a", 0) == buf);
+
+ assert_se(efi_memcpy(buf, "", 1) == buf);
+ assert_se(memcmp(buf, "", 1) == 0);
+ assert_se(efi_memcpy(buf, "1", 1) == buf);
+ assert_se(memcmp(buf, "1", 1) == 0);
+ assert_se(efi_memcpy(buf, "23", 3) == buf);
+ assert_se(memcmp(buf, "23", 3) == 0);
+ assert_se(efi_memcpy(buf, "45\0ab\0\0\0c", 9) == buf);
+ assert_se(memcmp(buf, "45\0ab\0\0\0c", 9) == 0);
+}
+
+TEST(efi_memset) {
+ char buf[10];
+
+ assert_se(!efi_memset(NULL, '1', 0));
+ assert_se(!efi_memset(NULL, '1', 1));
+ assert_se(efi_memset(buf, '1', 0) == buf);
+
+ assert_se(efi_memset(buf, '2', 1) == buf);
+ assert_se(memcmp(buf, "2", 1) == 0);
+ assert_se(efi_memset(buf, '4', 4) == buf);
+ assert_se(memcmp(buf, "4444", 4) == 0);
+ assert_se(efi_memset(buf, 'a', 10) == buf);
+ assert_se(memcmp(buf, "aaaaaaaaaa", 10) == 0);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/boot/efi/ticks.c b/src/boot/efi/ticks.c
new file mode 100644
index 0000000..889980a
--- /dev/null
+++ b/src/boot/efi/ticks.c
@@ -0,0 +1,84 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <efi.h>
+#include <efilib.h>
+
+#include "ticks.h"
+#include "util.h"
+
+#ifdef __x86_64__
+static uint64_t ticks_read(void) {
+ uint64_t a, d;
+
+ /* The TSC might or might not be virtualized in VMs (and thus might not be accurate or start at zero
+ * at boot), depending on hypervisor and CPU functionality. If it's not virtualized it's not useful
+ * for keeping time, hence don't attempt to use it. */
+ if (in_hypervisor())
+ return 0;
+
+ __asm__ volatile ("rdtsc" : "=a" (a), "=d" (d));
+ return (d << 32) | a;
+}
+#elif defined(__i386__)
+static uint64_t ticks_read(void) {
+ uint64_t val;
+
+ if (in_hypervisor())
+ return 0;
+
+ __asm__ volatile ("rdtsc" : "=A" (val));
+ return val;
+}
+#elif defined(__aarch64__)
+static uint64_t ticks_read(void) {
+ uint64_t val;
+ asm volatile("mrs %0, cntvct_el0" : "=r"(val));
+ return val;
+}
+#else
+static uint64_t ticks_read(void) {
+ return 0;
+}
+#endif
+
+#if defined(__aarch64__)
+static uint64_t ticks_freq(void) {
+ uint64_t freq;
+ asm volatile("mrs %0, cntfrq_el0" : "=r"(freq));
+ return freq;
+}
+#else
+/* count TSC ticks during a millisecond delay */
+static uint64_t ticks_freq(void) {
+ uint64_t ticks_start, ticks_end;
+ static uint64_t cache = 0;
+
+ if (cache != 0)
+ return cache;
+
+ ticks_start = ticks_read();
+ BS->Stall(1000);
+ ticks_end = ticks_read();
+
+ if (ticks_end < ticks_start) /* Check for an overflow (which is not that unlikely, given on some
+ * archs the value is 32bit) */
+ return 0;
+
+ cache = (ticks_end - ticks_start) * 1000UL;
+ return cache;
+}
+#endif
+
+uint64_t time_usec(void) {
+ uint64_t ticks, freq;
+
+ ticks = ticks_read();
+ if (ticks == 0)
+ return 0;
+
+ freq = ticks_freq();
+ if (freq == 0)
+ return 0;
+
+ return 1000UL * 1000UL * ticks / freq;
+}
diff --git a/src/boot/efi/ticks.h b/src/boot/efi/ticks.h
new file mode 100644
index 0000000..fec3764
--- /dev/null
+++ b/src/boot/efi/ticks.h
@@ -0,0 +1,6 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <stdint.h>
+
+uint64_t time_usec(void);
diff --git a/src/boot/efi/util.c b/src/boot/efi/util.c
new file mode 100644
index 0000000..66056f0
--- /dev/null
+++ b/src/boot/efi/util.c
@@ -0,0 +1,794 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <efi.h>
+#include <efilib.h>
+#if defined(__i386__) || defined(__x86_64__)
+# include <cpuid.h>
+#endif
+
+#include "ticks.h"
+#include "util.h"
+
+EFI_STATUS parse_boolean(const char *v, bool *b) {
+ assert(b);
+
+ if (!v)
+ return EFI_INVALID_PARAMETER;
+
+ if (streq8(v, "1") || streq8(v, "yes") || streq8(v, "y") || streq8(v, "true") || streq8(v, "t") ||
+ streq8(v, "on")) {
+ *b = true;
+ return EFI_SUCCESS;
+ }
+
+ if (streq8(v, "0") || streq8(v, "no") || streq8(v, "n") || streq8(v, "false") || streq8(v, "f") ||
+ streq8(v, "off")) {
+ *b = false;
+ return EFI_SUCCESS;
+ }
+
+ return EFI_INVALID_PARAMETER;
+}
+
+EFI_STATUS efivar_set_raw(const EFI_GUID *vendor, const char16_t *name, const void *buf, UINTN size, uint32_t flags) {
+ assert(vendor);
+ assert(name);
+ assert(buf || size == 0);
+
+ flags |= EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS;
+ return RT->SetVariable((char16_t *) name, (EFI_GUID *) vendor, flags, size, (void *) buf);
+}
+
+EFI_STATUS efivar_set(const EFI_GUID *vendor, const char16_t *name, const char16_t *value, uint32_t flags) {
+ assert(vendor);
+ assert(name);
+
+ return efivar_set_raw(vendor, name, value, value ? strsize16(value) : 0, flags);
+}
+
+EFI_STATUS efivar_set_uint_string(const EFI_GUID *vendor, const char16_t *name, UINTN i, uint32_t flags) {
+ char16_t str[32];
+
+ assert(vendor);
+ assert(name);
+
+ /* Note that SPrint has no native sized length specifier and will always use ValueToString()
+ * regardless of what sign we tell it to use. Therefore, UINTN_MAX will come out as -1 on
+ * 64bit machines. */
+ ValueToString(str, false, i);
+ return efivar_set(vendor, name, str, flags);
+}
+
+EFI_STATUS efivar_set_uint32_le(const EFI_GUID *vendor, const char16_t *name, uint32_t value, uint32_t flags) {
+ uint8_t buf[4];
+
+ assert(vendor);
+ assert(name);
+
+ buf[0] = (uint8_t)(value >> 0U & 0xFF);
+ buf[1] = (uint8_t)(value >> 8U & 0xFF);
+ buf[2] = (uint8_t)(value >> 16U & 0xFF);
+ buf[3] = (uint8_t)(value >> 24U & 0xFF);
+
+ return efivar_set_raw(vendor, name, buf, sizeof(buf), flags);
+}
+
+EFI_STATUS efivar_set_uint64_le(const EFI_GUID *vendor, const char16_t *name, uint64_t value, uint32_t flags) {
+ uint8_t buf[8];
+
+ assert(vendor);
+ assert(name);
+
+ buf[0] = (uint8_t)(value >> 0U & 0xFF);
+ buf[1] = (uint8_t)(value >> 8U & 0xFF);
+ buf[2] = (uint8_t)(value >> 16U & 0xFF);
+ buf[3] = (uint8_t)(value >> 24U & 0xFF);
+ buf[4] = (uint8_t)(value >> 32U & 0xFF);
+ buf[5] = (uint8_t)(value >> 40U & 0xFF);
+ buf[6] = (uint8_t)(value >> 48U & 0xFF);
+ buf[7] = (uint8_t)(value >> 56U & 0xFF);
+
+ return efivar_set_raw(vendor, name, buf, sizeof(buf), flags);
+}
+
+EFI_STATUS efivar_get(const EFI_GUID *vendor, const char16_t *name, char16_t **value) {
+ _cleanup_free_ char16_t *buf = NULL;
+ EFI_STATUS err;
+ char16_t *val;
+ UINTN size;
+
+ assert(vendor);
+ assert(name);
+
+ err = efivar_get_raw(vendor, name, (char **) &buf, &size);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ /* Make sure there are no incomplete characters in the buffer */
+ if ((size % sizeof(char16_t)) != 0)
+ return EFI_INVALID_PARAMETER;
+
+ if (!value)
+ return EFI_SUCCESS;
+
+ /* Return buffer directly if it happens to be NUL terminated already */
+ if (size >= sizeof(char16_t) && buf[size / sizeof(char16_t) - 1] == 0) {
+ *value = TAKE_PTR(buf);
+ return EFI_SUCCESS;
+ }
+
+ /* Make sure a terminating NUL is available at the end */
+ val = xmalloc(size + sizeof(char16_t));
+
+ memcpy(val, buf, size);
+ val[size / sizeof(char16_t) - 1] = 0; /* NUL terminate */
+
+ *value = val;
+ return EFI_SUCCESS;
+}
+
+EFI_STATUS efivar_get_uint_string(const EFI_GUID *vendor, const char16_t *name, UINTN *i) {
+ _cleanup_free_ char16_t *val = NULL;
+ EFI_STATUS err;
+ uint64_t u;
+
+ assert(vendor);
+ assert(name);
+ assert(i);
+
+ err = efivar_get(vendor, name, &val);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ if (!parse_number16(val, &u, NULL) || u > UINTN_MAX)
+ return EFI_INVALID_PARAMETER;
+
+ *i = u;
+ return EFI_SUCCESS;
+}
+
+EFI_STATUS efivar_get_uint32_le(const EFI_GUID *vendor, const char16_t *name, uint32_t *ret) {
+ _cleanup_free_ char *buf = NULL;
+ UINTN size;
+ EFI_STATUS err;
+
+ assert(vendor);
+ assert(name);
+
+ err = efivar_get_raw(vendor, name, &buf, &size);
+ if (err == EFI_SUCCESS && ret) {
+ if (size != sizeof(uint32_t))
+ return EFI_BUFFER_TOO_SMALL;
+
+ *ret = (uint32_t) buf[0] << 0U | (uint32_t) buf[1] << 8U | (uint32_t) buf[2] << 16U |
+ (uint32_t) buf[3] << 24U;
+ }
+
+ return err;
+}
+
+EFI_STATUS efivar_get_uint64_le(const EFI_GUID *vendor, const char16_t *name, uint64_t *ret) {
+ _cleanup_free_ char *buf = NULL;
+ UINTN size;
+ EFI_STATUS err;
+
+ assert(vendor);
+ assert(name);
+
+ err = efivar_get_raw(vendor, name, &buf, &size);
+ if (err == EFI_SUCCESS && ret) {
+ if (size != sizeof(uint64_t))
+ return EFI_BUFFER_TOO_SMALL;
+
+ *ret = (uint64_t) buf[0] << 0U | (uint64_t) buf[1] << 8U | (uint64_t) buf[2] << 16U |
+ (uint64_t) buf[3] << 24U | (uint64_t) buf[4] << 32U | (uint64_t) buf[5] << 40U |
+ (uint64_t) buf[6] << 48U | (uint64_t) buf[7] << 56U;
+ }
+
+ return err;
+}
+
+EFI_STATUS efivar_get_raw(const EFI_GUID *vendor, const char16_t *name, char **buffer, UINTN *size) {
+ _cleanup_free_ char *buf = NULL;
+ UINTN l;
+ EFI_STATUS err;
+
+ assert(vendor);
+ assert(name);
+
+ l = sizeof(char16_t *) * EFI_MAXIMUM_VARIABLE_SIZE;
+ buf = xmalloc(l);
+
+ err = RT->GetVariable((char16_t *) name, (EFI_GUID *) vendor, NULL, &l, buf);
+ if (err == EFI_SUCCESS) {
+
+ if (buffer)
+ *buffer = TAKE_PTR(buf);
+
+ if (size)
+ *size = l;
+ }
+
+ return err;
+}
+
+EFI_STATUS efivar_get_boolean_u8(const EFI_GUID *vendor, const char16_t *name, bool *ret) {
+ _cleanup_free_ char *b = NULL;
+ UINTN size;
+ EFI_STATUS err;
+
+ assert(vendor);
+ assert(name);
+ assert(ret);
+
+ err = efivar_get_raw(vendor, name, &b, &size);
+ if (err == EFI_SUCCESS)
+ *ret = *b > 0;
+
+ return err;
+}
+
+void efivar_set_time_usec(const EFI_GUID *vendor, const char16_t *name, uint64_t usec) {
+ char16_t str[32];
+
+ assert(vendor);
+ assert(name);
+
+ if (usec == 0)
+ usec = time_usec();
+ if (usec == 0)
+ return;
+
+ /* See comment on ValueToString in efivar_set_uint_string(). */
+ ValueToString(str, false, usec);
+ efivar_set(vendor, name, str, 0);
+}
+
+void convert_efi_path(char16_t *path) {
+ assert(path);
+
+ for (size_t i = 0, fixed = 0;; i++) {
+ /* Fix device path node separator. */
+ path[fixed] = (path[i] == '/') ? '\\' : path[i];
+
+ /* Double '\' is not allowed in EFI file paths. */
+ if (fixed > 0 && path[fixed - 1] == '\\' && path[fixed] == '\\')
+ continue;
+
+ if (path[i] == '\0')
+ break;
+
+ fixed++;
+ }
+}
+
+char16_t *xstr8_to_path(const char *str8) {
+ assert(str8);
+ char16_t *path = xstr8_to_16(str8);
+ convert_efi_path(path);
+ return path;
+}
+
+void mangle_stub_cmdline(char16_t *cmdline) {
+ char16_t *p = cmdline;
+
+ for (; *cmdline != '\0'; cmdline++)
+ /* Convert ASCII control characters to spaces. */
+ if (*cmdline <= 0x1F)
+ *cmdline = ' ';
+
+ /* chomp the trailing whitespaces */
+ while (cmdline != p) {
+ --cmdline;
+
+ if (*cmdline != ' ')
+ break;
+
+ *cmdline = '\0';
+ }
+}
+
+EFI_STATUS chunked_read(EFI_FILE *file, size_t *size, void *buf) {
+ EFI_STATUS err;
+
+ assert(file);
+ assert(size);
+ assert(buf);
+
+ /* This is a drop-in replacement for EFI_FILE->Read() with the same API behavior.
+ * Some broken firmwares cannot handle large file reads and will instead return
+ * an error. As a workaround, read such files in small chunks.
+ * Note that we cannot just try reading the whole file first on such firmware as
+ * that will permanently break the handle even if it is re-opened.
+ *
+ * https://github.com/systemd/systemd/issues/25911 */
+
+ if (*size == 0)
+ return EFI_SUCCESS;
+
+ size_t read = 0, remaining = *size;
+ while (remaining > 0) {
+ size_t chunk = MIN(1024U * 1024U, remaining);
+
+ err = file->Read(file, &chunk, (uint8_t *) buf + read);
+ if (err != EFI_SUCCESS)
+ return err;
+ if (chunk == 0)
+ /* Caller requested more bytes than are in file. */
+ break;
+
+ assert(chunk <= remaining);
+ read += chunk;
+ remaining -= chunk;
+ }
+
+ *size = read;
+ return EFI_SUCCESS;
+}
+
+EFI_STATUS file_read(EFI_FILE *dir, const char16_t *name, UINTN off, UINTN size, char **ret, UINTN *ret_size) {
+ _cleanup_(file_closep) EFI_FILE *handle = NULL;
+ _cleanup_free_ char *buf = NULL;
+ EFI_STATUS err;
+
+ assert(dir);
+ assert(name);
+ assert(ret);
+
+ err = dir->Open(dir, &handle, (char16_t*) name, EFI_FILE_MODE_READ, 0ULL);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ if (size == 0) {
+ _cleanup_free_ EFI_FILE_INFO *info = NULL;
+
+ err = get_file_info_harder(handle, &info, NULL);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ size = info->FileSize;
+ }
+
+ if (off > 0) {
+ err = handle->SetPosition(handle, off);
+ if (err != EFI_SUCCESS)
+ return err;
+ }
+
+ /* Allocate some extra bytes to guarantee the result is NUL-terminated for char and char16_t strings. */
+ UINTN extra = size % sizeof(char16_t) + sizeof(char16_t);
+
+ buf = xmalloc(size + extra);
+ err = chunked_read(handle, &size, buf);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ /* Note that chunked_read() changes size to reflect the actual bytes read. */
+ memset(buf + size, 0, extra);
+
+ *ret = TAKE_PTR(buf);
+ if (ret_size)
+ *ret_size = size;
+
+ return err;
+}
+
+void log_error_stall(const char16_t *fmt, ...) {
+ va_list args;
+
+ assert(fmt);
+
+ int32_t attr = ST->ConOut->Mode->Attribute;
+ ST->ConOut->SetAttribute(ST->ConOut, EFI_LIGHTRED|EFI_BACKGROUND_BLACK);
+
+ if (ST->ConOut->Mode->CursorColumn > 0)
+ Print(L"\n");
+
+ va_start(args, fmt);
+ VPrint(fmt, args);
+ va_end(args);
+
+ Print(L"\n");
+
+ ST->ConOut->SetAttribute(ST->ConOut, attr);
+
+ /* Give the user a chance to see the message. */
+ BS->Stall(3 * 1000 * 1000);
+}
+
+EFI_STATUS log_oom(void) {
+ log_error_stall(L"Out of memory.");
+ return EFI_OUT_OF_RESOURCES;
+}
+
+void print_at(UINTN x, UINTN y, UINTN attr, const char16_t *str) {
+ assert(str);
+ ST->ConOut->SetCursorPosition(ST->ConOut, x, y);
+ ST->ConOut->SetAttribute(ST->ConOut, attr);
+ ST->ConOut->OutputString(ST->ConOut, (char16_t *) str);
+}
+
+void clear_screen(UINTN attr) {
+ ST->ConOut->SetAttribute(ST->ConOut, attr);
+ ST->ConOut->ClearScreen(ST->ConOut);
+}
+
+void sort_pointer_array(
+ void **array,
+ UINTN n_members,
+ compare_pointer_func_t compare) {
+
+ assert(array || n_members == 0);
+ assert(compare);
+
+ if (n_members <= 1)
+ return;
+
+ for (UINTN i = 1; i < n_members; i++) {
+ UINTN k;
+ void *entry = array[i];
+
+ for (k = i; k > 0; k--) {
+ if (compare(array[k - 1], entry) <= 0)
+ break;
+
+ array[k] = array[k - 1];
+ }
+
+ array[k] = entry;
+ }
+}
+
+EFI_STATUS get_file_info_harder(
+ EFI_FILE *handle,
+ EFI_FILE_INFO **ret,
+ UINTN *ret_size) {
+
+ UINTN size = offsetof(EFI_FILE_INFO, FileName) + 256;
+ _cleanup_free_ EFI_FILE_INFO *fi = NULL;
+ EFI_STATUS err;
+
+ assert(handle);
+ assert(ret);
+
+ /* A lot like LibFileInfo() but with useful error propagation */
+
+ fi = xmalloc(size);
+ err = handle->GetInfo(handle, &GenericFileInfo, &size, fi);
+ if (err == EFI_BUFFER_TOO_SMALL) {
+ free(fi);
+ fi = xmalloc(size); /* GetInfo tells us the required size, let's use that now */
+ err = handle->GetInfo(handle, &GenericFileInfo, &size, fi);
+ }
+
+ if (err != EFI_SUCCESS)
+ return err;
+
+ *ret = TAKE_PTR(fi);
+
+ if (ret_size)
+ *ret_size = size;
+
+ return EFI_SUCCESS;
+}
+
+EFI_STATUS readdir_harder(
+ EFI_FILE *handle,
+ EFI_FILE_INFO **buffer,
+ UINTN *buffer_size) {
+
+ EFI_STATUS err;
+ UINTN sz;
+
+ assert(handle);
+ assert(buffer);
+ assert(buffer_size);
+
+ /* buffer/buffer_size are both in and output parameters. Should be zero-initialized initially, and
+ * the specified buffer needs to be freed by caller, after final use. */
+
+ if (!*buffer) {
+ /* Some broken firmware violates the EFI spec by still advancing the readdir
+ * position when returning EFI_BUFFER_TOO_SMALL, effectively skipping over any files when
+ * the buffer was too small. Therefore, start with a buffer that should handle FAT32 max
+ * file name length.
+ * As a side effect, most readdir_harder() calls will now be slightly faster. */
+ sz = sizeof(EFI_FILE_INFO) + 256 * sizeof(char16_t);
+ *buffer = xmalloc(sz);
+ *buffer_size = sz;
+ } else
+ sz = *buffer_size;
+
+ err = handle->Read(handle, &sz, *buffer);
+ if (err == EFI_BUFFER_TOO_SMALL) {
+ free(*buffer);
+ *buffer = xmalloc(sz);
+ *buffer_size = sz;
+ err = handle->Read(handle, &sz, *buffer);
+ }
+ if (err != EFI_SUCCESS)
+ return err;
+
+ if (sz == 0) {
+ /* End of directory */
+ free(*buffer);
+ *buffer = NULL;
+ *buffer_size = 0;
+ }
+
+ return EFI_SUCCESS;
+}
+
+bool is_ascii(const char16_t *f) {
+ if (!f)
+ return false;
+
+ for (; *f != 0; f++)
+ if (*f > 127)
+ return false;
+
+ return true;
+}
+
+char16_t **strv_free(char16_t **v) {
+ if (!v)
+ return NULL;
+
+ for (char16_t **i = v; *i; i++)
+ free(*i);
+
+ free(v);
+ return NULL;
+}
+
+EFI_STATUS open_directory(
+ EFI_FILE *root,
+ const char16_t *path,
+ EFI_FILE **ret) {
+
+ _cleanup_(file_closep) EFI_FILE *dir = NULL;
+ _cleanup_free_ EFI_FILE_INFO *file_info = NULL;
+ EFI_STATUS err;
+
+ assert(root);
+
+ /* Opens a file, and then verifies it is actually a directory */
+
+ err = root->Open(root, &dir, (char16_t *) path, EFI_FILE_MODE_READ, 0);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ err = get_file_info_harder(dir, &file_info, NULL);
+ if (err != EFI_SUCCESS)
+ return err;
+ if (!FLAGS_SET(file_info->Attribute, EFI_FILE_DIRECTORY))
+ return EFI_LOAD_ERROR;
+
+ *ret = TAKE_PTR(dir);
+ return EFI_SUCCESS;
+}
+
+uint64_t get_os_indications_supported(void) {
+ uint64_t osind;
+ EFI_STATUS err;
+
+ /* Returns the supported OS indications. If we can't acquire it, returns a zeroed out mask, i.e. no
+ * supported features. */
+
+ err = efivar_get_uint64_le(EFI_GLOBAL_GUID, L"OsIndicationsSupported", &osind);
+ if (err != EFI_SUCCESS)
+ return 0;
+
+ return osind;
+}
+
+#ifdef EFI_DEBUG
+__attribute__((noinline)) void debug_break(void) {
+ /* This is a poor programmer's breakpoint to wait until a debugger
+ * has attached to us. Just "set variable wait = 0" or "return" to continue. */
+ volatile bool wait = true;
+ while (wait)
+ /* Prefer asm based stalling so that gdb has a source location to present. */
+#if defined(__i386__) || defined(__x86_64__)
+ asm volatile("pause");
+#elif defined(__aarch64__)
+ asm volatile("wfi");
+#else
+ BS->Stall(5000);
+#endif
+}
+#endif
+
+
+#ifdef EFI_DEBUG
+void hexdump(const char16_t *prefix, const void *data, UINTN size) {
+ static const char hex[16] = "0123456789abcdef";
+ _cleanup_free_ char16_t *buf = NULL;
+ const uint8_t *d = data;
+
+ assert(prefix);
+ assert(data || size == 0);
+
+ /* Debugging helper — please keep this around, even if not used */
+
+ buf = xnew(char16_t, size*2+1);
+
+ for (UINTN i = 0; i < size; i++) {
+ buf[i*2] = hex[d[i] >> 4];
+ buf[i*2+1] = hex[d[i] & 0x0F];
+ }
+
+ buf[size*2] = 0;
+
+ log_error_stall(L"%s[%" PRIuN "]: %s", prefix, size, buf);
+}
+#endif
+
+#if defined(__i386__) || defined(__x86_64__)
+static inline uint8_t inb(uint16_t port) {
+ uint8_t value;
+ asm volatile("inb %1, %0" : "=a"(value) : "Nd"(port));
+ return value;
+}
+
+static inline void outb(uint16_t port, uint8_t value) {
+ asm volatile("outb %0, %1" : : "a"(value), "Nd"(port));
+}
+
+void beep(UINTN beep_count) {
+ enum {
+ PITCH = 500,
+ BEEP_DURATION_USEC = 100 * 1000,
+ WAIT_DURATION_USEC = 400 * 1000,
+
+ PIT_FREQUENCY = 0x1234dd,
+ SPEAKER_CONTROL_PORT = 0x61,
+ SPEAKER_ON_MASK = 0x03,
+ TIMER_PORT_MAGIC = 0xB6,
+ TIMER_CONTROL_PORT = 0x43,
+ TIMER_CONTROL2_PORT = 0x42,
+ };
+
+ /* Set frequency. */
+ uint32_t counter = PIT_FREQUENCY / PITCH;
+ outb(TIMER_CONTROL_PORT, TIMER_PORT_MAGIC);
+ outb(TIMER_CONTROL2_PORT, counter & 0xFF);
+ outb(TIMER_CONTROL2_PORT, (counter >> 8) & 0xFF);
+
+ uint8_t value = inb(SPEAKER_CONTROL_PORT);
+
+ while (beep_count > 0) {
+ /* Turn speaker on. */
+ value |= SPEAKER_ON_MASK;
+ outb(SPEAKER_CONTROL_PORT, value);
+
+ BS->Stall(BEEP_DURATION_USEC);
+
+ /* Turn speaker off. */
+ value &= ~SPEAKER_ON_MASK;
+ outb(SPEAKER_CONTROL_PORT, value);
+
+ beep_count--;
+ if (beep_count > 0)
+ BS->Stall(WAIT_DURATION_USEC);
+ }
+}
+#endif
+
+EFI_STATUS open_volume(EFI_HANDLE device, EFI_FILE **ret_file) {
+ EFI_STATUS err;
+ EFI_FILE *file;
+ EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *volume;
+
+ assert(ret_file);
+
+ err = BS->HandleProtocol(device, &FileSystemProtocol, (void **) &volume);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ err = volume->OpenVolume(volume, &file);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ *ret_file = file;
+ return EFI_SUCCESS;
+}
+
+EFI_STATUS make_file_device_path(EFI_HANDLE device, const char16_t *file, EFI_DEVICE_PATH **ret_dp) {
+ EFI_STATUS err;
+ EFI_DEVICE_PATH *dp;
+
+ assert(file);
+ assert(ret_dp);
+
+ err = BS->HandleProtocol(device, &DevicePathProtocol, (void **) &dp);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ EFI_DEVICE_PATH *end_node = dp;
+ while (!IsDevicePathEnd(end_node))
+ end_node = NextDevicePathNode(end_node);
+
+ size_t file_size = strsize16(file);
+ size_t dp_size = (uint8_t *) end_node - (uint8_t *) dp;
+
+ /* Make a copy that can also hold a file media device path. */
+ *ret_dp = xmalloc(dp_size + file_size + SIZE_OF_FILEPATH_DEVICE_PATH + END_DEVICE_PATH_LENGTH);
+ dp = mempcpy(*ret_dp, dp, dp_size);
+
+ /* Replace end node with file media device path. Use memcpy() in case dp is unaligned (if accessed as
+ * FILEPATH_DEVICE_PATH). */
+ dp->Type = MEDIA_DEVICE_PATH;
+ dp->SubType = MEDIA_FILEPATH_DP;
+ memcpy((uint8_t *) dp + offsetof(FILEPATH_DEVICE_PATH, PathName), file, file_size);
+ SetDevicePathNodeLength(dp, offsetof(FILEPATH_DEVICE_PATH, PathName) + file_size);
+
+ dp = NextDevicePathNode(dp);
+ SetDevicePathEndNode(dp);
+ return EFI_SUCCESS;
+}
+
+EFI_STATUS device_path_to_str(const EFI_DEVICE_PATH *dp, char16_t **ret) {
+ EFI_DEVICE_PATH_TO_TEXT_PROTOCOL *dp_to_text;
+ EFI_STATUS err;
+ _cleanup_free_ char16_t *str = NULL;
+
+ assert(dp);
+ assert(ret);
+
+ err = BS->LocateProtocol(&(EFI_GUID) EFI_DEVICE_PATH_TO_TEXT_PROTOCOL_GUID, NULL, (void **) &dp_to_text);
+ if (err != EFI_SUCCESS) {
+ /* If the device path to text protocol is not available we can still do a best-effort attempt
+ * to convert it ourselves if we are given filepath-only device path. */
+
+ size_t size = 0;
+ for (const EFI_DEVICE_PATH *node = dp; !IsDevicePathEnd(node);
+ node = NextDevicePathNode(node)) {
+
+ if (DevicePathType(node) != MEDIA_DEVICE_PATH ||
+ DevicePathSubType(node) != MEDIA_FILEPATH_DP)
+ return err;
+
+ size_t path_size = DevicePathNodeLength(node);
+ if (path_size <= offsetof(FILEPATH_DEVICE_PATH, PathName) || path_size % sizeof(char16_t))
+ return EFI_INVALID_PARAMETER;
+ path_size -= offsetof(FILEPATH_DEVICE_PATH, PathName);
+
+ _cleanup_free_ char16_t *old = str;
+ str = xmalloc(size + path_size);
+ if (old) {
+ memcpy(str, old, size);
+ str[size / sizeof(char16_t) - 1] = '\\';
+ }
+
+ memcpy(str + (size / sizeof(char16_t)),
+ ((uint8_t *) node) + offsetof(FILEPATH_DEVICE_PATH, PathName),
+ path_size);
+ size += path_size;
+ }
+
+ *ret = TAKE_PTR(str);
+ return EFI_SUCCESS;
+ }
+
+ str = dp_to_text->ConvertDevicePathToText(dp, false, false);
+ if (!str)
+ return EFI_OUT_OF_RESOURCES;
+
+ *ret = TAKE_PTR(str);
+ return EFI_SUCCESS;
+}
+
+#if defined(__i386__) || defined(__x86_64__)
+bool in_hypervisor(void) {
+ uint32_t eax, ebx, ecx, edx;
+
+ /* This is a dumbed down version of src/basic/virt.c's detect_vm() that safely works in the UEFI
+ * environment. */
+
+ if (__get_cpuid(1, &eax, &ebx, &ecx, &edx) == 0)
+ return false;
+
+ return !!(ecx & 0x80000000U);
+}
+#endif
diff --git a/src/boot/efi/util.h b/src/boot/efi/util.h
new file mode 100644
index 0000000..bcab0df
--- /dev/null
+++ b/src/boot/efi/util.h
@@ -0,0 +1,220 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <efi.h>
+#include <efilib.h>
+#include <stddef.h>
+
+#include "string-util-fundamental.h"
+
+#define UINTN_MAX (~(UINTN)0)
+#define INTN_MAX ((INTN)(UINTN_MAX>>1))
+
+/* gnu-efi format specifiers for integers are fixed to either 64bit with 'l' and 32bit without a size prefix.
+ * We rely on %u/%d/%x to format regular ints, so ensure the size is what we expect. At the same time, we also
+ * need specifiers for (U)INTN which are native (pointer) sized. */
+assert_cc(sizeof(int) == sizeof(uint32_t));
+#if __SIZEOF_POINTER__ == 4
+# define PRIuN L"u"
+# define PRIiN L"d"
+#elif __SIZEOF_POINTER__ == 8
+# define PRIuN L"lu"
+# define PRIiN L"ld"
+#else
+# error "Unexpected pointer size"
+#endif
+
+static inline void free(void *p) {
+ if (!p)
+ return;
+
+ /* Debugging an invalid free requires trace logging to find the call site or a debugger attached. For
+ * release builds it is not worth the bother to even warn when we cannot even print a call stack. */
+#ifdef EFI_DEBUG
+ assert_se(BS->FreePool(p) == EFI_SUCCESS);
+#else
+ (void) BS->FreePool(p);
+#endif
+}
+
+static inline void freep(void *p) {
+ free(*(void **) p);
+}
+
+#define _cleanup_free_ _cleanup_(freep)
+
+_malloc_ _alloc_(1) _returns_nonnull_ _warn_unused_result_
+static inline void *xmalloc(size_t size) {
+ void *p;
+ assert_se(BS->AllocatePool(EfiLoaderData, size, &p) == EFI_SUCCESS);
+ return p;
+}
+
+_malloc_ _alloc_(1, 2) _returns_nonnull_ _warn_unused_result_
+static inline void *xmalloc_multiply(size_t n, size_t size) {
+ assert_se(!__builtin_mul_overflow(size, n, &size));
+ return xmalloc(size);
+}
+
+/* Use malloc attribute as this never returns p like userspace realloc. */
+_malloc_ _alloc_(3) _returns_nonnull_ _warn_unused_result_
+static inline void *xrealloc(void *p, size_t old_size, size_t new_size) {
+ void *r = xmalloc(new_size);
+ new_size = MIN(old_size, new_size);
+ if (new_size > 0)
+ memcpy(r, p, new_size);
+ free(p);
+ return r;
+}
+
+#define xpool_print(fmt, ...) ((char16_t *) ASSERT_SE_PTR(PoolPrint((fmt), ##__VA_ARGS__)))
+#define xnew(type, n) ((type *) xmalloc_multiply((n), sizeof(type)))
+
+typedef struct {
+ EFI_PHYSICAL_ADDRESS addr;
+ size_t n_pages;
+} Pages;
+
+static inline void cleanup_pages(Pages *p) {
+ if (p->n_pages == 0)
+ return;
+#ifdef EFI_DEBUG
+ assert_se(BS->FreePages(p->addr, p->n_pages) == EFI_SUCCESS);
+#else
+ (void) BS->FreePages(p->addr, p->n_pages);
+#endif
+}
+
+#define _cleanup_pages_ _cleanup_(cleanup_pages)
+
+static inline Pages xmalloc_pages(
+ EFI_ALLOCATE_TYPE type, EFI_MEMORY_TYPE memory_type, size_t n_pages, EFI_PHYSICAL_ADDRESS addr) {
+ assert_se(BS->AllocatePages(type, memory_type, n_pages, &addr) == EFI_SUCCESS);
+ return (Pages) {
+ .addr = addr,
+ .n_pages = n_pages,
+ };
+}
+
+EFI_STATUS parse_boolean(const char *v, bool *b);
+
+EFI_STATUS efivar_set(const EFI_GUID *vendor, const char16_t *name, const char16_t *value, uint32_t flags);
+EFI_STATUS efivar_set_raw(const EFI_GUID *vendor, const char16_t *name, const void *buf, UINTN size, uint32_t flags);
+EFI_STATUS efivar_set_uint_string(const EFI_GUID *vendor, const char16_t *name, UINTN i, uint32_t flags);
+EFI_STATUS efivar_set_uint32_le(const EFI_GUID *vendor, const char16_t *NAME, uint32_t value, uint32_t flags);
+EFI_STATUS efivar_set_uint64_le(const EFI_GUID *vendor, const char16_t *name, uint64_t value, uint32_t flags);
+void efivar_set_time_usec(const EFI_GUID *vendor, const char16_t *name, uint64_t usec);
+
+EFI_STATUS efivar_get(const EFI_GUID *vendor, const char16_t *name, char16_t **value);
+EFI_STATUS efivar_get_raw(const EFI_GUID *vendor, const char16_t *name, char **buffer, UINTN *size);
+EFI_STATUS efivar_get_uint_string(const EFI_GUID *vendor, const char16_t *name, UINTN *i);
+EFI_STATUS efivar_get_uint32_le(const EFI_GUID *vendor, const char16_t *name, uint32_t *ret);
+EFI_STATUS efivar_get_uint64_le(const EFI_GUID *vendor, const char16_t *name, uint64_t *ret);
+EFI_STATUS efivar_get_boolean_u8(const EFI_GUID *vendor, const char16_t *name, bool *ret);
+
+void convert_efi_path(char16_t *path);
+char16_t *xstr8_to_path(const char *stra);
+void mangle_stub_cmdline(char16_t *cmdline);
+
+EFI_STATUS file_read(EFI_FILE *dir, const char16_t *name, UINTN off, UINTN size, char **content, UINTN *content_size);
+EFI_STATUS chunked_read(EFI_FILE *file, size_t *size, void *buf);
+
+static inline void file_closep(EFI_FILE **handle) {
+ if (!*handle)
+ return;
+
+ (*handle)->Close(*handle);
+}
+
+static inline void unload_imagep(EFI_HANDLE *image) {
+ if (*image)
+ (void) BS->UnloadImage(*image);
+}
+
+/*
+ * Allocated random UUID, intended to be shared across tools that implement
+ * the (ESP)\loader\entries\<vendor>-<revision>.conf convention and the
+ * associated EFI variables.
+ */
+#define LOADER_GUID \
+ &(const EFI_GUID) { 0x4a67b082, 0x0a4c, 0x41cf, { 0xb6, 0xc7, 0x44, 0x0b, 0x29, 0xbb, 0x8c, 0x4f } }
+#define EFI_GLOBAL_GUID &(const EFI_GUID) EFI_GLOBAL_VARIABLE
+
+void log_error_stall(const char16_t *fmt, ...);
+EFI_STATUS log_oom(void);
+
+/* This works just like log_error_errno() from userspace, but requires you
+ * to provide err a second time if you want to use %r in the message! */
+#define log_error_status_stall(err, fmt, ...) \
+ ({ \
+ log_error_stall(fmt, ##__VA_ARGS__); \
+ err; \
+ })
+
+void print_at(UINTN x, UINTN y, UINTN attr, const char16_t *str);
+void clear_screen(UINTN attr);
+
+typedef int (*compare_pointer_func_t)(const void *a, const void *b);
+void sort_pointer_array(void **array, UINTN n_members, compare_pointer_func_t compare);
+
+EFI_STATUS get_file_info_harder(EFI_FILE *handle, EFI_FILE_INFO **ret, UINTN *ret_size);
+
+EFI_STATUS readdir_harder(EFI_FILE *handle, EFI_FILE_INFO **buffer, UINTN *buffer_size);
+
+bool is_ascii(const char16_t *f);
+
+char16_t **strv_free(char16_t **l);
+
+static inline void strv_freep(char16_t ***p) {
+ strv_free(*p);
+}
+
+EFI_STATUS open_directory(EFI_FILE *root_dir, const char16_t *path, EFI_FILE **ret);
+
+/* Conversion between EFI_PHYSICAL_ADDRESS and pointers is not obvious. The former is always 64bit, even on
+ * 32bit archs. And gcc complains if we cast a pointer to an integer of a different size. Hence let's do the
+ * conversion indirectly: first into uintptr_t and then extended to EFI_PHYSICAL_ADDRESS. */
+static inline EFI_PHYSICAL_ADDRESS POINTER_TO_PHYSICAL_ADDRESS(const void *p) {
+ return (EFI_PHYSICAL_ADDRESS) (uintptr_t) p;
+}
+
+static inline void *PHYSICAL_ADDRESS_TO_POINTER(EFI_PHYSICAL_ADDRESS addr) {
+ /* On 32bit systems the address might not be convertible (as pointers are 32bit but
+ * EFI_PHYSICAL_ADDRESS 64bit) */
+ assert(addr <= UINTPTR_MAX);
+ return (void *) (uintptr_t) addr;
+}
+
+uint64_t get_os_indications_supported(void);
+
+#ifdef EFI_DEBUG
+void debug_break(void);
+extern uint8_t _text, _data;
+/* Report the relocated position of text and data sections so that a debugger
+ * can attach to us. See debug-sd-boot.sh for how this can be done. */
+# define debug_hook(identity) Print(identity L"@0x%lx,0x%lx\n", POINTER_TO_PHYSICAL_ADDRESS(&_text), POINTER_TO_PHYSICAL_ADDRESS(&_data))
+#else
+# define debug_hook(identity)
+#endif
+
+#ifdef EFI_DEBUG
+void hexdump(const char16_t *prefix, const void *data, UINTN size);
+#endif
+
+#if defined(__i386__) || defined(__x86_64__)
+void beep(UINTN beep_count);
+#else
+static inline void beep(UINTN beep_count) {}
+#endif
+
+EFI_STATUS open_volume(EFI_HANDLE device, EFI_FILE **ret_file);
+EFI_STATUS make_file_device_path(EFI_HANDLE device, const char16_t *file, EFI_DEVICE_PATH **ret_dp);
+EFI_STATUS device_path_to_str(const EFI_DEVICE_PATH *dp, char16_t **ret);
+
+#if defined(__i386__) || defined(__x86_64__)
+bool in_hypervisor(void);
+#else
+static inline bool in_hypervisor(void) {
+ return false;
+}
+#endif
diff --git a/src/boot/efi/vmm.c b/src/boot/efi/vmm.c
new file mode 100644
index 0000000..f840705
--- /dev/null
+++ b/src/boot/efi/vmm.c
@@ -0,0 +1,39 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <efi.h>
+#include <efilib.h>
+#include <stdbool.h>
+
+#include "drivers.h"
+#include "efi-string.h"
+#include "string-util-fundamental.h"
+#include "util.h"
+
+#define QEMU_KERNEL_LOADER_FS_MEDIA_GUID \
+ { 0x1428f772, 0xb64a, 0x441e, {0xb8, 0xc3, 0x9e, 0xbd, 0xd7, 0xf8, 0x93, 0xc7 }}
+
+#define VMM_BOOT_ORDER_GUID \
+ { 0x668f4529, 0x63d0, 0x4bb5, {0xb6, 0x5d, 0x6f, 0xbb, 0x9d, 0x36, 0xa4, 0x4a }}
+
+/* detect direct boot */
+bool is_direct_boot(EFI_HANDLE device) {
+ EFI_STATUS err;
+ VENDOR_DEVICE_PATH *dp;
+
+ err = BS->HandleProtocol(device, &DevicePathProtocol, (void **) &dp);
+ if (err != EFI_SUCCESS)
+ return false;
+
+ /* 'qemu -kernel systemd-bootx64.efi' */
+ if (dp->Header.Type == MEDIA_DEVICE_PATH &&
+ dp->Header.SubType == MEDIA_VENDOR_DP &&
+ memcmp(&dp->Guid, &(EFI_GUID)QEMU_KERNEL_LOADER_FS_MEDIA_GUID, sizeof(EFI_GUID)) == 0)
+ return true;
+
+ /* loaded from firmware volume (sd-boot added to ovmf) */
+ if (dp->Header.Type == MEDIA_DEVICE_PATH &&
+ dp->Header.SubType == MEDIA_PIWG_FW_VOL_DP)
+ return true;
+
+ return false;
+}
diff --git a/src/boot/efi/vmm.h b/src/boot/efi/vmm.h
new file mode 100644
index 0000000..c8eb84f
--- /dev/null
+++ b/src/boot/efi/vmm.h
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <efi.h>
+#include <efilib.h>
+
+bool is_direct_boot(EFI_HANDLE device);
diff --git a/src/boot/efi/xbootldr.c b/src/boot/efi/xbootldr.c
new file mode 100644
index 0000000..7fef909
--- /dev/null
+++ b/src/boot/efi/xbootldr.c
@@ -0,0 +1,285 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <efi.h>
+#include <efigpt.h>
+#include <efilib.h>
+
+#include "util.h"
+#include "xbootldr.h"
+
+union GptHeaderBuffer {
+ EFI_PARTITION_TABLE_HEADER gpt_header;
+ uint8_t space[CONST_ALIGN_TO(sizeof(EFI_PARTITION_TABLE_HEADER), 512)];
+};
+
+static EFI_DEVICE_PATH *path_replace_hd(
+ const EFI_DEVICE_PATH *path,
+ const EFI_DEVICE_PATH *node,
+ const HARDDRIVE_DEVICE_PATH *new_node) {
+
+ /* Create a new device path as a copy of path, while chopping off the remainder starting at the given
+ * node. If new_node is provided, it is appended at the end of the new path. */
+
+ assert(path);
+ assert(node);
+
+ size_t len = (uint8_t *) node - (uint8_t *) path, new_node_len = 0;
+ if (new_node)
+ new_node_len = DevicePathNodeLength(&new_node->Header);
+
+ EFI_DEVICE_PATH *ret = xmalloc(len + new_node_len + END_DEVICE_PATH_LENGTH);
+ EFI_DEVICE_PATH *end = mempcpy(ret, path, len);
+
+ if (new_node)
+ end = mempcpy(end, new_node, new_node_len);
+
+ SetDevicePathEndNode(end);
+ return ret;
+}
+
+static bool verify_gpt(union GptHeaderBuffer *gpt_header_buffer, EFI_LBA lba_expected) {
+ EFI_PARTITION_TABLE_HEADER *h;
+ uint32_t crc32, crc32_saved;
+ EFI_STATUS err;
+
+ assert(gpt_header_buffer);
+
+ h = &gpt_header_buffer->gpt_header;
+
+ /* Some superficial validation of the GPT header */
+ if (memcmp(&h->Header.Signature, "EFI PART", sizeof(h->Header.Signature)) != 0)
+ return false;
+
+ if (h->Header.HeaderSize < 92 || h->Header.HeaderSize > 512)
+ return false;
+
+ if (h->Header.Revision != 0x00010000U)
+ return false;
+
+ /* Calculate CRC check */
+ crc32_saved = h->Header.CRC32;
+ h->Header.CRC32 = 0;
+ err = BS->CalculateCrc32(gpt_header_buffer, h->Header.HeaderSize, &crc32);
+ h->Header.CRC32 = crc32_saved;
+ if (err != EFI_SUCCESS || crc32 != crc32_saved)
+ return false;
+
+ if (h->MyLBA != lba_expected)
+ return false;
+
+ if ((h->SizeOfPartitionEntry % sizeof(EFI_PARTITION_ENTRY)) != 0)
+ return false;
+
+ if (h->NumberOfPartitionEntries <= 0 || h->NumberOfPartitionEntries > 1024)
+ return false;
+
+ /* overflow check */
+ if (h->SizeOfPartitionEntry > UINTN_MAX / h->NumberOfPartitionEntries)
+ return false;
+
+ return true;
+}
+
+static EFI_STATUS try_gpt(
+ EFI_BLOCK_IO_PROTOCOL *block_io,
+ EFI_LBA lba,
+ EFI_LBA *ret_backup_lba, /* May be changed even on error! */
+ HARDDRIVE_DEVICE_PATH *ret_hd) {
+
+ _cleanup_free_ EFI_PARTITION_ENTRY *entries = NULL;
+ union GptHeaderBuffer gpt;
+ EFI_STATUS err;
+ uint32_t crc32;
+ UINTN size;
+
+ assert(block_io);
+ assert(ret_hd);
+
+ /* Read the GPT header */
+ err = block_io->ReadBlocks(
+ block_io,
+ block_io->Media->MediaId,
+ lba,
+ sizeof(gpt), &gpt);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ /* Indicate the location of backup LBA even if the rest of the header is corrupt. */
+ if (ret_backup_lba)
+ *ret_backup_lba = gpt.gpt_header.AlternateLBA;
+
+ if (!verify_gpt(&gpt, lba))
+ return EFI_NOT_FOUND;
+
+ /* Now load the GPT entry table */
+ size = ALIGN_TO((UINTN) gpt.gpt_header.SizeOfPartitionEntry * (UINTN) gpt.gpt_header.NumberOfPartitionEntries, 512);
+ entries = xmalloc(size);
+
+ err = block_io->ReadBlocks(
+ block_io,
+ block_io->Media->MediaId,
+ gpt.gpt_header.PartitionEntryLBA,
+ size, entries);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ /* Calculate CRC of entries array, too */
+ err = BS->CalculateCrc32(entries, size, &crc32);
+ if (err != EFI_SUCCESS || crc32 != gpt.gpt_header.PartitionEntryArrayCRC32)
+ return EFI_CRC_ERROR;
+
+ /* Now we can finally look for xbootloader partitions. */
+ for (UINTN i = 0; i < gpt.gpt_header.NumberOfPartitionEntries; i++) {
+ EFI_PARTITION_ENTRY *entry =
+ (EFI_PARTITION_ENTRY *) ((uint8_t *) entries + gpt.gpt_header.SizeOfPartitionEntry * i);
+
+ if (memcmp(&entry->PartitionTypeGUID, XBOOTLDR_GUID, sizeof(entry->PartitionTypeGUID)) != 0)
+ continue;
+
+ if (entry->EndingLBA < entry->StartingLBA) /* Bogus? */
+ continue;
+
+ *ret_hd = (HARDDRIVE_DEVICE_PATH) {
+ .Header = {
+ .Type = MEDIA_DEVICE_PATH,
+ .SubType = MEDIA_HARDDRIVE_DP,
+ },
+ .PartitionNumber = i + 1,
+ .PartitionStart = entry->StartingLBA,
+ .PartitionSize = entry->EndingLBA - entry->StartingLBA + 1,
+ .MBRType = MBR_TYPE_EFI_PARTITION_TABLE_HEADER,
+ .SignatureType = SIGNATURE_TYPE_GUID,
+ };
+ memcpy(ret_hd->Signature, &entry->UniquePartitionGUID, sizeof(ret_hd->Signature));
+
+ /* HARDDRIVE_DEVICE_PATH has padding, which at least OVMF does not like. */
+ SetDevicePathNodeLength(
+ &ret_hd->Header,
+ offsetof(HARDDRIVE_DEVICE_PATH, SignatureType) + sizeof(ret_hd->SignatureType));
+
+ return EFI_SUCCESS;
+ }
+
+ /* This GPT was fully valid, but we didn't find what we are looking for. This
+ * means there's no reason to check the second copy of the GPT header */
+ return EFI_NOT_FOUND;
+}
+
+static EFI_STATUS find_device(EFI_HANDLE *device, EFI_DEVICE_PATH **ret_device_path) {
+ EFI_STATUS err;
+
+ assert(device);
+ assert(ret_device_path);
+
+ EFI_DEVICE_PATH *partition_path;
+ err = BS->HandleProtocol(device, &DevicePathProtocol, (void **) &partition_path);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ /* Find the (last) partition node itself. */
+ EFI_DEVICE_PATH *part_node = NULL;
+ for (EFI_DEVICE_PATH *node = partition_path; !IsDevicePathEnd(node); node = NextDevicePathNode(node)) {
+ if (DevicePathType(node) != MEDIA_DEVICE_PATH)
+ continue;
+
+ if (DevicePathSubType(node) != MEDIA_HARDDRIVE_DP)
+ continue;
+
+ part_node = node;
+ }
+
+ if (!part_node)
+ return EFI_NOT_FOUND;
+
+ /* Chop off the partition part, leaving us with the full path to the disk itself. */
+ _cleanup_free_ EFI_DEVICE_PATH *disk_path = NULL;
+ EFI_DEVICE_PATH *p = disk_path = path_replace_hd(partition_path, part_node, NULL);
+
+ EFI_HANDLE disk_handle;
+ EFI_BLOCK_IO_PROTOCOL *block_io;
+ err = BS->LocateDevicePath(&BlockIoProtocol, &p, &disk_handle);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ /* The drivers for other partitions on this drive may not be initialized on fastboot firmware, so we
+ * have to ask the firmware to do just that. */
+ (void) BS->ConnectController(disk_handle, NULL, NULL, true);
+
+ err = BS->HandleProtocol(disk_handle, &BlockIoProtocol, (void **)&block_io);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ /* Filter out some block devices early. (We only care about block devices that aren't
+ * partitions themselves — we look for GPT partition tables to parse after all —, and only
+ * those which contain a medium and have at least 2 blocks.) */
+ if (block_io->Media->LogicalPartition ||
+ !block_io->Media->MediaPresent ||
+ block_io->Media->LastBlock <= 1)
+ return EFI_NOT_FOUND;
+
+ /* Try several copies of the GPT header, in case one is corrupted */
+ EFI_LBA backup_lba = 0;
+ for (UINTN nr = 0; nr < 3; nr++) {
+ EFI_LBA lba;
+
+ /* Read the first copy at LBA 1 and then try the backup GPT header pointed
+ * to by the first header if that one was corrupted. As a last resort,
+ * try the very last LBA of this block device. */
+ if (nr == 0)
+ lba = 1;
+ else if (nr == 1 && backup_lba != 0)
+ lba = backup_lba;
+ else if (nr == 2 && backup_lba != block_io->Media->LastBlock)
+ lba = block_io->Media->LastBlock;
+ else
+ continue;
+
+ HARDDRIVE_DEVICE_PATH hd;
+ err = try_gpt(
+ block_io, lba,
+ nr == 0 ? &backup_lba : NULL, /* Only get backup LBA location from first GPT header. */
+ &hd);
+ if (err != EFI_SUCCESS) {
+ /* GPT was valid but no XBOOT loader partition found. */
+ if (err == EFI_NOT_FOUND)
+ break;
+ /* Bad GPT, try next one. */
+ continue;
+ }
+
+ /* Patch in the data we found */
+ *ret_device_path = path_replace_hd(partition_path, part_node, &hd);
+ return EFI_SUCCESS;
+ }
+
+ /* No xbootloader partition found */
+ return EFI_NOT_FOUND;
+}
+
+EFI_STATUS xbootldr_open(EFI_HANDLE *device, EFI_HANDLE *ret_device, EFI_FILE **ret_root_dir) {
+ _cleanup_free_ EFI_DEVICE_PATH *partition_path = NULL;
+ EFI_HANDLE new_device;
+ EFI_FILE *root_dir;
+ EFI_STATUS err;
+
+ assert(device);
+ assert(ret_device);
+ assert(ret_root_dir);
+
+ err = find_device(device, &partition_path);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ EFI_DEVICE_PATH *dp = partition_path;
+ err = BS->LocateDevicePath(&BlockIoProtocol, &dp, &new_device);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ err = open_volume(new_device, &root_dir);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ *ret_device = new_device;
+ *ret_root_dir = root_dir;
+ return EFI_SUCCESS;
+}
diff --git a/src/boot/efi/xbootldr.h b/src/boot/efi/xbootldr.h
new file mode 100644
index 0000000..205ce71
--- /dev/null
+++ b/src/boot/efi/xbootldr.h
@@ -0,0 +1,9 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <efi.h>
+
+#define XBOOTLDR_GUID \
+ &(const EFI_GUID) { 0xbc13c2ff, 0x59e6, 0x4262, { 0xa3, 0x52, 0xb2, 0x75, 0xfd, 0x6f, 0x71, 0x72 } }
+
+EFI_STATUS xbootldr_open(EFI_HANDLE *device, EFI_HANDLE *ret_device, EFI_FILE **ret_root_dir);
diff --git a/src/boot/measure.c b/src/boot/measure.c
new file mode 100644
index 0000000..0bbd386
--- /dev/null
+++ b/src/boot/measure.c
@@ -0,0 +1,1164 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "efi-loader.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "hexdecoct.h"
+#include "json.h"
+#include "main-func.h"
+#include "openssl-util.h"
+#include "parse-argument.h"
+#include "parse-util.h"
+#include "pretty-print.h"
+#include "sha256.h"
+#include "terminal-util.h"
+#include "tpm-pcr.h"
+#include "tpm2-util.h"
+#include "verbs.h"
+
+/* Tool for pre-calculating expected TPM PCR values based on measured resources. This is intended to be used
+ * to pre-calculate suitable values for PCR 11, the way sd-stub measures into it. */
+
+static char *arg_sections[_UNIFIED_SECTION_MAX] = {};
+static char **arg_banks = NULL;
+static char *arg_tpm2_device = NULL;
+static char *arg_private_key = NULL;
+static char *arg_public_key = NULL;
+static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO|JSON_FORMAT_OFF;
+static PagerFlags arg_pager_flags = 0;
+static bool arg_current = false;
+static char **arg_phase = NULL;
+
+STATIC_DESTRUCTOR_REGISTER(arg_banks, strv_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_private_key, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_public_key, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_phase, strv_freep);
+
+static inline void free_sections(char*(*sections)[_UNIFIED_SECTION_MAX]) {
+ for (UnifiedSection c = 0; c < _UNIFIED_SECTION_MAX; c++)
+ free((*sections)[c]);
+}
+
+STATIC_DESTRUCTOR_REGISTER(arg_sections, free_sections);
+
+static int help(int argc, char *argv[], void *userdata) {
+ _cleanup_free_ char *link = NULL;
+ int r;
+
+ r = terminal_urlify_man("systemd-measure", "1", &link);
+ if (r < 0)
+ return log_oom();
+
+ printf("%1$s [OPTIONS...] COMMAND ...\n"
+ "\n%5$sPre-calculate and sign PCR hash for a unified kernel image (UKI).%6$s\n"
+ "\n%3$sCommands:%4$s\n"
+ " status Show current PCR values\n"
+ " calculate Calculate expected PCR values\n"
+ " sign Calculate and sign expected PCR values\n"
+ "\n%3$sOptions:%4$s\n"
+ " -h --help Show this help\n"
+ " --version Print version\n"
+ " --no-pager Do not pipe output into a pager\n"
+ " -c --current Use current PCR values\n"
+ " --phase=PHASE Specify a boot phase to sign for\n"
+ " --bank=DIGEST Select TPM bank (SHA1, SHA256, SHA384, SHA512)\n"
+ " --tpm2-device=PATH Use specified TPM2 device\n"
+ " --private-key=KEY Private key (PEM) to sign with\n"
+ " --public-key=KEY Public key (PEM) to validate against\n"
+ " --json=MODE Output as JSON\n"
+ " -j Same as --json=pretty on tty, --json=short otherwise\n"
+ "\n%3$sUKI PE Section Options:%4$s %3$sUKI PE Section%4$s\n"
+ " --linux=PATH Path to Linux kernel image file %7$s .linux\n"
+ " --osrel=PATH Path to os-release file %7$s .osrel\n"
+ " --cmdline=PATH Path to file with kernel command line %7$s .cmdline\n"
+ " --initrd=PATH Path to initrd image file %7$s .initrd\n"
+ " --splash=PATH Path to splash bitmap file %7$s .splash\n"
+ " --dtb=PATH Path to Devicetree file %7$s .dtb\n"
+ " --pcrpkey=PATH Path to public key for PCR signatures %7$s .pcrpkey\n"
+ "\nSee the %2$s for details.\n",
+ program_invocation_short_name,
+ link,
+ ansi_underline(),
+ ansi_normal(),
+ ansi_highlight(),
+ ansi_normal(),
+ special_glyph(SPECIAL_GLYPH_ARROW_RIGHT));
+
+ return 0;
+}
+
+static char *normalize_phase(const char *s) {
+ _cleanup_strv_free_ char **l = NULL;
+
+ /* Let's normalize phase expressions. We split the series of colon-separated words up, then remove
+ * all empty ones, and glue them back together again. In other words we remove duplicate ":", as well
+ * as leading and trailing ones. */
+
+ l = strv_split(s, ":"); /* Split series of words */
+ if (!l)
+ return NULL;
+
+ /* Remove all empty words and glue things back together */
+ return strv_join(strv_remove(l, ""), ":");
+}
+
+static int parse_argv(int argc, char *argv[]) {
+ enum {
+ ARG_VERSION = 0x100,
+ ARG_NO_PAGER,
+ _ARG_SECTION_FIRST,
+ ARG_LINUX = _ARG_SECTION_FIRST,
+ ARG_OSREL,
+ ARG_CMDLINE,
+ ARG_INITRD,
+ ARG_SPLASH,
+ ARG_DTB,
+ _ARG_PCRSIG, /* the .pcrsig section is not input for signing, hence not actually an argument here */
+ _ARG_SECTION_LAST,
+ ARG_PCRPKEY = _ARG_SECTION_LAST,
+ ARG_BANK,
+ ARG_PRIVATE_KEY,
+ ARG_PUBLIC_KEY,
+ ARG_TPM2_DEVICE,
+ ARG_JSON,
+ ARG_PHASE,
+ };
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "no-pager", no_argument, NULL, ARG_NO_PAGER },
+ { "version", no_argument, NULL, ARG_VERSION },
+ { "linux", required_argument, NULL, ARG_LINUX },
+ { "osrel", required_argument, NULL, ARG_OSREL },
+ { "cmdline", required_argument, NULL, ARG_CMDLINE },
+ { "initrd", required_argument, NULL, ARG_INITRD },
+ { "splash", required_argument, NULL, ARG_SPLASH },
+ { "dtb", required_argument, NULL, ARG_DTB },
+ { "pcrpkey", required_argument, NULL, ARG_PCRPKEY },
+ { "current", no_argument, NULL, 'c' },
+ { "bank", required_argument, NULL, ARG_BANK },
+ { "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE },
+ { "private-key", required_argument, NULL, ARG_PRIVATE_KEY },
+ { "public-key", required_argument, NULL, ARG_PUBLIC_KEY },
+ { "json", required_argument, NULL, ARG_JSON },
+ { "phase", required_argument, NULL, ARG_PHASE },
+ {}
+ };
+
+ int c, r;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ /* Make sure the arguments list and the section list, stays in sync */
+ assert_cc(_ARG_SECTION_FIRST + _UNIFIED_SECTION_MAX == _ARG_SECTION_LAST + 1);
+
+ while ((c = getopt_long(argc, argv, "hjc", options, NULL)) >= 0)
+ switch (c) {
+
+ case 'h':
+ help(0, NULL, NULL);
+ return 0;
+
+ case ARG_VERSION:
+ return version();
+
+ case ARG_NO_PAGER:
+ arg_pager_flags |= PAGER_DISABLE;
+ break;
+
+ case _ARG_SECTION_FIRST..._ARG_SECTION_LAST: {
+ UnifiedSection section = c - _ARG_SECTION_FIRST;
+
+ r = parse_path_argument(optarg, /* suppress_root= */ false, arg_sections + section);
+ if (r < 0)
+ return r;
+ break;
+ }
+
+ case 'c':
+ arg_current = true;
+ break;
+
+ case ARG_BANK: {
+ const EVP_MD *implementation;
+
+ implementation = EVP_get_digestbyname(optarg);
+ if (!implementation)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown bank '%s', refusing.", optarg);
+
+ if (strv_extend(&arg_banks, EVP_MD_name(implementation)) < 0)
+ return log_oom();
+
+ break;
+ }
+
+ case ARG_PRIVATE_KEY:
+ r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_private_key);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case ARG_PUBLIC_KEY:
+ r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_public_key);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case ARG_TPM2_DEVICE: {
+ _cleanup_free_ char *device = NULL;
+
+ if (streq(optarg, "list"))
+ return tpm2_list_devices();
+
+ if (!streq(optarg, "auto")) {
+ device = strdup(optarg);
+ if (!device)
+ return log_oom();
+ }
+
+ free_and_replace(arg_tpm2_device, device);
+ break;
+ }
+
+ case 'j':
+ arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO;
+ break;
+
+ case ARG_JSON:
+ r = parse_json_argument(optarg, &arg_json_format_flags);
+ if (r <= 0)
+ return r;
+
+ break;
+
+ case ARG_PHASE: {
+ char *n;
+
+ n = normalize_phase(optarg);
+ if (!n)
+ return log_oom();
+
+ r = strv_consume(&arg_phase, TAKE_PTR(n));
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached();
+ }
+
+ if (strv_isempty(arg_banks)) {
+ /* If no banks are specifically selected, pick all known banks */
+ arg_banks = strv_new("SHA1", "SHA256", "SHA384", "SHA512");
+ if (!arg_banks)
+ return log_oom();
+ }
+
+ strv_sort(arg_banks);
+ strv_uniq(arg_banks);
+
+ if (arg_current)
+ for (UnifiedSection us = 0; us < _UNIFIED_SECTION_MAX; us++)
+ if (arg_sections[us])
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --current switch cannot be used in combination with --linux= and related switches.");
+
+ if (strv_isempty(arg_phase)) {
+ /* If no phases are specifically selected, pick everything from the beginning of the initrd
+ * to the beginning of shutdown. */
+ if (strv_extend_strv(&arg_phase,
+ STRV_MAKE("enter-initrd",
+ "enter-initrd:leave-initrd",
+ "enter-initrd:leave-initrd:sysinit",
+ "enter-initrd:leave-initrd:sysinit:ready"),
+ /* filter_duplicates= */ false) < 0)
+ return log_oom();
+ } else {
+ strv_sort(arg_phase);
+ strv_uniq(arg_phase);
+ }
+
+ _cleanup_free_ char *j = NULL;
+ j = strv_join(arg_phase, ", ");
+ if (!j)
+ return log_oom();
+
+ log_debug("Measuring boot phases: %s", j);
+ return 1;
+}
+
+/* The PCR 11 state for one specific bank */
+typedef struct PcrState {
+ char *bank;
+ const EVP_MD *md;
+ void *value;
+ size_t value_size;
+ void *saved_value; /* A copy of the original value we calculated, used by pcr_states_save()/pcr_states_restore() to come later back to */
+} PcrState;
+
+static void pcr_state_free_all(PcrState **pcr_state) {
+ assert(pcr_state);
+
+ if (!*pcr_state)
+ return;
+
+ for (size_t i = 0; (*pcr_state)[i].value; i++) {
+ free((*pcr_state)[i].bank);
+ free((*pcr_state)[i].value);
+ free((*pcr_state)[i].saved_value);
+ }
+
+ *pcr_state = mfree(*pcr_state);
+}
+
+static void evp_md_ctx_free_all(EVP_MD_CTX **md[]) {
+ assert(md);
+
+ if (!*md)
+ return;
+
+ for (size_t i = 0; (*md)[i]; i++)
+ EVP_MD_CTX_free((*md)[i]);
+
+ *md = mfree(*md);
+}
+
+static int pcr_state_extend(PcrState *pcr_state, const void *data, size_t sz) {
+ _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *mc = NULL;
+ unsigned value_size;
+
+ assert(pcr_state);
+ assert(data || sz == 0);
+ assert(pcr_state->md);
+ assert(pcr_state->value);
+ assert(pcr_state->value_size > 0);
+
+ /* Extends a (virtual) PCR by the given data */
+
+ mc = EVP_MD_CTX_new();
+ if (!mc)
+ return log_oom();
+
+ if (EVP_DigestInit_ex(mc, pcr_state->md, NULL) != 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize %s context.", pcr_state->bank);
+
+ /* First thing we do, is hash the old PCR value */
+ if (EVP_DigestUpdate(mc, pcr_state->value, pcr_state->value_size) != 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to run digest.");
+
+ /* Then, we hash the new data */
+ if (EVP_DigestUpdate(mc, data, sz) != 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to run digest.");
+
+ if (EVP_DigestFinal_ex(mc, pcr_state->value, &value_size) != 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to finalize hash context.");
+
+ assert(value_size == pcr_state->value_size);
+ return 0;
+}
+
+#define BUFFER_SIZE (16U * 1024U)
+
+static int measure_kernel(PcrState *pcr_states, size_t n) {
+ _cleanup_free_ void *buffer = NULL;
+ int r;
+
+ assert(n > 0);
+ assert(pcr_states);
+
+ /* Virtually measures the components of a unified kernel image into PCR 11 */
+
+ if (arg_current) {
+ /* Shortcut things, if we should just use the current PCR value */
+
+ for (size_t i = 0; i < n; i++) {
+ _cleanup_free_ char *p = NULL, *s = NULL;
+ _cleanup_free_ void *v = NULL;
+ size_t sz;
+
+ if (asprintf(&p, "/sys/class/tpm/tpm0/pcr-%s/%" PRIu32, pcr_states[i].bank, TPM_PCR_INDEX_KERNEL_IMAGE) < 0)
+ return log_oom();
+
+ r = read_virtual_file(p, 4096, &s, NULL);
+ if (r == -ENOENT && access("/sys/class/tpm/tpm0/", F_OK) >= 0)
+ return log_error_errno(r, "TPM device exists, but cannot open '%s'; either the kernel is too old, or selected PCR bank is not supported: %m", p);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read '%s': %m", p);
+
+ r = unhexmem(strstrip(s), SIZE_MAX, &v, &sz);
+ if (r < 0)
+ return log_error_errno(r, "Failed to decode PCR value '%s': %m", s);
+
+ assert(pcr_states[i].value_size == sz);
+ memcpy(pcr_states[i].value, v, sz);
+ }
+
+ return 0;
+ }
+
+ buffer = malloc(BUFFER_SIZE);
+ if (!buffer)
+ return log_oom();
+
+ for (UnifiedSection c = 0; c < _UNIFIED_SECTION_MAX; c++) {
+ _cleanup_(evp_md_ctx_free_all) EVP_MD_CTX **mdctx = NULL;
+ _cleanup_close_ int fd = -1;
+ uint64_t m = 0;
+
+ if (!arg_sections[c])
+ continue;
+
+ fd = open(arg_sections[c], O_RDONLY|O_CLOEXEC);
+ if (fd < 0)
+ return log_error_errno(errno, "Failed to open '%s': %m", arg_sections[c]);
+
+ /* Allocate one message digest context per bank (NULL terminated) */
+ mdctx = new0(EVP_MD_CTX*, n + 1);
+ if (!mdctx)
+ return log_oom();
+
+ for (size_t i = 0; i < n; i++) {
+ mdctx[i] = EVP_MD_CTX_new();
+ if (!mdctx[i])
+ return log_oom();
+
+ if (EVP_DigestInit_ex(mdctx[i], pcr_states[i].md, NULL) != 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize data %s context.", pcr_states[i].bank);
+ }
+
+ for (;;) {
+ ssize_t sz;
+
+ sz = read(fd, buffer, BUFFER_SIZE);
+ if (sz < 0)
+ return log_error_errno(errno, "Failed to read '%s': %m", arg_sections[c]);
+ if (sz == 0) /* EOF */
+ break;
+
+ for (size_t i = 0; i < n; i++)
+ if (EVP_DigestUpdate(mdctx[i], buffer, sz) != 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to run digest.");
+
+ m += sz;
+ }
+
+ fd = safe_close(fd);
+
+ if (m == 0) /* We skip over empty files, the stub does so too */
+ continue;
+
+ for (size_t i = 0; i < n; i++) {
+ _cleanup_free_ void *data_hash = NULL;
+ unsigned data_hash_size;
+
+ data_hash = malloc(pcr_states[i].value_size);
+ if (!data_hash)
+ return log_oom();
+
+ /* Measure name of section */
+ if (EVP_Digest(unified_sections[c], strlen(unified_sections[c]) + 1, data_hash, &data_hash_size, pcr_states[i].md, NULL) != 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to hash section name with %s.", pcr_states[i].bank);
+
+ assert(data_hash_size == (unsigned) pcr_states[i].value_size);
+
+ r = pcr_state_extend(pcr_states + i, data_hash, data_hash_size);
+ if (r < 0)
+ return r;
+
+ /* Retrieve hash of data and measure it */
+ if (EVP_DigestFinal_ex(mdctx[i], data_hash, &data_hash_size) != 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to finalize hash context.");
+
+ assert(data_hash_size == (unsigned) pcr_states[i].value_size);
+
+ r = pcr_state_extend(pcr_states + i, data_hash, data_hash_size);
+ if (r < 0)
+ return r;
+ }
+ }
+
+ return 0;
+}
+
+static int measure_phase(PcrState *pcr_states, size_t n, const char *phase) {
+ _cleanup_strv_free_ char **l = NULL;
+ int r;
+
+ assert(pcr_states);
+ assert(n > 0);
+
+ /* Measure a phase string into PCR 11. This splits up the "phase" expression at colons, and then
+ * virtually extends each specified word into PCR 11, to model how during boot we measure a series of
+ * words into PCR 11, one for each phase. */
+
+ l = strv_split(phase, ":");
+ if (!l)
+ return log_oom();
+
+ STRV_FOREACH(word, l) {
+ size_t wl;
+
+ if (isempty(*word))
+ continue;
+
+ wl = strlen(*word);
+
+ for (size_t i = 0; i < n; i++) { /* For each bank */
+ _cleanup_free_ void *b = NULL;
+ int bsz;
+
+ bsz = EVP_MD_size(pcr_states[i].md);
+ assert(bsz > 0);
+
+ b = malloc(bsz);
+ if (!b)
+ return log_oom();
+
+ /* First hash the word itself */
+ if (EVP_Digest(*word, wl, b, NULL, pcr_states[i].md, NULL) != 1)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to hash word '%s'.", *word);
+
+ /* And then extend the PCR with the resulting hash */
+ r = pcr_state_extend(pcr_states + i, b, bsz);
+ if (r < 0)
+ return r;
+ }
+ }
+
+ return 0;
+}
+
+static int pcr_states_allocate(PcrState **ret) {
+ _cleanup_(pcr_state_free_all) PcrState *pcr_states = NULL;
+ size_t n = 0;
+
+ pcr_states = new0(PcrState, strv_length(arg_banks) + 1);
+ if (!pcr_states)
+ return log_oom();
+
+ /* Allocate a PCR state structure, one for each bank */
+ STRV_FOREACH(d, arg_banks) {
+ const EVP_MD *implementation;
+ _cleanup_free_ void *v = NULL;
+ _cleanup_free_ char *b = NULL;
+ int sz;
+
+ assert_se(implementation = EVP_get_digestbyname(*d)); /* Must work, we already checked while parsing command line */
+
+ b = strdup(EVP_MD_name(implementation));
+ if (!b)
+ return log_oom();
+
+ sz = EVP_MD_size(implementation);
+ if (sz <= 0 || sz >= INT_MAX)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unexpected digest size: %i", sz);
+
+ v = malloc0(sz); /* initial PCR state is all zeroes */
+ if (!v)
+ return log_oom();
+
+ pcr_states[n++] = (struct PcrState) {
+ .bank = ascii_strlower(TAKE_PTR(b)),
+ .md = implementation,
+ .value = TAKE_PTR(v),
+ .value_size = sz,
+ };
+ }
+
+ *ret = TAKE_PTR(pcr_states);
+ return (int) n;
+}
+
+static int pcr_states_save(PcrState *pcr_states, size_t n) {
+ assert(pcr_states);
+ assert(n > 0);
+
+ for (size_t i = 0; i < n; i++) {
+ _cleanup_free_ void *saved = NULL;
+
+ if (!pcr_states[i].value)
+ continue;
+
+ saved = memdup(pcr_states[i].value, pcr_states[i].value_size);
+ if (!saved)
+ return log_oom();
+
+ free_and_replace(pcr_states[i].saved_value, saved);
+ }
+
+ return 0;
+}
+
+static void pcr_states_restore(PcrState *pcr_states, size_t n) {
+ assert(pcr_states);
+ assert(n > 0);
+
+ for (size_t i = 0; i < n; i++) {
+
+ assert(pcr_states[i].value);
+ assert(pcr_states[i].saved_value);
+
+ memcpy(pcr_states[i].value, pcr_states[i].saved_value, pcr_states[i].value_size);
+ }
+}
+
+static int verb_calculate(int argc, char *argv[], void *userdata) {
+ _cleanup_(json_variant_unrefp) JsonVariant *w = NULL;
+ _cleanup_(pcr_state_free_all) PcrState *pcr_states = NULL;
+ size_t n;
+ int r;
+
+ if (!arg_sections[UNIFIED_SECTION_LINUX] && !arg_current)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Either --linux= or --current must be specified, refusing.");
+
+ assert(!strv_isempty(arg_banks));
+ assert(!strv_isempty(arg_phase));
+
+ r = pcr_states_allocate(&pcr_states);
+ if (r < 0)
+ return r;
+
+ n = (size_t) r;
+
+ r = measure_kernel(pcr_states, n);
+ if (r < 0)
+ return r;
+
+ /* Save the current state, so that we later can restore to it. This way we can measure the PCR values
+ * for multiple different boot phases without heaving to start from zero each time */
+ r = pcr_states_save(pcr_states, n);
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH(phase, arg_phase) {
+
+ r = measure_phase(pcr_states, n, *phase);
+ if (r < 0)
+ return r;
+
+ for (size_t i = 0; i < n; i++) {
+ if (arg_json_format_flags & JSON_FORMAT_OFF) {
+ _cleanup_free_ char *hd = NULL;
+
+ if (i == 0) {
+ fflush(stdout);
+ fprintf(stderr, "%s# PCR[%" PRIu32 "] Phase <%s>%s\n",
+ ansi_grey(),
+ TPM_PCR_INDEX_KERNEL_IMAGE,
+ isempty(*phase) ? ":" : *phase,
+ ansi_normal());
+ fflush(stderr);
+ }
+
+ hd = hexmem(pcr_states[i].value, pcr_states[i].value_size);
+ if (!hd)
+ return log_oom();
+
+ printf("%" PRIu32 ":%s=%s\n", TPM_PCR_INDEX_KERNEL_IMAGE, pcr_states[i].bank, hd);
+ } else {
+ _cleanup_(json_variant_unrefp) JsonVariant *bv = NULL, *array = NULL;
+
+ array = json_variant_ref(json_variant_by_key(w, pcr_states[i].bank));
+
+ r = json_build(&bv,
+ JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_CONDITION(!isempty(*phase), "phase", JSON_BUILD_STRING(*phase)),
+ JSON_BUILD_PAIR("pcr", JSON_BUILD_INTEGER(TPM_PCR_INDEX_KERNEL_IMAGE)),
+ JSON_BUILD_PAIR("hash", JSON_BUILD_HEX(pcr_states[i].value, pcr_states[i].value_size))
+ )
+ );
+ if (r < 0)
+ return log_error_errno(r, "Failed to build JSON object: %m");
+
+ r = json_variant_append_array(&array, bv);
+ if (r < 0)
+ return log_error_errno(r, "Failed to append JSON object to array: %m");
+
+ r = json_variant_set_field(&w, pcr_states[i].bank, array);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add bank info to object: %m");
+ }
+ }
+
+ /* Return to the original kernel measurement for the next phase calculation */
+ pcr_states_restore(pcr_states, n);
+ }
+
+ if (!FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) {
+
+ if (arg_json_format_flags & (JSON_FORMAT_PRETTY|JSON_FORMAT_PRETTY_AUTO))
+ pager_open(arg_pager_flags);
+
+ json_variant_dump(w, arg_json_format_flags, stdout, NULL);
+ }
+
+ return 0;
+}
+
+static int verb_sign(int argc, char *argv[], void *userdata) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_(pcr_state_free_all) PcrState *pcr_states = NULL;
+ _cleanup_(EVP_PKEY_freep) EVP_PKEY *privkey = NULL, *pubkey = NULL;
+ _cleanup_(tpm2_context_destroy) struct tpm2_context c = {};
+ _cleanup_fclose_ FILE *privkeyf = NULL;
+ ESYS_TR session_handle = ESYS_TR_NONE;
+ TSS2_RC rc;
+ size_t n;
+ int r;
+
+ if (!arg_sections[UNIFIED_SECTION_LINUX] && !arg_current)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Either --linux= or --current must be specified, refusing.");
+
+ if (!arg_private_key)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No private key specified, use --private-key=.");
+
+ assert(!strv_isempty(arg_banks));
+ assert(!strv_isempty(arg_phase));
+
+ /* When signing we only support JSON output */
+ arg_json_format_flags &= ~JSON_FORMAT_OFF;
+
+ privkeyf = fopen(arg_private_key, "re");
+ if (!privkeyf)
+ return log_error_errno(errno, "Failed to open private key file '%s': %m", arg_private_key);
+
+ privkey = PEM_read_PrivateKey(privkeyf, NULL, NULL, NULL);
+ if (!privkey)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse private key '%s'.", arg_private_key);
+
+ if (arg_public_key) {
+ _cleanup_fclose_ FILE *pubkeyf = NULL;
+
+ pubkeyf = fopen(arg_public_key, "re");
+ if (!pubkeyf)
+ return log_error_errno(errno, "Failed to open public key file '%s': %m", arg_public_key);
+
+ pubkey = PEM_read_PUBKEY(pubkeyf, NULL, NULL, NULL);
+ if (!pubkey)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse public key '%s'.", arg_public_key);
+ } else {
+ _cleanup_free_ char *data = NULL;
+ _cleanup_fclose_ FILE *tf = NULL;
+ size_t sz;
+
+ /* No public key was specified, let's derive it automatically, if we can */
+
+ tf = open_memstream_unlocked(&data, &sz);
+ if (!tf)
+ return log_oom();
+
+ if (i2d_PUBKEY_fp(tf, privkey) != 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to extract public key from private key file '%s'.", arg_private_key);
+
+ fflush(tf);
+ rewind(tf);
+
+ if (!d2i_PUBKEY_fp(tf, &pubkey))
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse extracted public key of private key file '%s'.", arg_private_key);
+ }
+
+ r = pcr_states_allocate(&pcr_states);
+ if (r < 0)
+ return r;
+
+ n = (size_t) r;
+
+ r = measure_kernel(pcr_states, n);
+ if (r < 0)
+ return r;
+
+ r = pcr_states_save(pcr_states, n);
+ if (r < 0)
+ return r;
+
+ r = dlopen_tpm2();
+ if (r < 0)
+ return r;
+
+ r = tpm2_context_init(arg_tpm2_device, &c);
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH(phase, arg_phase) {
+
+ r = measure_phase(pcr_states, n, *phase);
+ if (r < 0)
+ return r;
+
+ for (size_t i = 0; i < n; i++) {
+ static const TPMT_SYM_DEF symmetric = {
+ .algorithm = TPM2_ALG_AES,
+ .keyBits.aes = 128,
+ .mode.aes = TPM2_ALG_CFB,
+ };
+ PcrState *p = pcr_states + i;
+
+ rc = sym_Esys_StartAuthSession(
+ c.esys_context,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ NULL,
+ TPM2_SE_TRIAL,
+ &symmetric,
+ TPM2_ALG_SHA256,
+ &session_handle);
+ if (rc != TSS2_RC_SUCCESS) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to open session in TPM: %s", sym_Tss2_RC_Decode(rc));
+ goto finish;
+ }
+
+ /* Generate a single hash value from the PCRs included in our policy. Given that that's
+ * exactly one, the calculation is trivial. */
+ TPM2B_DIGEST intermediate_digest = {
+ .size = SHA256_DIGEST_SIZE,
+ };
+ assert(sizeof(intermediate_digest.buffer) >= SHA256_DIGEST_SIZE);
+ sha256_direct(p->value, p->value_size, intermediate_digest.buffer);
+
+ int tpmalg = tpm2_pcr_bank_from_string(EVP_MD_name(p->md));
+ if (tpmalg < 0) {
+ log_error_errno(tpmalg, "Unsupported PCR bank");
+ goto finish;
+ }
+
+ TPML_PCR_SELECTION pcr_selection;
+ tpm2_pcr_mask_to_selection(1 << TPM_PCR_INDEX_KERNEL_IMAGE, tpmalg, &pcr_selection);
+
+ rc = sym_Esys_PolicyPCR(
+ c.esys_context,
+ session_handle,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ &intermediate_digest,
+ &pcr_selection);
+ if (rc != TSS2_RC_SUCCESS) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to push PCR policy into TPM: %s", sym_Tss2_RC_Decode(rc));
+ goto finish;
+ }
+
+ _cleanup_(Esys_Freep) TPM2B_DIGEST *pcr_policy_digest = NULL;
+ rc = sym_Esys_PolicyGetDigest(
+ c.esys_context,
+ session_handle,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ &pcr_policy_digest);
+ if (rc != TSS2_RC_SUCCESS) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to get policy digest from TPM: %s", sym_Tss2_RC_Decode(rc));
+ goto finish;
+ }
+
+ session_handle = tpm2_flush_context_verbose(c.esys_context, session_handle);
+
+ _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX* mdctx = NULL;
+ mdctx = EVP_MD_CTX_new();
+ if (!mdctx) {
+ r = log_oom();
+ goto finish;
+ }
+
+ if (EVP_DigestSignInit(mdctx, NULL, p->md, NULL, privkey) != 1) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to initialize signature context.");
+ goto finish;
+ }
+
+ if (EVP_DigestSignUpdate(mdctx, pcr_policy_digest->buffer, pcr_policy_digest->size) != 1) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to sign data.");
+ goto finish;
+ }
+
+ size_t ss;
+ if (EVP_DigestSignFinal(mdctx, NULL, &ss) != 1) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to finalize signature");
+ goto finish;
+ }
+
+ _cleanup_free_ void *sig = malloc(ss);
+ if (!sig) {
+ r = log_oom();
+ goto finish;
+ }
+
+ if (EVP_DigestSignFinal(mdctx, sig, &ss) != 1) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to acquire signature data");
+ goto finish;
+ }
+
+ _cleanup_free_ void *pubkey_fp = NULL;
+ size_t pubkey_fp_size = 0;
+ r = pubkey_fingerprint(pubkey, EVP_sha256(), &pubkey_fp, &pubkey_fp_size);
+ if (r < 0)
+ goto finish;
+
+ _cleanup_(json_variant_unrefp) JsonVariant *a = NULL;
+ r = tpm2_make_pcr_json_array(UINT64_C(1) << TPM_PCR_INDEX_KERNEL_IMAGE, &a);
+ if (r < 0) {
+ log_error_errno(r, "Failed to build JSON PCR mask array: %m");
+ goto finish;
+ }
+
+ _cleanup_(json_variant_unrefp) JsonVariant *bv = NULL;
+ r = json_build(&bv, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("pcrs", JSON_BUILD_VARIANT(a)), /* PCR mask */
+ JSON_BUILD_PAIR("pkfp", JSON_BUILD_HEX(pubkey_fp, pubkey_fp_size)), /* SHA256 fingerprint of public key (DER) used for the signature */
+ JSON_BUILD_PAIR("pol", JSON_BUILD_HEX(pcr_policy_digest->buffer, pcr_policy_digest->size)), /* TPM2 policy hash that is signed */
+ JSON_BUILD_PAIR("sig", JSON_BUILD_BASE64(sig, ss)))); /* signature data */
+ if (r < 0) {
+ log_error_errno(r, "Failed to build JSON object: %m");
+ goto finish;
+ }
+
+ _cleanup_(json_variant_unrefp) JsonVariant *av = NULL;
+ av = json_variant_ref(json_variant_by_key(v, p->bank));
+
+ r = json_variant_append_array(&av, bv);
+ if (r < 0) {
+ log_error_errno(r, "Failed to append JSON object: %m");
+ goto finish;
+ }
+
+ r = json_variant_set_field(&v, p->bank, av);
+ if (r < 0) {
+ log_error_errno(r, "Failed to add JSON field: %m");
+ goto finish;
+ }
+ }
+
+ /* Return to the original kernel measurement for the next phase calculation */
+ pcr_states_restore(pcr_states, n);
+ }
+
+ if (arg_json_format_flags & (JSON_FORMAT_PRETTY|JSON_FORMAT_PRETTY_AUTO))
+ pager_open(arg_pager_flags);
+
+ json_variant_dump(v, arg_json_format_flags, stdout, NULL);
+ r = 0;
+
+finish:
+ session_handle = tpm2_flush_context_verbose(c.esys_context, session_handle);
+ return r;
+}
+
+static int compare_reported_pcr_nr(uint32_t pcr, const char *varname, const char *description) {
+ _cleanup_free_ char *s = NULL;
+ uint32_t v;
+ int r;
+
+ r = efi_get_variable_string(varname, &s);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
+ return log_error_errno(r, "Failed to read EFI variable '%s': %m", varname);
+
+ r = safe_atou32(s, &v);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse EFI variable '%s': %s", varname, s);
+
+ if (pcr != v)
+ log_warning("PCR number reported by stub for %s (%" PRIu32 ") different from our expectation (%" PRIu32 ").\n"
+ "The measurements are likely inconsistent.", description, v, pcr);
+
+ return 0;
+}
+
+static int validate_stub(void) {
+ uint64_t features;
+ bool found = false;
+ int r;
+
+ if (tpm2_support() != TPM2_SUPPORT_FULL)
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Sorry, system lacks full TPM2 support.");
+
+ r = efi_stub_get_features(&features);
+ if (r < 0)
+ return log_error_errno(r, "Unable to get stub features: %m");
+
+ if (!FLAGS_SET(features, EFI_STUB_FEATURE_THREE_PCRS))
+ log_warning("Warning: current kernel image does not support measuring itself, the command line or initrd system extension images.\n"
+ "The PCR measurements seen are unlikely to be valid.");
+
+ r = compare_reported_pcr_nr(TPM_PCR_INDEX_KERNEL_IMAGE, EFI_LOADER_VARIABLE("StubPcrKernelImage"), "kernel image");
+ if (r < 0)
+ return r;
+
+ r = compare_reported_pcr_nr(TPM_PCR_INDEX_KERNEL_PARAMETERS, EFI_LOADER_VARIABLE("StubPcrKernelParameters"), "kernel parameters");
+ if (r < 0)
+ return r;
+
+ r = compare_reported_pcr_nr(TPM_PCR_INDEX_INITRD_SYSEXTS, EFI_LOADER_VARIABLE("StubPcrInitRDSysExts"), "initrd system extension images");
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH(bank, arg_banks) {
+ _cleanup_free_ char *b = NULL, *p = NULL;
+
+ b = strdup(*bank);
+ if (!b)
+ return log_oom();
+
+ if (asprintf(&p, "/sys/class/tpm/tpm0/pcr-%s/", ascii_strlower(b)) < 0)
+ return log_oom();
+
+ if (access(p, F_OK) < 0) {
+ if (errno != ENOENT)
+ return log_error_errno(errno, "Failed to detect if '%s' exists: %m", b);
+ } else
+ found = true;
+ }
+
+ if (!found)
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "None of the select PCR banks appear to exist.");
+
+ return 0;
+}
+
+static int verb_status(int argc, char *argv[], void *userdata) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+
+ static const struct {
+ uint32_t nr;
+ const char *description;
+ } relevant_pcrs[] = {
+ { TPM_PCR_INDEX_KERNEL_IMAGE, "Unified Kernel Image" },
+ { TPM_PCR_INDEX_KERNEL_PARAMETERS, "Kernel Parameters" },
+ { TPM_PCR_INDEX_INITRD_SYSEXTS, "initrd System Extensions" },
+ };
+
+ int r;
+
+ r = validate_stub();
+ if (r < 0)
+ return r;
+
+ for (size_t i = 0; i < ELEMENTSOF(relevant_pcrs); i++) {
+
+ STRV_FOREACH(bank, arg_banks) {
+ _cleanup_free_ char *b = NULL, *p = NULL, *s = NULL;
+ _cleanup_free_ void *h = NULL;
+ size_t l;
+
+ b = strdup(*bank);
+ if (!b)
+ return log_oom();
+
+ if (asprintf(&p, "/sys/class/tpm/tpm0/pcr-%s/%" PRIu32, ascii_strlower(b), relevant_pcrs[i].nr) < 0)
+ return log_oom();
+
+ r = read_virtual_file(p, 4096, &s, NULL);
+ if (r == -ENOENT)
+ continue;
+ if (r < 0)
+ return log_error_errno(r, "Failed to read '%s': %m", p);
+
+ r = unhexmem(strstrip(s), SIZE_MAX, &h, &l);
+ if (r < 0)
+ return log_error_errno(r, "Failed to decode PCR value '%s': %m", s);
+
+ if (arg_json_format_flags & JSON_FORMAT_OFF) {
+ _cleanup_free_ char *f = NULL;
+
+ f = hexmem(h, l);
+ if (!h)
+ return log_oom();
+
+ if (bank == arg_banks) {
+ /* before the first line for each PCR, write a short descriptive text to
+ * stderr, and leave the primary content on stdout */
+ fflush(stdout);
+ fprintf(stderr, "%s# PCR[%" PRIu32 "] %s%s%s\n",
+ ansi_grey(),
+ relevant_pcrs[i].nr,
+ relevant_pcrs[i].description,
+ memeqzero(h, l) ? " (NOT SET!)" : "",
+ ansi_normal());
+ fflush(stderr);
+ }
+
+ printf("%" PRIu32 ":%s=%s\n", relevant_pcrs[i].nr, b, f);
+
+ } else {
+ _cleanup_(json_variant_unrefp) JsonVariant *bv = NULL, *a = NULL;
+
+ r = json_build(&bv,
+ JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("pcr", JSON_BUILD_INTEGER(relevant_pcrs[i].nr)),
+ JSON_BUILD_PAIR("hash", JSON_BUILD_HEX(h, l))
+ )
+ );
+ if (r < 0)
+ return log_error_errno(r, "Failed to build JSON object: %m");
+
+ a = json_variant_ref(json_variant_by_key(v, b));
+
+ r = json_variant_append_array(&a, bv);
+ if (r < 0)
+ return log_error_errno(r, "Failed to append PCR entry to JSON array: %m");
+
+ r = json_variant_set_field(&v, b, a);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add bank info to object: %m");
+ }
+ }
+ }
+
+ if (!FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) {
+ if (arg_json_format_flags & (JSON_FORMAT_PRETTY|JSON_FORMAT_PRETTY_AUTO))
+ pager_open(arg_pager_flags);
+
+ json_variant_dump(v, arg_json_format_flags, stdout, NULL);
+ }
+
+ return 0;
+}
+
+static int measure_main(int argc, char *argv[]) {
+ static const Verb verbs[] = {
+ { "help", VERB_ANY, VERB_ANY, 0, help },
+ { "status", VERB_ANY, 1, VERB_DEFAULT, verb_status },
+ { "calculate", VERB_ANY, 1, 0, verb_calculate },
+ { "sign", VERB_ANY, 1, 0, verb_sign },
+ {}
+ };
+
+ return dispatch_verb(argc, argv, verbs, NULL);
+}
+
+static int run(int argc, char *argv[]) {
+ int r;
+
+ log_show_color(true);
+ log_parse_environment();
+ log_open();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ return measure_main(argc, argv);
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/boot/pcrphase.c b/src/boot/pcrphase.c
new file mode 100644
index 0000000..2808ee8
--- /dev/null
+++ b/src/boot/pcrphase.c
@@ -0,0 +1,275 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.h>
+
+#include <sd-messages.h>
+
+#include "efivars.h"
+#include "main-func.h"
+#include "openssl-util.h"
+#include "parse-util.h"
+#include "pretty-print.h"
+#include "tpm-pcr.h"
+#include "tpm2-util.h"
+
+static bool arg_graceful = false;
+static char *arg_tpm2_device = NULL;
+static char **arg_banks = NULL;
+
+STATIC_DESTRUCTOR_REGISTER(arg_banks, strv_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep);
+
+static int help(int argc, char *argv[], void *userdata) {
+ _cleanup_free_ char *link = NULL;
+ int r;
+
+ r = terminal_urlify_man("systemd-pcrphase", "8", &link);
+ if (r < 0)
+ return log_oom();
+
+ printf("%1$s [OPTIONS...] WORD ...\n"
+ "\n%5$sMeasure boot phase into TPM2 PCR 11.%6$s\n"
+ "\n%3$sOptions:%4$s\n"
+ " -h --help Show this help\n"
+ " --version Print version\n"
+ " --bank=DIGEST Select TPM bank (SHA1, SHA256)\n"
+ " --tpm2-device=PATH Use specified TPM2 device\n"
+ " --graceful Exit gracefully if no TPM2 device is found\n"
+ "\nSee the %2$s for details.\n",
+ program_invocation_short_name,
+ link,
+ ansi_underline(),
+ ansi_normal(),
+ ansi_highlight(),
+ ansi_normal());
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+ enum {
+ ARG_VERSION = 0x100,
+ ARG_BANK,
+ ARG_TPM2_DEVICE,
+ ARG_GRACEFUL,
+ };
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, ARG_VERSION },
+ { "bank", required_argument, NULL, ARG_BANK },
+ { "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE },
+ { "graceful", no_argument, NULL, ARG_GRACEFUL },
+ {}
+ };
+
+ int c;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0)
+ switch (c) {
+
+ case 'h':
+ help(0, NULL, NULL);
+ return 0;
+
+ case ARG_VERSION:
+ return version();
+
+ case ARG_BANK: {
+ const EVP_MD *implementation;
+
+ implementation = EVP_get_digestbyname(optarg);
+ if (!implementation)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown bank '%s', refusing.", optarg);
+
+ if (strv_extend(&arg_banks, EVP_MD_name(implementation)) < 0)
+ return log_oom();
+
+ break;
+ }
+
+ case ARG_TPM2_DEVICE: {
+ _cleanup_free_ char *device = NULL;
+
+ if (streq(optarg, "list"))
+ return tpm2_list_devices();
+
+ if (!streq(optarg, "auto")) {
+ device = strdup(optarg);
+ if (!device)
+ return log_oom();
+ }
+
+ free_and_replace(arg_tpm2_device, device);
+ break;
+ }
+
+ case ARG_GRACEFUL:
+ arg_graceful = true;
+ break;
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached();
+ }
+
+ return 1;
+}
+
+static int determine_banks(struct tpm2_context *c) {
+ _cleanup_free_ TPMI_ALG_HASH *algs = NULL;
+ int n_algs, r;
+
+ assert(c);
+
+ if (!strv_isempty(arg_banks)) /* Explicitly configured? Then use that */
+ return 0;
+
+ n_algs = tpm2_get_good_pcr_banks(c->esys_context, UINT32_C(1) << TPM_PCR_INDEX_KERNEL_IMAGE, &algs);
+ if (n_algs <= 0)
+ return n_algs;
+
+ for (int i = 0; i < n_algs; i++) {
+ const EVP_MD *implementation;
+ const char *salg;
+
+ salg = tpm2_pcr_bank_to_string(algs[i]);
+ if (!salg)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "TPM2 operates with unknown PCR algorithm, can't measure.");
+
+ implementation = EVP_get_digestbyname(salg);
+ if (!implementation)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "TPM2 operates with unsupported PCR algorithm, can't measure.");
+
+ r = strv_extend(&arg_banks, EVP_MD_name(implementation));
+ if (r < 0)
+ return log_oom();
+ }
+
+ return 0;
+}
+
+static int run(int argc, char *argv[]) {
+ _cleanup_(tpm2_context_destroy) struct tpm2_context c = {};
+ _cleanup_free_ char *joined = NULL, *pcr_string = NULL;
+ const char *word;
+ unsigned pcr_nr;
+ size_t length;
+ TSS2_RC rc;
+ int r;
+
+ log_setup();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ if (optind+1 != argc)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected a single argument.");
+
+ word = argv[optind];
+
+ /* Refuse to measure an empty word. We want to be able to write the series of measured words
+ * separated by colons, where multiple separating colons are collapsed. Thus it makes sense to
+ * disallow an empty word to avoid ambiguities. */
+ if (isempty(word))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "String to measure cannot be empty, refusing.");
+
+ if (arg_graceful && tpm2_support() != TPM2_SUPPORT_FULL) {
+ log_notice("No complete TPM2 support detected, exiting gracefully.");
+ return EXIT_SUCCESS;
+ }
+
+ length = strlen(word);
+
+ /* Skip logic if sd-stub is not used, after all PCR 11 might have a very different purpose then. */
+ r = efi_get_variable_string(EFI_LOADER_VARIABLE(StubPcrKernelImage), &pcr_string);
+ if (r == -ENOENT) {
+ log_info("Kernel stub did not measure kernel image into PCR %u, skipping measurement.", TPM_PCR_INDEX_KERNEL_IMAGE);
+ return EXIT_SUCCESS;
+ }
+ if (r < 0)
+ return log_error_errno(r, "Failed to read StubPcrKernelImage EFI variable: %m");
+
+ /* Let's validate that the stub announced PCR 11 as we expected. */
+ r = safe_atou(pcr_string, &pcr_nr);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse StubPcrKernelImage EFI variable: %s", pcr_string);
+ if (pcr_nr != TPM_PCR_INDEX_KERNEL_IMAGE)
+ return log_error_errno(SYNTHETIC_ERRNO(EREMOTE), "Kernel stub measured kernel image into PCR %u, which is different than expected %u.", pcr_nr, TPM_PCR_INDEX_KERNEL_IMAGE);
+
+ r = dlopen_tpm2();
+ if (r < 0)
+ return log_error_errno(r, "Failed to load TPM2 libraries: %m");
+
+ r = tpm2_context_init(arg_tpm2_device, &c);
+ if (r < 0)
+ return r;
+
+ r = determine_banks(&c);
+ if (r < 0)
+ return r;
+ if (strv_isempty(arg_banks)) /* Still none? */
+ return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Found a TPM2 without enabled PCR banks. Can't operate.");
+
+ TPML_DIGEST_VALUES values = {};
+ STRV_FOREACH(bank, arg_banks) {
+ const EVP_MD *implementation;
+ int id;
+
+ assert_se(implementation = EVP_get_digestbyname(*bank));
+
+ if (values.count >= ELEMENTSOF(values.digests))
+ return log_error_errno(SYNTHETIC_ERRNO(E2BIG), "Too many banks selected.");
+
+ if ((size_t) EVP_MD_size(implementation) > sizeof(values.digests[values.count].digest))
+ return log_error_errno(SYNTHETIC_ERRNO(E2BIG), "Hash result too large for TPM2.");
+
+ id = tpm2_pcr_bank_from_string(EVP_MD_name(implementation));
+ if (id < 0)
+ return log_error_errno(id, "Can't map hash name to TPM2.");
+
+ values.digests[values.count].hashAlg = id;
+
+ if (EVP_Digest(word, length, (unsigned char*) &values.digests[values.count].digest, NULL, implementation, NULL) != 1)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to hash word.");
+
+ values.count++;
+ }
+
+ joined = strv_join(arg_banks, ", ");
+ if (!joined)
+ return log_oom();
+
+ log_debug("Measuring '%s' into PCR index %u, banks %s.", word, TPM_PCR_INDEX_KERNEL_IMAGE, joined);
+
+ rc = sym_Esys_PCR_Extend(
+ c.esys_context,
+ ESYS_TR_PCR0 + TPM_PCR_INDEX_KERNEL_IMAGE, /* → PCR 11 */
+ ESYS_TR_PASSWORD,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ &values);
+ if (rc != TSS2_RC_SUCCESS)
+ return log_error_errno(
+ SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to measure '%s': %s",
+ word,
+ sym_Tss2_RC_Decode(rc));
+
+ log_struct(LOG_INFO,
+ "MESSAGE_ID=" SD_MESSAGE_TPM_PCR_EXTEND_STR,
+ LOG_MESSAGE("Successfully extended PCR index %u with '%s' (banks %s).", TPM_PCR_INDEX_KERNEL_IMAGE, word, joined),
+ "MEASURING=%s", word,
+ "PCR=%u", TPM_PCR_INDEX_KERNEL_IMAGE,
+ "BANKS=%s", joined);
+
+ return EXIT_SUCCESS;
+}
+
+DEFINE_MAIN_FUNCTION(run);