diff options
Diffstat (limited to '')
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, ¤t, &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); |